├── .devcontainer └── devcontainer.json ├── .github └── workflows │ ├── auto_publish.yml │ └── test.yml ├── .gitignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── Dockerfile ├── LICENSE.md ├── Makefile ├── README.md ├── poetry.lock ├── pyproject.toml ├── script └── test ├── src └── function_pipes │ ├── __init__.py │ ├── pipes.jinja-py │ ├── py.typed │ ├── with_paramspec │ └── function_pipes.py │ └── without_paramspec │ └── function_pipes.py ├── tests ├── __init__.py ├── test_bridge.py ├── test_errors.py ├── test_fast.py ├── test_functional_equiv.py ├── test_meta.py └── test_speed.py └── typesafety ├── pipetype.py └── quickstart.py /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "function_pipes", 3 | "build": { 4 | "dockerfile": "../Dockerfile" 5 | }, 6 | "extensions": [ 7 | "ms-vscode.test-adapter-converter", 8 | "bungcip.better-toml", 9 | "ms-python.python", 10 | "ms-python.vscode-pylance", 11 | "ms-azuretools.vscode-docker", 12 | "valentjn.vscode-ltex" 13 | ] 14 | } -------------------------------------------------------------------------------- /.github/workflows/auto_publish.yml: -------------------------------------------------------------------------------- 1 | # Tools to publish package to pypi automatically 2 | # on update of poetry version. 3 | # Will also update tags on automatic release. 4 | 5 | name: "Publish package" 6 | 7 | # don't allow multiple 'identical' processes to run. A second push should cancel the job from the first one. 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}-${{ github.event.inputs.pypi }}-${{ github.event.inputs.testpypi }} 10 | cancel-in-progress: true 11 | 12 | on: 13 | workflow_dispatch: 14 | inputs: 15 | pypi: 16 | description: Force to pypi 17 | type: boolean 18 | default: false 19 | testpypi: 20 | description: Force to testpypi 21 | type: boolean 22 | default: false 23 | push: 24 | branches: [main] 25 | 26 | jobs: 27 | 28 | # run the tests first, if this fails nothing continues 29 | test: 30 | uses: ./.github/workflows/test.yml 31 | 32 | # run auto either if nothing explicit forced in workflow or it is a push event 33 | publish-auto: 34 | if: ${{ (github.event.inputs.testpypi == 'false' && github.event.inputs.pypi == 'false') || github.event_name == 'push' }} 35 | needs: test 36 | runs-on: ubuntu-latest 37 | steps: 38 | 39 | - uses: actions/checkout@v3 40 | 41 | - name: Fetch repo name 42 | id: repo_name 43 | uses: ajparsons/action-repo-name@main 44 | with: 45 | github_repo: ${{ github.repository }} 46 | 47 | - id: get_status 48 | name: get_status 49 | uses: ajparsons/compare-pypi-poetry-version@main 50 | with: 51 | package_name: ${{ steps.repo_name.outputs.repo_name }} 52 | 53 | - name: Update git tags 54 | # if: ${{ steps.get_status.outputs.pypi_version_difference == 'true' }} 55 | uses: ajparsons/semver-to-tag@main 56 | with: 57 | semver: ${{ steps.get_status.outputs.repo_poetry_version }} 58 | update_tags: true 59 | 60 | - name: Build and publish to pypi 61 | if: ${{ steps.get_status.outputs.pypi_version_difference == 'true'}} 62 | uses: JRubics/poetry-publish@v1.11 63 | with: 64 | pypi_token: ${{ secrets.PYPI_TOKEN }} 65 | 66 | # run manual if one of the boolean buttons for workflow was used 67 | # this can force the initial creation of the package 68 | publish-manual: 69 | if: ${{ github.event.inputs.testpypi == 'true' || github.event.inputs.pypi == 'true' }} 70 | needs: test 71 | runs-on: ubuntu-latest 72 | steps: 73 | 74 | - uses: actions/checkout@v2 75 | 76 | - name: Build and publish to pypi 77 | if: ${{ github.event.inputs.pypi == 'true' }} 78 | uses: JRubics/poetry-publish@v1.11 79 | with: 80 | pypi_token: ${{ secrets.PYPI_TOKEN }} 81 | 82 | - name: Build and publish to testpypi 83 | if: ${{ github.event.inputs.testpypi == 'true' }} 84 | uses: JRubics/poetry-publish@v1.11 85 | with: 86 | pypi_token: ${{ secrets.TEST_PYPI_TOKEN }} 87 | repository_name: "testpypi" 88 | repository_url: "https://test.pypi.org/legacy/" -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: 4 | push: 5 | branches-ignore: [ main ] 6 | pull_request: 7 | branches-ignore: [ main ] 8 | workflow_call: 9 | workflow_dispatch: 10 | 11 | jobs: 12 | test: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | python-version: ["3.8", "3.9", "3.10"] 17 | poetry-version: ["1.1.13"] 18 | 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | 24 | - name: Set up Python ${{ matrix.python-version }} 25 | uses: actions/setup-python@v2 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | 29 | - name: Install poetry ${{ matrix.poetry-version }} 30 | run: | 31 | python -m ensurepip 32 | python -m pip install --upgrade pip 33 | python -m pip install poetry==${{ matrix.poetry-version }} 34 | 35 | - name: Install dependencies 36 | shell: bash 37 | run: | 38 | python -m poetry config virtualenvs.create false 39 | python -m poetry install 40 | 41 | - name: Test with pytest 42 | shell: bash 43 | run: | 44 | python -m pytest -v tests 45 | 46 | - name: Test with pyright 47 | uses: jakebailey/pyright-action@v1 48 | with: 49 | extra-args: src 50 | 51 | - name: Test with black 52 | uses: psf/black@stable -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .settings/ 3 | .project 4 | .metadata 5 | .gradle 6 | tmp/ 7 | *.tmp 8 | *.bak 9 | *.swp 10 | *~.nib 11 | local.properties 12 | .classpath 13 | .settings/ 14 | __pycache__ 15 | dist -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.linting.pylintEnabled": true, 3 | "python.defaultInterpreterPath": "/usr/local/bin/python", 4 | "python.terminal.activateEnvironment": false, 5 | "python.formatting.provider": "black", 6 | "python.analysis.typeCheckingMode": "strict", 7 | "editor.formatOnSave": true, 8 | "editor.codeActionsOnSave": { 9 | "source.organizeImports": true 10 | }, 11 | "ltex.language": "en-GB", 12 | "files.exclude": { 13 | "**/.git": true, 14 | "**/.svn": true, 15 | "**/.hg": true, 16 | "**/CVS": true, 17 | "**/.DS_Store": true, 18 | "**/*.pyc": { 19 | "when": "$(basename).py" 20 | }, 21 | "**/__pycache__": true 22 | }, 23 | "files.associations": { 24 | "**/*.jinja-py": "jinja-py", 25 | "**/*.html": "html", 26 | "**/templates/**/*.html": "django-html", 27 | "**/templates/**/*": "django-txt", 28 | "**/requirements{/**,*}.{txt,in}": "pip-requirements" 29 | }, 30 | "python.linting.pylintArgs": [ 31 | "--max-line-length=88", 32 | "--disable=C0103,E1101,E1123,W0102", 33 | ], 34 | "jupyter.jupyterServerType": "local", 35 | "[markdown]": { 36 | "editor.quickSuggestions": { 37 | "comments": "on", 38 | "strings": "on", 39 | "other": "on" 40 | } 41 | }, 42 | "python.testing.pytestArgs": [ 43 | "tests/" 44 | ], 45 | "python.testing.unittestEnabled": false, 46 | "python.testing.pytestEnabled": true 47 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.1.2] - 2022-10-16 9 | ### Changed 10 | - Adjusted deploy Github Action (testing effectiveness) 11 | 12 | ## [0.1.1] - 2022-07-10 13 | ### Changed 14 | - More descriptive module docstrings 15 | 16 | 17 | ## [0.1.0] - 2022-07-10 18 | ### Added 19 | - Initial version with passing tests 20 | 21 | 22 | [comment]: # (Template for updates) 23 | ## [x.x.x] - YYYY-MM-DD 24 | ### Added 25 | - Anything added since last version 26 | ### Changed 27 | - Anything changed from last version 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-bullseye 2 | 3 | ENV DEBIAN_FRONTEND noninteractive 4 | COPY pyproject.toml poetry.loc[k] / 5 | RUN curl -sSL https://install.python-poetry.org | python - && \ 6 | echo "export PATH=\"/root/.local/bin:$PATH\"" > ~/.bashrc && \ 7 | export PATH="/root/.local/bin:$PATH" && \ 8 | poetry config virtualenvs.create false && \ 9 | poetry self update --preview && \ 10 | poetry self add poetry-bumpversion && \ 11 | poetry install && \ 12 | echo "/workspaces/function-pipes/src/" > /usr/local/lib/python3.10/site-packages/function_pipes.pth -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Alex Parsons 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 16 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 18 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 19 | OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | export arg_limit=20 && export param_spec=1 && j2 src/function_pipes/pipes.jinja-py > src/function_pipes/with_paramspec/function_pipes.py 3 | black src/function_pipes/with_paramspec/function_pipes.py 4 | export arg_limit=20 && export param_spec=0 && j2 src/function_pipes/pipes.jinja-py > src/function_pipes/without_paramspec/function_pipes.py 5 | black src/function_pipes/without_paramspec/function_pipes.py -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # function-pipes 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/function-pipes.svg)](https://pypi.org/project/function-pipes/) 4 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/inkleby/function-pipes/blob/main/LICENSE.md) 5 | [![Copy and Paste](https://img.shields.io/badge/Copy%20%2B%20Paste%3F-yes!-blue)](#install) 6 | 7 | Fast, type-hinted python equivalent for R pipes. 8 | 9 | This decorator only relies on the standard library, so can just be copied into a project as a single file. 10 | 11 | # Why is this needed? 12 | 13 | Various languages have versions of a 'pipe' syntax, where a value can be passed through a succession of different functions before returning the final value. 14 | 15 | This means you can avoid syntax like the below, where the sequence is hard to read (especially if extra arguments are introduced). 16 | 17 | ```python 18 | a = c(b(a(value))) 19 | ``` 20 | 21 | In Python, there is not a good built-in way of doing this, and other attempts at a pipe do not play nice with type hinting. 22 | 23 | This library has a very simple API, and does the fiddly bits behind the scenes to keep the pipe fast. 24 | 25 | ## The pipe 26 | 27 | There is a `pipe`, function which expects a value and then a list of callables. 28 | 29 | ```python 30 | from function_pipes import pipe 31 | 32 | value = pipe(5, lambda x: x + 2, str) 33 | value == "7" 34 | 35 | ``` 36 | 37 | ## No special form for extra arguments, small special case for functions that don't return a value 38 | 39 | There is no bespoke syntax for passing in extra arguments or moving where the pipe's current value is placed - just use a lambda. This is a well understood approach, that is compatible with type hinting. In the above, `value` will be recognised as a string, but the x is understood as an int. 40 | 41 | There is a small bit of bespoke syntax for when you want to pass something through a function, but that function doesn't return the result to the next function. Here the `pipe_bridge` function will wrap another function, pass the function into it, and continue onwards. The following will print `7`, before passing the value on. 42 | 43 | ```python 44 | from function_pipes import pipe, pipe_bridge 45 | 46 | value = pipe(5, lambda x: x + 2, pipe_bridge(print), str) 47 | value == "7" 48 | 49 | ``` 50 | 51 | ## Merging functions to use later 52 | 53 | There is also a `pipeline`, which given a set of functions will return a function which a value can be passed into. Where possible based on other hints, this will hint the input and output variable types. 54 | 55 | ```python 56 | from function_pipes import pipeline 57 | 58 | func = pipeline(lambda x: x + 2, str) 59 | func(5) == "7" 60 | 61 | ``` 62 | 63 | ## Optimising use of pipes 64 | 65 | There's work behind the scenes to minimise the overhead of using the pipe, but it is still adding a function call. If you want the readability of the pipe *and* the speed of the native ugly approach you can use the `@fast_pipes` decorator. This rewrites the function it is called on to expand out the pipe and any lambdas into the fastest native equivalent. 66 | 67 | e.g. These two functions should have equivalent AST trees: 68 | 69 | ```python 70 | 71 | @fast_pipes 72 | def function_that_has_a_pipe(v: int) -> str: 73 | value = pipe(v, a, lambda x: b(x, foo="other_input"), c) 74 | return pipe 75 | ``` 76 | 77 | ```python 78 | def function_that_has_a_pipe(v: int) -> str: 79 | value = c(b(a(v),foo="other_input")) 80 | return pipe 81 | ``` 82 | 83 | This version of the function is solving three versions of the same puzzle at the same time: 84 | 85 | * The type hinting is unpacking the structure when it is being written. 86 | * The pipe function solves the problem in standard python. 87 | * The fast_pipes decorator is rewriting the AST tree to get the same outcome faster. 88 | 89 | But to the user, it all looks the same - pipes! 90 | 91 | There is a limit of 20 functions that can be passed to a pipe or pipeline. If you *really* want to do more, you could chain multiple pipelines together. 92 | 93 | ## Install 94 | 95 | You can install from pip: `python -m pip install function-pipes` 96 | 97 | Or you can copy the module directly into your projects. 98 | 99 | * For python 3.10+: [with_paramspec/function_pipes.py](src/function_pipes/with_paramspec/function_pipes.py) 100 | * For python 3.8, 3.9: [without_paramspec/function_pipes.py](src/function_pipes/without_paramspec/function_pipes.py) 101 | 102 | ## Development 103 | 104 | This project comes with a Dockerfile and devcontainer that should get a good environment set up. 105 | 106 | The actual code is generated from `src/function_pipes/pipes.jinja-py` using jinja to generate the code and the seperate versions with and without use of paramspec. 107 | 108 | Use `make` to regenerate the files. The number of allowed arguments is specified in `Makefile`. 109 | 110 | There is a test suite that does checks for equivalence between this syntax and the raw syntax, as well as checking that fast_pipes and other optimisations are faster. 111 | 112 | This can be run with `script/test`. -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "astroid" 3 | version = "2.11.7" 4 | description = "An abstract syntax tree for Python with inference support." 5 | category = "dev" 6 | optional = false 7 | python-versions = ">=3.6.2" 8 | 9 | [package.dependencies] 10 | lazy-object-proxy = ">=1.4.0" 11 | setuptools = ">=20.0" 12 | typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} 13 | wrapt = ">=1.11,<2" 14 | 15 | [[package]] 16 | name = "atomicwrites" 17 | version = "1.4.1" 18 | description = "Atomic file writes." 19 | category = "dev" 20 | optional = false 21 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 22 | 23 | [[package]] 24 | name = "attrs" 25 | version = "21.4.0" 26 | description = "Classes Without Boilerplate" 27 | category = "dev" 28 | optional = false 29 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 30 | 31 | [package.extras] 32 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope-interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] 33 | docs = ["furo", "sphinx", "zope-interface", "sphinx-notfound-page"] 34 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope-interface", "cloudpickle"] 35 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] 36 | 37 | [[package]] 38 | name = "black" 39 | version = "22.6.0" 40 | description = "The uncompromising code formatter." 41 | category = "dev" 42 | optional = false 43 | python-versions = ">=3.6.2" 44 | 45 | [package.dependencies] 46 | click = ">=8.0.0" 47 | mypy-extensions = ">=0.4.3" 48 | pathspec = ">=0.9.0" 49 | platformdirs = ">=2" 50 | tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} 51 | typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} 52 | 53 | [package.extras] 54 | colorama = ["colorama (>=0.4.3)"] 55 | d = ["aiohttp (>=3.7.4)"] 56 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 57 | uvloop = ["uvloop (>=0.15.2)"] 58 | 59 | [[package]] 60 | name = "click" 61 | version = "8.1.3" 62 | description = "Composable command line interface toolkit" 63 | category = "dev" 64 | optional = false 65 | python-versions = ">=3.7" 66 | 67 | [package.dependencies] 68 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 69 | 70 | [[package]] 71 | name = "colorama" 72 | version = "0.4.5" 73 | description = "Cross-platform colored terminal text." 74 | category = "dev" 75 | optional = false 76 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 77 | 78 | [[package]] 79 | name = "coverage" 80 | version = "6.4.1" 81 | description = "Code coverage measurement for Python" 82 | category = "dev" 83 | optional = false 84 | python-versions = ">=3.7" 85 | 86 | [package.dependencies] 87 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 88 | 89 | [package.extras] 90 | toml = ["tomli"] 91 | 92 | [[package]] 93 | name = "dill" 94 | version = "0.3.5.1" 95 | description = "serialize all of python" 96 | category = "dev" 97 | optional = false 98 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" 99 | 100 | [package.extras] 101 | graph = ["objgraph (>=1.7.2)"] 102 | 103 | [[package]] 104 | name = "iniconfig" 105 | version = "1.1.1" 106 | description = "iniconfig: brain-dead simple config-ini parsing" 107 | category = "dev" 108 | optional = false 109 | python-versions = "*" 110 | 111 | [[package]] 112 | name = "isort" 113 | version = "5.10.1" 114 | description = "A Python utility / library to sort Python imports." 115 | category = "dev" 116 | optional = false 117 | python-versions = ">=3.6.1,<4.0" 118 | 119 | [package.extras] 120 | pipfile_deprecated_finder = ["pipreqs", "requirementslib"] 121 | requirements_deprecated_finder = ["pipreqs", "pip-api"] 122 | colors = ["colorama (>=0.4.3,<0.5.0)"] 123 | plugins = ["setuptools"] 124 | 125 | [[package]] 126 | name = "j2cli" 127 | version = "0.3.10" 128 | description = "Command-line interface to Jinja2 for templating in shell scripts." 129 | category = "dev" 130 | optional = false 131 | python-versions = "*" 132 | 133 | [package.dependencies] 134 | jinja2 = ">=2.7.2" 135 | 136 | [package.extras] 137 | yaml = ["pyyaml (>=3.10)"] 138 | 139 | [[package]] 140 | name = "jinja2" 141 | version = "3.1.2" 142 | description = "A very fast and expressive template engine." 143 | category = "dev" 144 | optional = false 145 | python-versions = ">=3.7" 146 | 147 | [package.dependencies] 148 | MarkupSafe = ">=2.0" 149 | 150 | [package.extras] 151 | i18n = ["Babel (>=2.7)"] 152 | 153 | [[package]] 154 | name = "lazy-object-proxy" 155 | version = "1.7.1" 156 | description = "A fast and thorough lazy object proxy." 157 | category = "dev" 158 | optional = false 159 | python-versions = ">=3.6" 160 | 161 | [[package]] 162 | name = "markupsafe" 163 | version = "2.1.1" 164 | description = "Safely add untrusted strings to HTML/XML markup." 165 | category = "dev" 166 | optional = false 167 | python-versions = ">=3.7" 168 | 169 | [[package]] 170 | name = "mccabe" 171 | version = "0.7.0" 172 | description = "McCabe checker, plugin for flake8" 173 | category = "dev" 174 | optional = false 175 | python-versions = ">=3.6" 176 | 177 | [[package]] 178 | name = "mypy-extensions" 179 | version = "0.4.3" 180 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 181 | category = "dev" 182 | optional = false 183 | python-versions = "*" 184 | 185 | [[package]] 186 | name = "nodeenv" 187 | version = "1.7.0" 188 | description = "Node.js virtual environment builder" 189 | category = "dev" 190 | optional = false 191 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" 192 | 193 | [package.dependencies] 194 | setuptools = "*" 195 | 196 | [[package]] 197 | name = "packaging" 198 | version = "21.3" 199 | description = "Core utilities for Python packages" 200 | category = "dev" 201 | optional = false 202 | python-versions = ">=3.6" 203 | 204 | [package.dependencies] 205 | pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" 206 | 207 | [[package]] 208 | name = "pathspec" 209 | version = "0.9.0" 210 | description = "Utility library for gitignore style pattern matching of file paths." 211 | category = "dev" 212 | optional = false 213 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 214 | 215 | [[package]] 216 | name = "platformdirs" 217 | version = "2.5.2" 218 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 219 | category = "dev" 220 | optional = false 221 | python-versions = ">=3.7" 222 | 223 | [package.extras] 224 | docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] 225 | test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] 226 | 227 | [[package]] 228 | name = "pluggy" 229 | version = "1.0.0" 230 | description = "plugin and hook calling mechanisms for python" 231 | category = "dev" 232 | optional = false 233 | python-versions = ">=3.6" 234 | 235 | [package.extras] 236 | dev = ["pre-commit", "tox"] 237 | testing = ["pytest", "pytest-benchmark"] 238 | 239 | [[package]] 240 | name = "py" 241 | version = "1.11.0" 242 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 243 | category = "dev" 244 | optional = false 245 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 246 | 247 | [[package]] 248 | name = "pydantic" 249 | version = "1.9.1" 250 | description = "Data validation and settings management using python type hints" 251 | category = "dev" 252 | optional = false 253 | python-versions = ">=3.6.1" 254 | 255 | [package.dependencies] 256 | typing-extensions = ">=3.7.4.3" 257 | 258 | [package.extras] 259 | dotenv = ["python-dotenv (>=0.10.4)"] 260 | email = ["email-validator (>=1.0.3)"] 261 | 262 | [[package]] 263 | name = "pylint" 264 | version = "2.14.4" 265 | description = "python code static checker" 266 | category = "dev" 267 | optional = false 268 | python-versions = ">=3.7.2" 269 | 270 | [package.dependencies] 271 | astroid = ">=2.11.6,<=2.12.0-dev0" 272 | colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} 273 | dill = ">=0.2" 274 | isort = ">=4.2.5,<6" 275 | mccabe = ">=0.6,<0.8" 276 | platformdirs = ">=2.2.0" 277 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 278 | tomlkit = ">=0.10.1" 279 | typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} 280 | 281 | [package.extras] 282 | spelling = ["pyenchant (>=3.2,<4.0)"] 283 | testutils = ["gitpython (>3)"] 284 | 285 | [[package]] 286 | name = "pyparsing" 287 | version = "3.0.9" 288 | description = "pyparsing module - Classes and methods to define and execute parsing grammars" 289 | category = "dev" 290 | optional = false 291 | python-versions = ">=3.6.8" 292 | 293 | [package.extras] 294 | diagrams = ["railroad-diagrams", "jinja2"] 295 | 296 | [[package]] 297 | name = "pyright" 298 | version = "1.1.258" 299 | description = "Command line wrapper for pyright" 300 | category = "dev" 301 | optional = false 302 | python-versions = ">=3.7" 303 | 304 | [package.dependencies] 305 | nodeenv = ">=1.6.0" 306 | 307 | [package.extras] 308 | all = ["twine (>=3.4.1)"] 309 | dev = ["twine (>=3.4.1)"] 310 | 311 | [[package]] 312 | name = "pytest" 313 | version = "7.1.2" 314 | description = "pytest: simple powerful testing with Python" 315 | category = "dev" 316 | optional = false 317 | python-versions = ">=3.7" 318 | 319 | [package.dependencies] 320 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 321 | attrs = ">=19.2.0" 322 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 323 | iniconfig = "*" 324 | packaging = "*" 325 | pluggy = ">=0.12,<2.0" 326 | py = ">=1.8.2" 327 | tomli = ">=1.0.0" 328 | 329 | [package.extras] 330 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] 331 | 332 | [[package]] 333 | name = "pytest-cov" 334 | version = "3.0.0" 335 | description = "Pytest plugin for measuring coverage." 336 | category = "dev" 337 | optional = false 338 | python-versions = ">=3.6" 339 | 340 | [package.dependencies] 341 | coverage = {version = ">=5.2.1", extras = ["toml"]} 342 | pytest = ">=4.6" 343 | 344 | [package.extras] 345 | testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] 346 | 347 | [[package]] 348 | name = "pytest-pyright" 349 | version = "0.0.2" 350 | description = "Pytest plugin for type checking code with Pyright" 351 | category = "dev" 352 | optional = false 353 | python-versions = ">=3.6" 354 | 355 | [package.dependencies] 356 | pydantic = ">=1.6.0" 357 | pyright = ">=0.0.7" 358 | pytest = ">=3.5.0" 359 | 360 | [[package]] 361 | name = "setuptools" 362 | version = "63.1.0" 363 | description = "Easily download, build, install, upgrade, and uninstall Python packages" 364 | category = "dev" 365 | optional = false 366 | python-versions = ">=3.7" 367 | 368 | [package.extras] 369 | docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-reredirects", "sphinxcontrib-towncrier", "furo"] 370 | testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-enabler (>=1.3)", "pytest-perf", "mock", "flake8-2020", "virtualenv (>=13.0.0)", "wheel", "pip (>=19.1)", "jaraco.envs (>=2.2)", "pytest-xdist", "jaraco.path (>=3.2.0)", "build", "filelock (>=3.4.0)", "pip-run (>=8.8)", "ini2toml[lite] (>=0.9)", "tomli-w (>=1.0.0)", "pytest-black (>=0.3.7)", "pytest-cov", "pytest-mypy (>=0.9.1)"] 371 | testing-integration = ["pytest", "pytest-xdist", "pytest-enabler", "virtualenv (>=13.0.0)", "tomli", "wheel", "jaraco.path (>=3.2.0)", "jaraco.envs (>=2.2)", "build", "filelock (>=3.4.0)"] 372 | 373 | [[package]] 374 | name = "toml" 375 | version = "0.10.2" 376 | description = "Python Library for Tom's Obvious, Minimal Language" 377 | category = "dev" 378 | optional = false 379 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 380 | 381 | [[package]] 382 | name = "tomli" 383 | version = "2.0.1" 384 | description = "A lil' TOML parser" 385 | category = "dev" 386 | optional = false 387 | python-versions = ">=3.7" 388 | 389 | [[package]] 390 | name = "tomlkit" 391 | version = "0.11.1" 392 | description = "Style preserving TOML library" 393 | category = "dev" 394 | optional = false 395 | python-versions = ">=3.6,<4.0" 396 | 397 | [[package]] 398 | name = "typing-extensions" 399 | version = "4.3.0" 400 | description = "Backported and Experimental Type Hints for Python 3.7+" 401 | category = "dev" 402 | optional = false 403 | python-versions = ">=3.7" 404 | 405 | [[package]] 406 | name = "wrapt" 407 | version = "1.14.1" 408 | description = "Module for decorators, wrappers and monkey patching." 409 | category = "dev" 410 | optional = false 411 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 412 | 413 | [metadata] 414 | lock-version = "1.1" 415 | python-versions = "^3.8" 416 | content-hash = "18ad65c236b2efcfd4f1f6b4434af1bf0ed6d926b4ecd95b3504c11dd1bd3a04" 417 | 418 | [metadata.files] 419 | astroid = [] 420 | atomicwrites = [] 421 | attrs = [] 422 | black = [] 423 | click = [] 424 | colorama = [] 425 | coverage = [] 426 | dill = [] 427 | iniconfig = [] 428 | isort = [] 429 | j2cli = [] 430 | jinja2 = [] 431 | lazy-object-proxy = [] 432 | markupsafe = [] 433 | mccabe = [] 434 | mypy-extensions = [] 435 | nodeenv = [] 436 | packaging = [] 437 | pathspec = [] 438 | platformdirs = [] 439 | pluggy = [] 440 | py = [] 441 | pydantic = [] 442 | pylint = [] 443 | pyparsing = [] 444 | pyright = [] 445 | pytest = [] 446 | pytest-cov = [] 447 | pytest-pyright = [] 448 | setuptools = [] 449 | toml = [] 450 | tomli = [] 451 | tomlkit = [] 452 | typing-extensions = [] 453 | wrapt = [] 454 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "function-pipes" 3 | version = "0.1.2" 4 | description = "Typed python equivalent for R pipes." 5 | authors = ["Alex Parsons "] 6 | readme = "README.md" 7 | license = "MIT" 8 | homepage = "https://github.com/ajparsons/function-pipes" 9 | repository = "https://github.com/ajparsons/function-pipes" 10 | include = [ 11 | "LICENSE.md", 12 | ] 13 | 14 | [tool.poetry.dependencies] 15 | python = "^3.8" 16 | 17 | [tool.poetry.dev-dependencies] 18 | pytest = "^7.1.2" 19 | pytest-cov = "^3.0.0" 20 | pylint = "^2.12.2" 21 | black = "^22.3.0" 22 | pyright = "^1.1" 23 | toml = "^0.10.2" 24 | j2cli = "^0.3.10" 25 | pytest-pyright = "^0.0.2" 26 | 27 | [build-system] 28 | requires = ["poetry-core>=1.0.0"] 29 | build-backend = "poetry.core.masonry.api" -------------------------------------------------------------------------------- /script/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | pytest --disable-warnings -------------------------------------------------------------------------------- /src/function_pipes/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Typed python equivalent for R pipes. 3 | """ 4 | 5 | __version__ = "0.1.2" 6 | 7 | import sys 8 | 9 | if sys.version_info >= (3, 10): 10 | from .with_paramspec.function_pipes import * 11 | else: 12 | from .without_paramspec.function_pipes import * 13 | -------------------------------------------------------------------------------- /src/function_pipes/pipes.jinja-py: -------------------------------------------------------------------------------- 1 | {% set arg_limit = arg_limit|int %} 2 | {% set param_spec = param_spec|int %} 3 | """ 4 | function-pipe 5 | 6 | Functions for pipe syntax in Python. 7 | 8 | {% if param_spec == 1 %} 9 | Version using ParamSpec for Python 3.10 + 10 | {% else %} 11 | Version without ParamSpec for Python 3.8-3.9. 12 | {% endif %} 13 | Read more at https://github.com/ajparsons/function-pipes 14 | 15 | Licence: MIT 16 | 17 | """ 18 | # pylint: disable=line-too-long 19 | 20 | from ast import (Call, Lambda, Load, Name, NamedExpr, NodeTransformer, 21 | NodeVisitor, Store, expr, increment_lineno, parse, walk) 22 | from inspect import getsource 23 | from itertools import takewhile 24 | from textwrap import dedent 25 | from typing import (Any, Union, Callable, {% if param_spec == 1 %}ParamSpec,{% endif %} TypeVar, overload) 26 | 27 | 28 | {% if param_spec == 1 %} 29 | InputParams = ParamSpec("InputParams") 30 | P = ParamSpec("P") 31 | {% set ip_ref = "InputParams" %} 32 | {% set ip_args = "InputParams.args" %} 33 | {% set ip_kwargs = "InputParams.kwargs" %} 34 | {% else %} 35 | {% set ip_ref = "..." %} 36 | {% set ip_args = "Any" %} 37 | {% set ip_kwargs = "Any" %} 38 | {% endif %} 39 | T = TypeVar("T") 40 | 41 | class _LambdaExtractor(NodeTransformer): 42 | """ 43 | Replace references to the lambda argument with the passed in value 44 | """ 45 | 46 | def __init__( 47 | self, 48 | _lambda: Lambda, 49 | value: Union[expr, Call], 50 | subsequent_value: Union[expr, Call, None] = None, 51 | ): 52 | self._lambda = _lambda 53 | self.arg_name = self._lambda.args.args[0].arg # type: ignore 54 | self.visit_count = 0 55 | self.value = value 56 | self.subsequent_value = subsequent_value 57 | 58 | def extract_and_replace(self): 59 | """ 60 | Return what the lambda does, but replaces 61 | references to the lambda arg with 62 | the passed in value 63 | """ 64 | self.visit(self._lambda.body) 65 | return self._lambda.body 66 | 67 | def visit_Name(self, node: Name): 68 | """ 69 | Replace the internal lambda arg reference with the given value 70 | If a subsequent value given, replace all values after the first with that 71 | This allows assigning using a walrus in the first value, and then 72 | using that value without recalculation in the second. 73 | """ 74 | if node.id == self.arg_name: 75 | if self.visit_count == 0: 76 | self.visit_count += 1 77 | return self.value 78 | if self.subsequent_value: 79 | return self.subsequent_value 80 | else: 81 | raise ValueError("This lambda contains multiple references to the arg") 82 | return node 83 | 84 | 85 | def copy_position(source: expr, destination: expr): 86 | """ 87 | Copy the position information from one AST node to another 88 | """ 89 | destination.lineno = source.lineno 90 | destination.end_lineno = source.end_lineno 91 | destination.col_offset = source.col_offset 92 | destination.end_col_offset = source.end_col_offset 93 | 94 | 95 | class _CountLambdaArgUses(NodeVisitor): 96 | """ 97 | Count the number of uses of the first argument in the lambda 98 | in the lambda definition 99 | """ 100 | 101 | def __init__(self, _lambda: Lambda): 102 | self._lambda = _lambda 103 | self.arg_name = self._lambda.args.args[0].arg # type: ignore 104 | self.uses: int = 0 105 | 106 | def check(self) -> int: 107 | """ 108 | Get the number of times the arugment is referenced 109 | """ 110 | self.visit(self._lambda.body) # type: ignore 111 | return self.uses 112 | 113 | def visit_Name(self, node: Name): 114 | """ 115 | Increment the uses count if the name is the given arg 116 | """ 117 | if node.id == self.arg_name: 118 | self.uses += 1 119 | self.generic_visit(node) 120 | 121 | 122 | class _PipeTransformer(NodeTransformer): 123 | """ 124 | A NodeTransformer that rewrites the code tree so that all references to replaced with 125 | a set of nested function calls. 126 | 127 | a = pipe(a,b,c,d) 128 | 129 | becomes 130 | 131 | a = d(c(b(a))) 132 | 133 | This also expands lambdas so that there is no function calling overhead. 134 | 135 | a = pipe(a,b,c,lambda x: x+1) 136 | 137 | becomes: 138 | 139 | a = (c(b(a))) + 1 140 | 141 | Where there are multiple uses of the argument in a lambda, 142 | a walrus is used to avoid duplication calculations. 143 | 144 | a = pipe(a,b,c,lambda x: x + x + 1) 145 | 146 | becomes: 147 | 148 | a = (var := c(b(a))) + var + 1 149 | 150 | """ 151 | 152 | def visit_Call(self, node: Call) -> Any: 153 | """ 154 | Replace all references to the pipe function with nested function calls. 155 | """ 156 | if node.func.id == "pipe": # type: ignore 157 | value = node.args[0] 158 | funcs = node.args[1:] 159 | 160 | # unpack the functions into nested calls 161 | # unless the function is a lambda 162 | # in which case, the lambda's body 163 | # needs to be unpacked 164 | for func in funcs: 165 | if isinstance(func, Lambda): 166 | arg_usage = _CountLambdaArgUses(func).check() 167 | if arg_usage == 0: 168 | # this will throw an error at build time rather than runtime 169 | # but shouldn't be a surprise to typecheckers 170 | raise ValueError("This lambda has no arguments.") 171 | elif arg_usage == 1: 172 | # if the lambda only uses the argument once 173 | # can just substitute the arg in the lambda 174 | # with the value in the loop 175 | # e.g. a = pipe(5, lambda x: x + 1) 176 | # becomes a = 5 + 1 177 | value = _LambdaExtractor(func, value).extract_and_replace() 178 | elif arg_usage > 1: 179 | # if the lambda uses the argument more than once 180 | # have to assign the value to a variable first 181 | # (using a walrus) 182 | # and then use the variable in the lambda in subsequent calls. 183 | # e.g. a = pipe(5, lambda x: x + x + 1) 184 | # becomes a = (var := 5) + var + 1 185 | # NamedExpr is how := works behind the scenes. 186 | walrus = NamedExpr( 187 | target=Name(id="_pipe_temp_var", ctx=Store()), value=value 188 | ) 189 | walrus.lineno = func.lineno 190 | copy_position(func, walrus) 191 | temp_var = Name(id="_pipe_temp_var", ctx=Load()) 192 | copy_position(func, temp_var) 193 | value = _LambdaExtractor( 194 | func, walrus, temp_var 195 | ).extract_and_replace() 196 | else: 197 | # if just a function, we're just building a nesting call chain 198 | value = Call(func, [value], []) 199 | copy_position(func, value) 200 | return value # type: ignore 201 | return self.generic_visit(node) 202 | 203 | 204 | def fast_pipes(func: Callable[{% if param_spec == 1%}P{% else %}...{% endif %}, T]) -> Callable[{% if param_spec == 1%}P{% else %}...{% endif %}, T]: 205 | """ 206 | Decorator function that replaces references to pipe with 207 | the direct equivalent of the pipe function. 208 | """ 209 | 210 | # This approach adapted from 211 | # adapted from https://github.com/robinhilliard/pipes/blob/master/pipeop/__init__.py 212 | ctx = func.__globals__ 213 | first_line_number = func.__code__.co_firstlineno 214 | 215 | source = getsource(func) 216 | 217 | # AST data structure representing parsed function code 218 | tree = parse(dedent(source)) 219 | 220 | # Fix line and column numbers so that debuggers still work 221 | increment_lineno(tree, first_line_number - 1) 222 | source_indent = sum([1 for _ in takewhile(str.isspace, source)]) + 1 223 | 224 | for node in walk(tree): 225 | if hasattr(node, "col_offset"): 226 | node.col_offset += source_indent 227 | 228 | # Update name of function or class to compile 229 | tree.body[0].name += "_fast_pipe" # type: ignore 230 | 231 | # remove the pipe decorator so that we don't recursively 232 | # call it again. The AST node for the decorator will be a 233 | # Call if it had braces, and a Name if it had no braces. 234 | # The location of the decorator function name in these 235 | # nodes is slightly different. 236 | tree.body[0].decorator_list = [ # type: ignore 237 | d 238 | for d in tree.body[0].decorator_list # type: ignore 239 | if isinstance(d, Call) 240 | and d.func.id != "fast_pipes" # type: ignore 241 | or isinstance(d, Name) 242 | and d.id != "fast_pipes" 243 | ] 244 | 245 | # Apply the visit_Call transformation 246 | tree = _PipeTransformer().visit(tree) 247 | 248 | # now compile the AST into an altered function or class definition 249 | try: 250 | code = compile( 251 | tree, 252 | filename=(ctx["__file__"] if "__file__" in ctx else "repl"), 253 | mode="exec", 254 | ) 255 | except SyntaxError as e: 256 | # The syntax is rearranged in a way that triggers a starred error correctly 257 | # Easier to adjust the error here than figure out how to raise it properly 258 | # in the AST visitor. 259 | # This is a bit hacky, but it's good enough for now. 260 | if e.msg == "can't use starred expression here" and ( 261 | e.text and "pipe(" in e.text 262 | ): 263 | e.msg = "pipe can't take a starred expression as an argument when fast_pipes is used." 264 | raise e 265 | 266 | # and execute the definition in the original context so that the 267 | # decorated function can access the same scopes as the original 268 | exec(code, ctx) 269 | 270 | # return the modified function or class - original is nevers called 271 | return ctx[tree.body[0].name] 272 | 273 | 274 | 275 | 276 | BridgeType = TypeVar("BridgeType") 277 | InputVal = TypeVar("InputVal") 278 | 279 | # Always overridden by the overloads but is 280 | # self consistent in the declared function 281 | stand_in_callable = Union[Callable[..., Any], None] 282 | 283 | {% for n in range(arg_limit) %} 284 | Out{{n}} = TypeVar("Out{{n}}"){% endfor %} 285 | 286 | def pipe_bridge(func: Callable[[BridgeType], Any] 287 | ) -> Callable[[BridgeType], BridgeType]: 288 | """ 289 | When debugging, you might want to use a function to see 290 | the current value in the pipe, but examination functions. 291 | may not return the value to let it continue down the chain. 292 | This wraps the function so that it does it's job, and then 293 | returns the original value to conitnue down the chain. 294 | For instance: 295 | ``` 296 | bridge(rich.print) 297 | ``` 298 | Will use the rich library's print function to look at the value, 299 | but then unlike calling `rich.print` directly in the pipe, 300 | will return the value to let it continue. 301 | """ 302 | 303 | def _inner(value: BridgeType) -> BridgeType: 304 | func(value) 305 | return value 306 | 307 | return _inner 308 | 309 | {% for n in range(1, arg_limit) %} 310 | @overload 311 | def pipe( 312 | value: InputVal, 313 | op0: Callable[[InputVal], Out0],{% for x in range(n-1) %} 314 | op{{ x + 1 }}: Callable[[Out{{x}}], Out{{x+1}}],{% endfor %} 315 | / 316 | ) -> Out{{n-1}}: 317 | ... 318 | {% endfor %} 319 | 320 | def pipe(value: Any{% for n in range(arg_limit)%}, op{{n}}: stand_in_callable = None{% endfor %},/) -> Any: # type: ignore 321 | """ 322 | Pipe takes up to {{arg_limit}} functions and applies them to a value. 323 | """ 324 | {% for n in range(arg_limit + 1)%} 325 | {% if n == arg_limit %}else{%else%}{% if n > 0 %}el{% endif %}if not op{{n}}{% endif %}: 326 | return {% for x in range(n)|reverse %}op{{x}}({% endfor %}value{% for x in range(n) %}){%endfor%} # fmt: skip 327 | {% endfor %} 328 | 329 | {% for n in range(1, arg_limit) %} 330 | @overload 331 | def pipeline( 332 | op0: Callable[{{ip_ref}}, Out0],{% for x in range(n-1) %} 333 | op{{ x + 1 }}: Callable[[Out{{x}}], Out{{x+1}}],{% endfor %} 334 | / 335 | ) -> Callable[{{ip_ref}},Out{{n-1}}]: 336 | ... 337 | {% endfor %} 338 | 339 | 340 | def pipeline(op0: Callable[{{ip_ref}}, Out0]{% for n in range(1,arg_limit)%}, op{{n}}: Union[Callable[[Out{{n-1}}], Out{{n}}], None] = None{% endfor %},/) -> Callable[{{ip_ref}}, Any]: # type: ignore 341 | """ 342 | Pipeline takes up to {{arg_limit}} functions and composites them into a single function. 343 | """ 344 | {% for n in range(1,arg_limit+1)%} 345 | {% if n == arg_limit %}else{%else%}{% if n > 1 %}el{% endif %}if not op{{n}}{% endif %}: 346 | def _inner{{n-1}}(*args:{{ip_args}}, **kwargs:{{ip_kwargs}}) -> Out{{n-1}}: 347 | return {% for x in range(n)|reverse %}op{{x}}({% endfor %}*args, **kwargs{% for x in range(n) %}){%endfor%} # fmt: skip 348 | return _inner{{n-1}} 349 | {% endfor %} 350 | 351 | 352 | def arbitary_length_pipe(value: Any, *funcs: Callable[[Any], Any]) -> Any: 353 | """ 354 | Pipe that takes an arbitary amount of functions. 355 | """ 356 | for func in funcs: 357 | value = func(value) 358 | return value 359 | -------------------------------------------------------------------------------- /src/function_pipes/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajparsons/function-pipes/2141c9e917f127a147c3d4b7e492b3cd76bca62c/src/function_pipes/py.typed -------------------------------------------------------------------------------- /src/function_pipes/with_paramspec/function_pipes.py: -------------------------------------------------------------------------------- 1 | """ 2 | function-pipe 3 | 4 | Functions for pipe syntax in Python. 5 | 6 | 7 | Version using ParamSpec for Python 3.10 + 8 | 9 | Read more at https://github.com/ajparsons/function-pipes 10 | 11 | Licence: MIT 12 | 13 | """ 14 | # pylint: disable=line-too-long 15 | 16 | from ast import ( 17 | Call, 18 | Lambda, 19 | Load, 20 | Name, 21 | NamedExpr, 22 | NodeTransformer, 23 | NodeVisitor, 24 | Store, 25 | expr, 26 | increment_lineno, 27 | parse, 28 | walk, 29 | ) 30 | from inspect import getsource 31 | from itertools import takewhile 32 | from textwrap import dedent 33 | from typing import Any, Union, Callable, ParamSpec, TypeVar, overload 34 | 35 | 36 | InputParams = ParamSpec("InputParams") 37 | P = ParamSpec("P") 38 | 39 | 40 | T = TypeVar("T") 41 | 42 | 43 | class _LambdaExtractor(NodeTransformer): 44 | """ 45 | Replace references to the lambda argument with the passed in value 46 | """ 47 | 48 | def __init__( 49 | self, 50 | _lambda: Lambda, 51 | value: Union[expr, Call], 52 | subsequent_value: Union[expr, Call, None] = None, 53 | ): 54 | self._lambda = _lambda 55 | self.arg_name = self._lambda.args.args[0].arg # type: ignore 56 | self.visit_count = 0 57 | self.value = value 58 | self.subsequent_value = subsequent_value 59 | 60 | def extract_and_replace(self): 61 | """ 62 | Return what the lambda does, but replaces 63 | references to the lambda arg with 64 | the passed in value 65 | """ 66 | self.visit(self._lambda.body) 67 | return self._lambda.body 68 | 69 | def visit_Name(self, node: Name): 70 | """ 71 | Replace the internal lambda arg reference with the given value 72 | If a subsequent value given, replace all values after the first with that 73 | This allows assigning using a walrus in the first value, and then 74 | using that value without recalculation in the second. 75 | """ 76 | if node.id == self.arg_name: 77 | if self.visit_count == 0: 78 | self.visit_count += 1 79 | return self.value 80 | if self.subsequent_value: 81 | return self.subsequent_value 82 | else: 83 | raise ValueError("This lambda contains multiple references to the arg") 84 | return node 85 | 86 | 87 | def copy_position(source: expr, destination: expr): 88 | """ 89 | Copy the position information from one AST node to another 90 | """ 91 | destination.lineno = source.lineno 92 | destination.end_lineno = source.end_lineno 93 | destination.col_offset = source.col_offset 94 | destination.end_col_offset = source.end_col_offset 95 | 96 | 97 | class _CountLambdaArgUses(NodeVisitor): 98 | """ 99 | Count the number of uses of the first argument in the lambda 100 | in the lambda definition 101 | """ 102 | 103 | def __init__(self, _lambda: Lambda): 104 | self._lambda = _lambda 105 | self.arg_name = self._lambda.args.args[0].arg # type: ignore 106 | self.uses: int = 0 107 | 108 | def check(self) -> int: 109 | """ 110 | Get the number of times the arugment is referenced 111 | """ 112 | self.visit(self._lambda.body) # type: ignore 113 | return self.uses 114 | 115 | def visit_Name(self, node: Name): 116 | """ 117 | Increment the uses count if the name is the given arg 118 | """ 119 | if node.id == self.arg_name: 120 | self.uses += 1 121 | self.generic_visit(node) 122 | 123 | 124 | class _PipeTransformer(NodeTransformer): 125 | """ 126 | A NodeTransformer that rewrites the code tree so that all references to replaced with 127 | a set of nested function calls. 128 | 129 | a = pipe(a,b,c,d) 130 | 131 | becomes 132 | 133 | a = d(c(b(a))) 134 | 135 | This also expands lambdas so that there is no function calling overhead. 136 | 137 | a = pipe(a,b,c,lambda x: x+1) 138 | 139 | becomes: 140 | 141 | a = (c(b(a))) + 1 142 | 143 | Where there are multiple uses of the argument in a lambda, 144 | a walrus is used to avoid duplication calculations. 145 | 146 | a = pipe(a,b,c,lambda x: x + x + 1) 147 | 148 | becomes: 149 | 150 | a = (var := c(b(a))) + var + 1 151 | 152 | """ 153 | 154 | def visit_Call(self, node: Call) -> Any: 155 | """ 156 | Replace all references to the pipe function with nested function calls. 157 | """ 158 | if node.func.id == "pipe": # type: ignore 159 | value = node.args[0] 160 | funcs = node.args[1:] 161 | 162 | # unpack the functions into nested calls 163 | # unless the function is a lambda 164 | # in which case, the lambda's body 165 | # needs to be unpacked 166 | for func in funcs: 167 | if isinstance(func, Lambda): 168 | arg_usage = _CountLambdaArgUses(func).check() 169 | if arg_usage == 0: 170 | # this will throw an error at build time rather than runtime 171 | # but shouldn't be a surprise to typecheckers 172 | raise ValueError("This lambda has no arguments.") 173 | elif arg_usage == 1: 174 | # if the lambda only uses the argument once 175 | # can just substitute the arg in the lambda 176 | # with the value in the loop 177 | # e.g. a = pipe(5, lambda x: x + 1) 178 | # becomes a = 5 + 1 179 | value = _LambdaExtractor(func, value).extract_and_replace() 180 | elif arg_usage > 1: 181 | # if the lambda uses the argument more than once 182 | # have to assign the value to a variable first 183 | # (using a walrus) 184 | # and then use the variable in the lambda in subsequent calls. 185 | # e.g. a = pipe(5, lambda x: x + x + 1) 186 | # becomes a = (var := 5) + var + 1 187 | # NamedExpr is how := works behind the scenes. 188 | walrus = NamedExpr( 189 | target=Name(id="_pipe_temp_var", ctx=Store()), value=value 190 | ) 191 | walrus.lineno = func.lineno 192 | copy_position(func, walrus) 193 | temp_var = Name(id="_pipe_temp_var", ctx=Load()) 194 | copy_position(func, temp_var) 195 | value = _LambdaExtractor( 196 | func, walrus, temp_var 197 | ).extract_and_replace() 198 | else: 199 | # if just a function, we're just building a nesting call chain 200 | value = Call(func, [value], []) 201 | copy_position(func, value) 202 | return value # type: ignore 203 | return self.generic_visit(node) 204 | 205 | 206 | def fast_pipes(func: Callable[P, T]) -> Callable[P, T]: 207 | """ 208 | Decorator function that replaces references to pipe with 209 | the direct equivalent of the pipe function. 210 | """ 211 | 212 | # This approach adapted from 213 | # adapted from https://github.com/robinhilliard/pipes/blob/master/pipeop/__init__.py 214 | ctx = func.__globals__ 215 | first_line_number = func.__code__.co_firstlineno 216 | 217 | source = getsource(func) 218 | 219 | # AST data structure representing parsed function code 220 | tree = parse(dedent(source)) 221 | 222 | # Fix line and column numbers so that debuggers still work 223 | increment_lineno(tree, first_line_number - 1) 224 | source_indent = sum([1 for _ in takewhile(str.isspace, source)]) + 1 225 | 226 | for node in walk(tree): 227 | if hasattr(node, "col_offset"): 228 | node.col_offset += source_indent 229 | 230 | # Update name of function or class to compile 231 | tree.body[0].name += "_fast_pipe" # type: ignore 232 | 233 | # remove the pipe decorator so that we don't recursively 234 | # call it again. The AST node for the decorator will be a 235 | # Call if it had braces, and a Name if it had no braces. 236 | # The location of the decorator function name in these 237 | # nodes is slightly different. 238 | tree.body[0].decorator_list = [ # type: ignore 239 | d 240 | for d in tree.body[0].decorator_list # type: ignore 241 | if isinstance(d, Call) 242 | and d.func.id != "fast_pipes" # type: ignore 243 | or isinstance(d, Name) 244 | and d.id != "fast_pipes" 245 | ] 246 | 247 | # Apply the visit_Call transformation 248 | tree = _PipeTransformer().visit(tree) 249 | 250 | # now compile the AST into an altered function or class definition 251 | try: 252 | code = compile( 253 | tree, 254 | filename=(ctx["__file__"] if "__file__" in ctx else "repl"), 255 | mode="exec", 256 | ) 257 | except SyntaxError as e: 258 | # The syntax is rearranged in a way that triggers a starred error correctly 259 | # Easier to adjust the error here than figure out how to raise it properly 260 | # in the AST visitor. 261 | # This is a bit hacky, but it's good enough for now. 262 | if e.msg == "can't use starred expression here" and ( 263 | e.text and "pipe(" in e.text 264 | ): 265 | e.msg = "pipe can't take a starred expression as an argument when fast_pipes is used." 266 | raise e 267 | 268 | # and execute the definition in the original context so that the 269 | # decorated function can access the same scopes as the original 270 | exec(code, ctx) 271 | 272 | # return the modified function or class - original is nevers called 273 | return ctx[tree.body[0].name] 274 | 275 | 276 | BridgeType = TypeVar("BridgeType") 277 | InputVal = TypeVar("InputVal") 278 | 279 | # Always overridden by the overloads but is 280 | # self consistent in the declared function 281 | stand_in_callable = Union[Callable[..., Any], None] 282 | 283 | 284 | Out0 = TypeVar("Out0") 285 | Out1 = TypeVar("Out1") 286 | Out2 = TypeVar("Out2") 287 | Out3 = TypeVar("Out3") 288 | Out4 = TypeVar("Out4") 289 | Out5 = TypeVar("Out5") 290 | Out6 = TypeVar("Out6") 291 | Out7 = TypeVar("Out7") 292 | Out8 = TypeVar("Out8") 293 | Out9 = TypeVar("Out9") 294 | Out10 = TypeVar("Out10") 295 | Out11 = TypeVar("Out11") 296 | Out12 = TypeVar("Out12") 297 | Out13 = TypeVar("Out13") 298 | Out14 = TypeVar("Out14") 299 | Out15 = TypeVar("Out15") 300 | Out16 = TypeVar("Out16") 301 | Out17 = TypeVar("Out17") 302 | Out18 = TypeVar("Out18") 303 | Out19 = TypeVar("Out19") 304 | 305 | 306 | def pipe_bridge( 307 | func: Callable[[BridgeType], Any] 308 | ) -> Callable[[BridgeType], BridgeType]: 309 | """ 310 | When debugging, you might want to use a function to see 311 | the current value in the pipe, but examination functions. 312 | may not return the value to let it continue down the chain. 313 | This wraps the function so that it does it's job, and then 314 | returns the original value to conitnue down the chain. 315 | For instance: 316 | ``` 317 | bridge(rich.print) 318 | ``` 319 | Will use the rich library's print function to look at the value, 320 | but then unlike calling `rich.print` directly in the pipe, 321 | will return the value to let it continue. 322 | """ 323 | 324 | def _inner(value: BridgeType) -> BridgeType: 325 | func(value) 326 | return value 327 | 328 | return _inner 329 | 330 | 331 | @overload 332 | def pipe(value: InputVal, op0: Callable[[InputVal], Out0], /) -> Out0: 333 | ... 334 | 335 | 336 | @overload 337 | def pipe( 338 | value: InputVal, op0: Callable[[InputVal], Out0], op1: Callable[[Out0], Out1], / 339 | ) -> Out1: 340 | ... 341 | 342 | 343 | @overload 344 | def pipe( 345 | value: InputVal, 346 | op0: Callable[[InputVal], Out0], 347 | op1: Callable[[Out0], Out1], 348 | op2: Callable[[Out1], Out2], 349 | /, 350 | ) -> Out2: 351 | ... 352 | 353 | 354 | @overload 355 | def pipe( 356 | value: InputVal, 357 | op0: Callable[[InputVal], Out0], 358 | op1: Callable[[Out0], Out1], 359 | op2: Callable[[Out1], Out2], 360 | op3: Callable[[Out2], Out3], 361 | /, 362 | ) -> Out3: 363 | ... 364 | 365 | 366 | @overload 367 | def pipe( 368 | value: InputVal, 369 | op0: Callable[[InputVal], Out0], 370 | op1: Callable[[Out0], Out1], 371 | op2: Callable[[Out1], Out2], 372 | op3: Callable[[Out2], Out3], 373 | op4: Callable[[Out3], Out4], 374 | /, 375 | ) -> Out4: 376 | ... 377 | 378 | 379 | @overload 380 | def pipe( 381 | value: InputVal, 382 | op0: Callable[[InputVal], Out0], 383 | op1: Callable[[Out0], Out1], 384 | op2: Callable[[Out1], Out2], 385 | op3: Callable[[Out2], Out3], 386 | op4: Callable[[Out3], Out4], 387 | op5: Callable[[Out4], Out5], 388 | /, 389 | ) -> Out5: 390 | ... 391 | 392 | 393 | @overload 394 | def pipe( 395 | value: InputVal, 396 | op0: Callable[[InputVal], Out0], 397 | op1: Callable[[Out0], Out1], 398 | op2: Callable[[Out1], Out2], 399 | op3: Callable[[Out2], Out3], 400 | op4: Callable[[Out3], Out4], 401 | op5: Callable[[Out4], Out5], 402 | op6: Callable[[Out5], Out6], 403 | /, 404 | ) -> Out6: 405 | ... 406 | 407 | 408 | @overload 409 | def pipe( 410 | value: InputVal, 411 | op0: Callable[[InputVal], Out0], 412 | op1: Callable[[Out0], Out1], 413 | op2: Callable[[Out1], Out2], 414 | op3: Callable[[Out2], Out3], 415 | op4: Callable[[Out3], Out4], 416 | op5: Callable[[Out4], Out5], 417 | op6: Callable[[Out5], Out6], 418 | op7: Callable[[Out6], Out7], 419 | /, 420 | ) -> Out7: 421 | ... 422 | 423 | 424 | @overload 425 | def pipe( 426 | value: InputVal, 427 | op0: Callable[[InputVal], Out0], 428 | op1: Callable[[Out0], Out1], 429 | op2: Callable[[Out1], Out2], 430 | op3: Callable[[Out2], Out3], 431 | op4: Callable[[Out3], Out4], 432 | op5: Callable[[Out4], Out5], 433 | op6: Callable[[Out5], Out6], 434 | op7: Callable[[Out6], Out7], 435 | op8: Callable[[Out7], Out8], 436 | /, 437 | ) -> Out8: 438 | ... 439 | 440 | 441 | @overload 442 | def pipe( 443 | value: InputVal, 444 | op0: Callable[[InputVal], Out0], 445 | op1: Callable[[Out0], Out1], 446 | op2: Callable[[Out1], Out2], 447 | op3: Callable[[Out2], Out3], 448 | op4: Callable[[Out3], Out4], 449 | op5: Callable[[Out4], Out5], 450 | op6: Callable[[Out5], Out6], 451 | op7: Callable[[Out6], Out7], 452 | op8: Callable[[Out7], Out8], 453 | op9: Callable[[Out8], Out9], 454 | /, 455 | ) -> Out9: 456 | ... 457 | 458 | 459 | @overload 460 | def pipe( 461 | value: InputVal, 462 | op0: Callable[[InputVal], Out0], 463 | op1: Callable[[Out0], Out1], 464 | op2: Callable[[Out1], Out2], 465 | op3: Callable[[Out2], Out3], 466 | op4: Callable[[Out3], Out4], 467 | op5: Callable[[Out4], Out5], 468 | op6: Callable[[Out5], Out6], 469 | op7: Callable[[Out6], Out7], 470 | op8: Callable[[Out7], Out8], 471 | op9: Callable[[Out8], Out9], 472 | op10: Callable[[Out9], Out10], 473 | /, 474 | ) -> Out10: 475 | ... 476 | 477 | 478 | @overload 479 | def pipe( 480 | value: InputVal, 481 | op0: Callable[[InputVal], Out0], 482 | op1: Callable[[Out0], Out1], 483 | op2: Callable[[Out1], Out2], 484 | op3: Callable[[Out2], Out3], 485 | op4: Callable[[Out3], Out4], 486 | op5: Callable[[Out4], Out5], 487 | op6: Callable[[Out5], Out6], 488 | op7: Callable[[Out6], Out7], 489 | op8: Callable[[Out7], Out8], 490 | op9: Callable[[Out8], Out9], 491 | op10: Callable[[Out9], Out10], 492 | op11: Callable[[Out10], Out11], 493 | /, 494 | ) -> Out11: 495 | ... 496 | 497 | 498 | @overload 499 | def pipe( 500 | value: InputVal, 501 | op0: Callable[[InputVal], Out0], 502 | op1: Callable[[Out0], Out1], 503 | op2: Callable[[Out1], Out2], 504 | op3: Callable[[Out2], Out3], 505 | op4: Callable[[Out3], Out4], 506 | op5: Callable[[Out4], Out5], 507 | op6: Callable[[Out5], Out6], 508 | op7: Callable[[Out6], Out7], 509 | op8: Callable[[Out7], Out8], 510 | op9: Callable[[Out8], Out9], 511 | op10: Callable[[Out9], Out10], 512 | op11: Callable[[Out10], Out11], 513 | op12: Callable[[Out11], Out12], 514 | /, 515 | ) -> Out12: 516 | ... 517 | 518 | 519 | @overload 520 | def pipe( 521 | value: InputVal, 522 | op0: Callable[[InputVal], Out0], 523 | op1: Callable[[Out0], Out1], 524 | op2: Callable[[Out1], Out2], 525 | op3: Callable[[Out2], Out3], 526 | op4: Callable[[Out3], Out4], 527 | op5: Callable[[Out4], Out5], 528 | op6: Callable[[Out5], Out6], 529 | op7: Callable[[Out6], Out7], 530 | op8: Callable[[Out7], Out8], 531 | op9: Callable[[Out8], Out9], 532 | op10: Callable[[Out9], Out10], 533 | op11: Callable[[Out10], Out11], 534 | op12: Callable[[Out11], Out12], 535 | op13: Callable[[Out12], Out13], 536 | /, 537 | ) -> Out13: 538 | ... 539 | 540 | 541 | @overload 542 | def pipe( 543 | value: InputVal, 544 | op0: Callable[[InputVal], Out0], 545 | op1: Callable[[Out0], Out1], 546 | op2: Callable[[Out1], Out2], 547 | op3: Callable[[Out2], Out3], 548 | op4: Callable[[Out3], Out4], 549 | op5: Callable[[Out4], Out5], 550 | op6: Callable[[Out5], Out6], 551 | op7: Callable[[Out6], Out7], 552 | op8: Callable[[Out7], Out8], 553 | op9: Callable[[Out8], Out9], 554 | op10: Callable[[Out9], Out10], 555 | op11: Callable[[Out10], Out11], 556 | op12: Callable[[Out11], Out12], 557 | op13: Callable[[Out12], Out13], 558 | op14: Callable[[Out13], Out14], 559 | /, 560 | ) -> Out14: 561 | ... 562 | 563 | 564 | @overload 565 | def pipe( 566 | value: InputVal, 567 | op0: Callable[[InputVal], Out0], 568 | op1: Callable[[Out0], Out1], 569 | op2: Callable[[Out1], Out2], 570 | op3: Callable[[Out2], Out3], 571 | op4: Callable[[Out3], Out4], 572 | op5: Callable[[Out4], Out5], 573 | op6: Callable[[Out5], Out6], 574 | op7: Callable[[Out6], Out7], 575 | op8: Callable[[Out7], Out8], 576 | op9: Callable[[Out8], Out9], 577 | op10: Callable[[Out9], Out10], 578 | op11: Callable[[Out10], Out11], 579 | op12: Callable[[Out11], Out12], 580 | op13: Callable[[Out12], Out13], 581 | op14: Callable[[Out13], Out14], 582 | op15: Callable[[Out14], Out15], 583 | /, 584 | ) -> Out15: 585 | ... 586 | 587 | 588 | @overload 589 | def pipe( 590 | value: InputVal, 591 | op0: Callable[[InputVal], Out0], 592 | op1: Callable[[Out0], Out1], 593 | op2: Callable[[Out1], Out2], 594 | op3: Callable[[Out2], Out3], 595 | op4: Callable[[Out3], Out4], 596 | op5: Callable[[Out4], Out5], 597 | op6: Callable[[Out5], Out6], 598 | op7: Callable[[Out6], Out7], 599 | op8: Callable[[Out7], Out8], 600 | op9: Callable[[Out8], Out9], 601 | op10: Callable[[Out9], Out10], 602 | op11: Callable[[Out10], Out11], 603 | op12: Callable[[Out11], Out12], 604 | op13: Callable[[Out12], Out13], 605 | op14: Callable[[Out13], Out14], 606 | op15: Callable[[Out14], Out15], 607 | op16: Callable[[Out15], Out16], 608 | /, 609 | ) -> Out16: 610 | ... 611 | 612 | 613 | @overload 614 | def pipe( 615 | value: InputVal, 616 | op0: Callable[[InputVal], Out0], 617 | op1: Callable[[Out0], Out1], 618 | op2: Callable[[Out1], Out2], 619 | op3: Callable[[Out2], Out3], 620 | op4: Callable[[Out3], Out4], 621 | op5: Callable[[Out4], Out5], 622 | op6: Callable[[Out5], Out6], 623 | op7: Callable[[Out6], Out7], 624 | op8: Callable[[Out7], Out8], 625 | op9: Callable[[Out8], Out9], 626 | op10: Callable[[Out9], Out10], 627 | op11: Callable[[Out10], Out11], 628 | op12: Callable[[Out11], Out12], 629 | op13: Callable[[Out12], Out13], 630 | op14: Callable[[Out13], Out14], 631 | op15: Callable[[Out14], Out15], 632 | op16: Callable[[Out15], Out16], 633 | op17: Callable[[Out16], Out17], 634 | /, 635 | ) -> Out17: 636 | ... 637 | 638 | 639 | @overload 640 | def pipe( 641 | value: InputVal, 642 | op0: Callable[[InputVal], Out0], 643 | op1: Callable[[Out0], Out1], 644 | op2: Callable[[Out1], Out2], 645 | op3: Callable[[Out2], Out3], 646 | op4: Callable[[Out3], Out4], 647 | op5: Callable[[Out4], Out5], 648 | op6: Callable[[Out5], Out6], 649 | op7: Callable[[Out6], Out7], 650 | op8: Callable[[Out7], Out8], 651 | op9: Callable[[Out8], Out9], 652 | op10: Callable[[Out9], Out10], 653 | op11: Callable[[Out10], Out11], 654 | op12: Callable[[Out11], Out12], 655 | op13: Callable[[Out12], Out13], 656 | op14: Callable[[Out13], Out14], 657 | op15: Callable[[Out14], Out15], 658 | op16: Callable[[Out15], Out16], 659 | op17: Callable[[Out16], Out17], 660 | op18: Callable[[Out17], Out18], 661 | /, 662 | ) -> Out18: 663 | ... 664 | 665 | 666 | def pipe(value: Any, op0: stand_in_callable = None, op1: stand_in_callable = None, op2: stand_in_callable = None, op3: stand_in_callable = None, op4: stand_in_callable = None, op5: stand_in_callable = None, op6: stand_in_callable = None, op7: stand_in_callable = None, op8: stand_in_callable = None, op9: stand_in_callable = None, op10: stand_in_callable = None, op11: stand_in_callable = None, op12: stand_in_callable = None, op13: stand_in_callable = None, op14: stand_in_callable = None, op15: stand_in_callable = None, op16: stand_in_callable = None, op17: stand_in_callable = None, op18: stand_in_callable = None, op19: stand_in_callable = None, /) -> Any: # type: ignore 667 | """ 668 | Pipe takes up to 20 functions and applies them to a value. 669 | """ 670 | 671 | if not op0: 672 | return value # fmt: skip 673 | 674 | elif not op1: 675 | return op0(value) # fmt: skip 676 | 677 | elif not op2: 678 | return op1(op0(value)) # fmt: skip 679 | 680 | elif not op3: 681 | return op2(op1(op0(value))) # fmt: skip 682 | 683 | elif not op4: 684 | return op3(op2(op1(op0(value)))) # fmt: skip 685 | 686 | elif not op5: 687 | return op4(op3(op2(op1(op0(value))))) # fmt: skip 688 | 689 | elif not op6: 690 | return op5(op4(op3(op2(op1(op0(value)))))) # fmt: skip 691 | 692 | elif not op7: 693 | return op6(op5(op4(op3(op2(op1(op0(value))))))) # fmt: skip 694 | 695 | elif not op8: 696 | return op7(op6(op5(op4(op3(op2(op1(op0(value)))))))) # fmt: skip 697 | 698 | elif not op9: 699 | return op8(op7(op6(op5(op4(op3(op2(op1(op0(value))))))))) # fmt: skip 700 | 701 | elif not op10: 702 | return op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(value)))))))))) # fmt: skip 703 | 704 | elif not op11: 705 | return op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(value))))))))))) # fmt: skip 706 | 707 | elif not op12: 708 | return op11(op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(value)))))))))))) # fmt: skip 709 | 710 | elif not op13: 711 | return op12(op11(op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(value))))))))))))) # fmt: skip 712 | 713 | elif not op14: 714 | return op13(op12(op11(op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(value)))))))))))))) # fmt: skip 715 | 716 | elif not op15: 717 | return op14(op13(op12(op11(op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(value))))))))))))))) # fmt: skip 718 | 719 | elif not op16: 720 | return op15(op14(op13(op12(op11(op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(value)))))))))))))))) # fmt: skip 721 | 722 | elif not op17: 723 | return op16(op15(op14(op13(op12(op11(op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(value))))))))))))))))) # fmt: skip 724 | 725 | elif not op18: 726 | return op17(op16(op15(op14(op13(op12(op11(op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(value)))))))))))))))))) # fmt: skip 727 | 728 | elif not op19: 729 | return op18(op17(op16(op15(op14(op13(op12(op11(op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(value))))))))))))))))))) # fmt: skip 730 | 731 | else: 732 | return op19(op18(op17(op16(op15(op14(op13(op12(op11(op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(value)))))))))))))))))))) # fmt: skip 733 | 734 | 735 | @overload 736 | def pipeline(op0: Callable[InputParams, Out0], /) -> Callable[InputParams, Out0]: 737 | ... 738 | 739 | 740 | @overload 741 | def pipeline( 742 | op0: Callable[InputParams, Out0], op1: Callable[[Out0], Out1], / 743 | ) -> Callable[InputParams, Out1]: 744 | ... 745 | 746 | 747 | @overload 748 | def pipeline( 749 | op0: Callable[InputParams, Out0], 750 | op1: Callable[[Out0], Out1], 751 | op2: Callable[[Out1], Out2], 752 | /, 753 | ) -> Callable[InputParams, Out2]: 754 | ... 755 | 756 | 757 | @overload 758 | def pipeline( 759 | op0: Callable[InputParams, Out0], 760 | op1: Callable[[Out0], Out1], 761 | op2: Callable[[Out1], Out2], 762 | op3: Callable[[Out2], Out3], 763 | /, 764 | ) -> Callable[InputParams, Out3]: 765 | ... 766 | 767 | 768 | @overload 769 | def pipeline( 770 | op0: Callable[InputParams, Out0], 771 | op1: Callable[[Out0], Out1], 772 | op2: Callable[[Out1], Out2], 773 | op3: Callable[[Out2], Out3], 774 | op4: Callable[[Out3], Out4], 775 | /, 776 | ) -> Callable[InputParams, Out4]: 777 | ... 778 | 779 | 780 | @overload 781 | def pipeline( 782 | op0: Callable[InputParams, Out0], 783 | op1: Callable[[Out0], Out1], 784 | op2: Callable[[Out1], Out2], 785 | op3: Callable[[Out2], Out3], 786 | op4: Callable[[Out3], Out4], 787 | op5: Callable[[Out4], Out5], 788 | /, 789 | ) -> Callable[InputParams, Out5]: 790 | ... 791 | 792 | 793 | @overload 794 | def pipeline( 795 | op0: Callable[InputParams, Out0], 796 | op1: Callable[[Out0], Out1], 797 | op2: Callable[[Out1], Out2], 798 | op3: Callable[[Out2], Out3], 799 | op4: Callable[[Out3], Out4], 800 | op5: Callable[[Out4], Out5], 801 | op6: Callable[[Out5], Out6], 802 | /, 803 | ) -> Callable[InputParams, Out6]: 804 | ... 805 | 806 | 807 | @overload 808 | def pipeline( 809 | op0: Callable[InputParams, Out0], 810 | op1: Callable[[Out0], Out1], 811 | op2: Callable[[Out1], Out2], 812 | op3: Callable[[Out2], Out3], 813 | op4: Callable[[Out3], Out4], 814 | op5: Callable[[Out4], Out5], 815 | op6: Callable[[Out5], Out6], 816 | op7: Callable[[Out6], Out7], 817 | /, 818 | ) -> Callable[InputParams, Out7]: 819 | ... 820 | 821 | 822 | @overload 823 | def pipeline( 824 | op0: Callable[InputParams, Out0], 825 | op1: Callable[[Out0], Out1], 826 | op2: Callable[[Out1], Out2], 827 | op3: Callable[[Out2], Out3], 828 | op4: Callable[[Out3], Out4], 829 | op5: Callable[[Out4], Out5], 830 | op6: Callable[[Out5], Out6], 831 | op7: Callable[[Out6], Out7], 832 | op8: Callable[[Out7], Out8], 833 | /, 834 | ) -> Callable[InputParams, Out8]: 835 | ... 836 | 837 | 838 | @overload 839 | def pipeline( 840 | op0: Callable[InputParams, Out0], 841 | op1: Callable[[Out0], Out1], 842 | op2: Callable[[Out1], Out2], 843 | op3: Callable[[Out2], Out3], 844 | op4: Callable[[Out3], Out4], 845 | op5: Callable[[Out4], Out5], 846 | op6: Callable[[Out5], Out6], 847 | op7: Callable[[Out6], Out7], 848 | op8: Callable[[Out7], Out8], 849 | op9: Callable[[Out8], Out9], 850 | /, 851 | ) -> Callable[InputParams, Out9]: 852 | ... 853 | 854 | 855 | @overload 856 | def pipeline( 857 | op0: Callable[InputParams, Out0], 858 | op1: Callable[[Out0], Out1], 859 | op2: Callable[[Out1], Out2], 860 | op3: Callable[[Out2], Out3], 861 | op4: Callable[[Out3], Out4], 862 | op5: Callable[[Out4], Out5], 863 | op6: Callable[[Out5], Out6], 864 | op7: Callable[[Out6], Out7], 865 | op8: Callable[[Out7], Out8], 866 | op9: Callable[[Out8], Out9], 867 | op10: Callable[[Out9], Out10], 868 | /, 869 | ) -> Callable[InputParams, Out10]: 870 | ... 871 | 872 | 873 | @overload 874 | def pipeline( 875 | op0: Callable[InputParams, Out0], 876 | op1: Callable[[Out0], Out1], 877 | op2: Callable[[Out1], Out2], 878 | op3: Callable[[Out2], Out3], 879 | op4: Callable[[Out3], Out4], 880 | op5: Callable[[Out4], Out5], 881 | op6: Callable[[Out5], Out6], 882 | op7: Callable[[Out6], Out7], 883 | op8: Callable[[Out7], Out8], 884 | op9: Callable[[Out8], Out9], 885 | op10: Callable[[Out9], Out10], 886 | op11: Callable[[Out10], Out11], 887 | /, 888 | ) -> Callable[InputParams, Out11]: 889 | ... 890 | 891 | 892 | @overload 893 | def pipeline( 894 | op0: Callable[InputParams, Out0], 895 | op1: Callable[[Out0], Out1], 896 | op2: Callable[[Out1], Out2], 897 | op3: Callable[[Out2], Out3], 898 | op4: Callable[[Out3], Out4], 899 | op5: Callable[[Out4], Out5], 900 | op6: Callable[[Out5], Out6], 901 | op7: Callable[[Out6], Out7], 902 | op8: Callable[[Out7], Out8], 903 | op9: Callable[[Out8], Out9], 904 | op10: Callable[[Out9], Out10], 905 | op11: Callable[[Out10], Out11], 906 | op12: Callable[[Out11], Out12], 907 | /, 908 | ) -> Callable[InputParams, Out12]: 909 | ... 910 | 911 | 912 | @overload 913 | def pipeline( 914 | op0: Callable[InputParams, Out0], 915 | op1: Callable[[Out0], Out1], 916 | op2: Callable[[Out1], Out2], 917 | op3: Callable[[Out2], Out3], 918 | op4: Callable[[Out3], Out4], 919 | op5: Callable[[Out4], Out5], 920 | op6: Callable[[Out5], Out6], 921 | op7: Callable[[Out6], Out7], 922 | op8: Callable[[Out7], Out8], 923 | op9: Callable[[Out8], Out9], 924 | op10: Callable[[Out9], Out10], 925 | op11: Callable[[Out10], Out11], 926 | op12: Callable[[Out11], Out12], 927 | op13: Callable[[Out12], Out13], 928 | /, 929 | ) -> Callable[InputParams, Out13]: 930 | ... 931 | 932 | 933 | @overload 934 | def pipeline( 935 | op0: Callable[InputParams, Out0], 936 | op1: Callable[[Out0], Out1], 937 | op2: Callable[[Out1], Out2], 938 | op3: Callable[[Out2], Out3], 939 | op4: Callable[[Out3], Out4], 940 | op5: Callable[[Out4], Out5], 941 | op6: Callable[[Out5], Out6], 942 | op7: Callable[[Out6], Out7], 943 | op8: Callable[[Out7], Out8], 944 | op9: Callable[[Out8], Out9], 945 | op10: Callable[[Out9], Out10], 946 | op11: Callable[[Out10], Out11], 947 | op12: Callable[[Out11], Out12], 948 | op13: Callable[[Out12], Out13], 949 | op14: Callable[[Out13], Out14], 950 | /, 951 | ) -> Callable[InputParams, Out14]: 952 | ... 953 | 954 | 955 | @overload 956 | def pipeline( 957 | op0: Callable[InputParams, Out0], 958 | op1: Callable[[Out0], Out1], 959 | op2: Callable[[Out1], Out2], 960 | op3: Callable[[Out2], Out3], 961 | op4: Callable[[Out3], Out4], 962 | op5: Callable[[Out4], Out5], 963 | op6: Callable[[Out5], Out6], 964 | op7: Callable[[Out6], Out7], 965 | op8: Callable[[Out7], Out8], 966 | op9: Callable[[Out8], Out9], 967 | op10: Callable[[Out9], Out10], 968 | op11: Callable[[Out10], Out11], 969 | op12: Callable[[Out11], Out12], 970 | op13: Callable[[Out12], Out13], 971 | op14: Callable[[Out13], Out14], 972 | op15: Callable[[Out14], Out15], 973 | /, 974 | ) -> Callable[InputParams, Out15]: 975 | ... 976 | 977 | 978 | @overload 979 | def pipeline( 980 | op0: Callable[InputParams, Out0], 981 | op1: Callable[[Out0], Out1], 982 | op2: Callable[[Out1], Out2], 983 | op3: Callable[[Out2], Out3], 984 | op4: Callable[[Out3], Out4], 985 | op5: Callable[[Out4], Out5], 986 | op6: Callable[[Out5], Out6], 987 | op7: Callable[[Out6], Out7], 988 | op8: Callable[[Out7], Out8], 989 | op9: Callable[[Out8], Out9], 990 | op10: Callable[[Out9], Out10], 991 | op11: Callable[[Out10], Out11], 992 | op12: Callable[[Out11], Out12], 993 | op13: Callable[[Out12], Out13], 994 | op14: Callable[[Out13], Out14], 995 | op15: Callable[[Out14], Out15], 996 | op16: Callable[[Out15], Out16], 997 | /, 998 | ) -> Callable[InputParams, Out16]: 999 | ... 1000 | 1001 | 1002 | @overload 1003 | def pipeline( 1004 | op0: Callable[InputParams, Out0], 1005 | op1: Callable[[Out0], Out1], 1006 | op2: Callable[[Out1], Out2], 1007 | op3: Callable[[Out2], Out3], 1008 | op4: Callable[[Out3], Out4], 1009 | op5: Callable[[Out4], Out5], 1010 | op6: Callable[[Out5], Out6], 1011 | op7: Callable[[Out6], Out7], 1012 | op8: Callable[[Out7], Out8], 1013 | op9: Callable[[Out8], Out9], 1014 | op10: Callable[[Out9], Out10], 1015 | op11: Callable[[Out10], Out11], 1016 | op12: Callable[[Out11], Out12], 1017 | op13: Callable[[Out12], Out13], 1018 | op14: Callable[[Out13], Out14], 1019 | op15: Callable[[Out14], Out15], 1020 | op16: Callable[[Out15], Out16], 1021 | op17: Callable[[Out16], Out17], 1022 | /, 1023 | ) -> Callable[InputParams, Out17]: 1024 | ... 1025 | 1026 | 1027 | @overload 1028 | def pipeline( 1029 | op0: Callable[InputParams, Out0], 1030 | op1: Callable[[Out0], Out1], 1031 | op2: Callable[[Out1], Out2], 1032 | op3: Callable[[Out2], Out3], 1033 | op4: Callable[[Out3], Out4], 1034 | op5: Callable[[Out4], Out5], 1035 | op6: Callable[[Out5], Out6], 1036 | op7: Callable[[Out6], Out7], 1037 | op8: Callable[[Out7], Out8], 1038 | op9: Callable[[Out8], Out9], 1039 | op10: Callable[[Out9], Out10], 1040 | op11: Callable[[Out10], Out11], 1041 | op12: Callable[[Out11], Out12], 1042 | op13: Callable[[Out12], Out13], 1043 | op14: Callable[[Out13], Out14], 1044 | op15: Callable[[Out14], Out15], 1045 | op16: Callable[[Out15], Out16], 1046 | op17: Callable[[Out16], Out17], 1047 | op18: Callable[[Out17], Out18], 1048 | /, 1049 | ) -> Callable[InputParams, Out18]: 1050 | ... 1051 | 1052 | 1053 | def pipeline(op0: Callable[InputParams, Out0], op1: Union[Callable[[Out0], Out1], None] = None, op2: Union[Callable[[Out1], Out2], None] = None, op3: Union[Callable[[Out2], Out3], None] = None, op4: Union[Callable[[Out3], Out4], None] = None, op5: Union[Callable[[Out4], Out5], None] = None, op6: Union[Callable[[Out5], Out6], None] = None, op7: Union[Callable[[Out6], Out7], None] = None, op8: Union[Callable[[Out7], Out8], None] = None, op9: Union[Callable[[Out8], Out9], None] = None, op10: Union[Callable[[Out9], Out10], None] = None, op11: Union[Callable[[Out10], Out11], None] = None, op12: Union[Callable[[Out11], Out12], None] = None, op13: Union[Callable[[Out12], Out13], None] = None, op14: Union[Callable[[Out13], Out14], None] = None, op15: Union[Callable[[Out14], Out15], None] = None, op16: Union[Callable[[Out15], Out16], None] = None, op17: Union[Callable[[Out16], Out17], None] = None, op18: Union[Callable[[Out17], Out18], None] = None, op19: Union[Callable[[Out18], Out19], None] = None, /) -> Callable[InputParams, Any]: # type: ignore 1054 | """ 1055 | Pipeline takes up to 20 functions and composites them into a single function. 1056 | """ 1057 | 1058 | if not op1: 1059 | 1060 | def _inner0(*args: InputParams.args, **kwargs: InputParams.kwargs) -> Out0: 1061 | return op0(*args, **kwargs) # fmt: skip 1062 | 1063 | return _inner0 1064 | 1065 | elif not op2: 1066 | 1067 | def _inner1(*args: InputParams.args, **kwargs: InputParams.kwargs) -> Out1: 1068 | return op1(op0(*args, **kwargs)) # fmt: skip 1069 | 1070 | return _inner1 1071 | 1072 | elif not op3: 1073 | 1074 | def _inner2(*args: InputParams.args, **kwargs: InputParams.kwargs) -> Out2: 1075 | return op2(op1(op0(*args, **kwargs))) # fmt: skip 1076 | 1077 | return _inner2 1078 | 1079 | elif not op4: 1080 | 1081 | def _inner3(*args: InputParams.args, **kwargs: InputParams.kwargs) -> Out3: 1082 | return op3(op2(op1(op0(*args, **kwargs)))) # fmt: skip 1083 | 1084 | return _inner3 1085 | 1086 | elif not op5: 1087 | 1088 | def _inner4(*args: InputParams.args, **kwargs: InputParams.kwargs) -> Out4: 1089 | return op4(op3(op2(op1(op0(*args, **kwargs))))) # fmt: skip 1090 | 1091 | return _inner4 1092 | 1093 | elif not op6: 1094 | 1095 | def _inner5(*args: InputParams.args, **kwargs: InputParams.kwargs) -> Out5: 1096 | return op5(op4(op3(op2(op1(op0(*args, **kwargs)))))) # fmt: skip 1097 | 1098 | return _inner5 1099 | 1100 | elif not op7: 1101 | 1102 | def _inner6(*args: InputParams.args, **kwargs: InputParams.kwargs) -> Out6: 1103 | return op6(op5(op4(op3(op2(op1(op0(*args, **kwargs))))))) # fmt: skip 1104 | 1105 | return _inner6 1106 | 1107 | elif not op8: 1108 | 1109 | def _inner7(*args: InputParams.args, **kwargs: InputParams.kwargs) -> Out7: 1110 | return op7(op6(op5(op4(op3(op2(op1(op0(*args, **kwargs)))))))) # fmt: skip 1111 | 1112 | return _inner7 1113 | 1114 | elif not op9: 1115 | 1116 | def _inner8(*args: InputParams.args, **kwargs: InputParams.kwargs) -> Out8: 1117 | return op8(op7(op6(op5(op4(op3(op2(op1(op0(*args, **kwargs))))))))) # fmt: skip 1118 | 1119 | return _inner8 1120 | 1121 | elif not op10: 1122 | 1123 | def _inner9(*args: InputParams.args, **kwargs: InputParams.kwargs) -> Out9: 1124 | return op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(*args, **kwargs)))))))))) # fmt: skip 1125 | 1126 | return _inner9 1127 | 1128 | elif not op11: 1129 | 1130 | def _inner10(*args: InputParams.args, **kwargs: InputParams.kwargs) -> Out10: 1131 | return op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(*args, **kwargs))))))))))) # fmt: skip 1132 | 1133 | return _inner10 1134 | 1135 | elif not op12: 1136 | 1137 | def _inner11(*args: InputParams.args, **kwargs: InputParams.kwargs) -> Out11: 1138 | return op11(op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(*args, **kwargs)))))))))))) # fmt: skip 1139 | 1140 | return _inner11 1141 | 1142 | elif not op13: 1143 | 1144 | def _inner12(*args: InputParams.args, **kwargs: InputParams.kwargs) -> Out12: 1145 | return op12(op11(op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(*args, **kwargs))))))))))))) # fmt: skip 1146 | 1147 | return _inner12 1148 | 1149 | elif not op14: 1150 | 1151 | def _inner13(*args: InputParams.args, **kwargs: InputParams.kwargs) -> Out13: 1152 | return op13(op12(op11(op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(*args, **kwargs)))))))))))))) # fmt: skip 1153 | 1154 | return _inner13 1155 | 1156 | elif not op15: 1157 | 1158 | def _inner14(*args: InputParams.args, **kwargs: InputParams.kwargs) -> Out14: 1159 | return op14(op13(op12(op11(op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(*args, **kwargs))))))))))))))) # fmt: skip 1160 | 1161 | return _inner14 1162 | 1163 | elif not op16: 1164 | 1165 | def _inner15(*args: InputParams.args, **kwargs: InputParams.kwargs) -> Out15: 1166 | return op15(op14(op13(op12(op11(op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(*args, **kwargs)))))))))))))))) # fmt: skip 1167 | 1168 | return _inner15 1169 | 1170 | elif not op17: 1171 | 1172 | def _inner16(*args: InputParams.args, **kwargs: InputParams.kwargs) -> Out16: 1173 | return op16(op15(op14(op13(op12(op11(op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(*args, **kwargs))))))))))))))))) # fmt: skip 1174 | 1175 | return _inner16 1176 | 1177 | elif not op18: 1178 | 1179 | def _inner17(*args: InputParams.args, **kwargs: InputParams.kwargs) -> Out17: 1180 | return op17(op16(op15(op14(op13(op12(op11(op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(*args, **kwargs)))))))))))))))))) # fmt: skip 1181 | 1182 | return _inner17 1183 | 1184 | elif not op19: 1185 | 1186 | def _inner18(*args: InputParams.args, **kwargs: InputParams.kwargs) -> Out18: 1187 | return op18(op17(op16(op15(op14(op13(op12(op11(op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(*args, **kwargs))))))))))))))))))) # fmt: skip 1188 | 1189 | return _inner18 1190 | 1191 | else: 1192 | 1193 | def _inner19(*args: InputParams.args, **kwargs: InputParams.kwargs) -> Out19: 1194 | return op19(op18(op17(op16(op15(op14(op13(op12(op11(op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(*args, **kwargs)))))))))))))))))))) # fmt: skip 1195 | 1196 | return _inner19 1197 | 1198 | 1199 | def arbitary_length_pipe(value: Any, *funcs: Callable[[Any], Any]) -> Any: 1200 | """ 1201 | Pipe that takes an arbitary amount of functions. 1202 | """ 1203 | for func in funcs: 1204 | value = func(value) 1205 | return value 1206 | -------------------------------------------------------------------------------- /src/function_pipes/without_paramspec/function_pipes.py: -------------------------------------------------------------------------------- 1 | """ 2 | function-pipe 3 | 4 | Functions for pipe syntax in Python. 5 | 6 | 7 | Version without ParamSpec for Python 3.8-3.9. 8 | 9 | Read more at https://github.com/ajparsons/function-pipes 10 | 11 | Licence: MIT 12 | 13 | """ 14 | # pylint: disable=line-too-long 15 | 16 | from ast import ( 17 | Call, 18 | Lambda, 19 | Load, 20 | Name, 21 | NamedExpr, 22 | NodeTransformer, 23 | NodeVisitor, 24 | Store, 25 | expr, 26 | increment_lineno, 27 | parse, 28 | walk, 29 | ) 30 | from inspect import getsource 31 | from itertools import takewhile 32 | from textwrap import dedent 33 | from typing import Any, Union, Callable, TypeVar, overload 34 | 35 | 36 | T = TypeVar("T") 37 | 38 | 39 | class _LambdaExtractor(NodeTransformer): 40 | """ 41 | Replace references to the lambda argument with the passed in value 42 | """ 43 | 44 | def __init__( 45 | self, 46 | _lambda: Lambda, 47 | value: Union[expr, Call], 48 | subsequent_value: Union[expr, Call, None] = None, 49 | ): 50 | self._lambda = _lambda 51 | self.arg_name = self._lambda.args.args[0].arg # type: ignore 52 | self.visit_count = 0 53 | self.value = value 54 | self.subsequent_value = subsequent_value 55 | 56 | def extract_and_replace(self): 57 | """ 58 | Return what the lambda does, but replaces 59 | references to the lambda arg with 60 | the passed in value 61 | """ 62 | self.visit(self._lambda.body) 63 | return self._lambda.body 64 | 65 | def visit_Name(self, node: Name): 66 | """ 67 | Replace the internal lambda arg reference with the given value 68 | If a subsequent value given, replace all values after the first with that 69 | This allows assigning using a walrus in the first value, and then 70 | using that value without recalculation in the second. 71 | """ 72 | if node.id == self.arg_name: 73 | if self.visit_count == 0: 74 | self.visit_count += 1 75 | return self.value 76 | if self.subsequent_value: 77 | return self.subsequent_value 78 | else: 79 | raise ValueError("This lambda contains multiple references to the arg") 80 | return node 81 | 82 | 83 | def copy_position(source: expr, destination: expr): 84 | """ 85 | Copy the position information from one AST node to another 86 | """ 87 | destination.lineno = source.lineno 88 | destination.end_lineno = source.end_lineno 89 | destination.col_offset = source.col_offset 90 | destination.end_col_offset = source.end_col_offset 91 | 92 | 93 | class _CountLambdaArgUses(NodeVisitor): 94 | """ 95 | Count the number of uses of the first argument in the lambda 96 | in the lambda definition 97 | """ 98 | 99 | def __init__(self, _lambda: Lambda): 100 | self._lambda = _lambda 101 | self.arg_name = self._lambda.args.args[0].arg # type: ignore 102 | self.uses: int = 0 103 | 104 | def check(self) -> int: 105 | """ 106 | Get the number of times the arugment is referenced 107 | """ 108 | self.visit(self._lambda.body) # type: ignore 109 | return self.uses 110 | 111 | def visit_Name(self, node: Name): 112 | """ 113 | Increment the uses count if the name is the given arg 114 | """ 115 | if node.id == self.arg_name: 116 | self.uses += 1 117 | self.generic_visit(node) 118 | 119 | 120 | class _PipeTransformer(NodeTransformer): 121 | """ 122 | A NodeTransformer that rewrites the code tree so that all references to replaced with 123 | a set of nested function calls. 124 | 125 | a = pipe(a,b,c,d) 126 | 127 | becomes 128 | 129 | a = d(c(b(a))) 130 | 131 | This also expands lambdas so that there is no function calling overhead. 132 | 133 | a = pipe(a,b,c,lambda x: x+1) 134 | 135 | becomes: 136 | 137 | a = (c(b(a))) + 1 138 | 139 | Where there are multiple uses of the argument in a lambda, 140 | a walrus is used to avoid duplication calculations. 141 | 142 | a = pipe(a,b,c,lambda x: x + x + 1) 143 | 144 | becomes: 145 | 146 | a = (var := c(b(a))) + var + 1 147 | 148 | """ 149 | 150 | def visit_Call(self, node: Call) -> Any: 151 | """ 152 | Replace all references to the pipe function with nested function calls. 153 | """ 154 | if node.func.id == "pipe": # type: ignore 155 | value = node.args[0] 156 | funcs = node.args[1:] 157 | 158 | # unpack the functions into nested calls 159 | # unless the function is a lambda 160 | # in which case, the lambda's body 161 | # needs to be unpacked 162 | for func in funcs: 163 | if isinstance(func, Lambda): 164 | arg_usage = _CountLambdaArgUses(func).check() 165 | if arg_usage == 0: 166 | # this will throw an error at build time rather than runtime 167 | # but shouldn't be a surprise to typecheckers 168 | raise ValueError("This lambda has no arguments.") 169 | elif arg_usage == 1: 170 | # if the lambda only uses the argument once 171 | # can just substitute the arg in the lambda 172 | # with the value in the loop 173 | # e.g. a = pipe(5, lambda x: x + 1) 174 | # becomes a = 5 + 1 175 | value = _LambdaExtractor(func, value).extract_and_replace() 176 | elif arg_usage > 1: 177 | # if the lambda uses the argument more than once 178 | # have to assign the value to a variable first 179 | # (using a walrus) 180 | # and then use the variable in the lambda in subsequent calls. 181 | # e.g. a = pipe(5, lambda x: x + x + 1) 182 | # becomes a = (var := 5) + var + 1 183 | # NamedExpr is how := works behind the scenes. 184 | walrus = NamedExpr( 185 | target=Name(id="_pipe_temp_var", ctx=Store()), value=value 186 | ) 187 | walrus.lineno = func.lineno 188 | copy_position(func, walrus) 189 | temp_var = Name(id="_pipe_temp_var", ctx=Load()) 190 | copy_position(func, temp_var) 191 | value = _LambdaExtractor( 192 | func, walrus, temp_var 193 | ).extract_and_replace() 194 | else: 195 | # if just a function, we're just building a nesting call chain 196 | value = Call(func, [value], []) 197 | copy_position(func, value) 198 | return value # type: ignore 199 | return self.generic_visit(node) 200 | 201 | 202 | def fast_pipes(func: Callable[..., T]) -> Callable[..., T]: 203 | """ 204 | Decorator function that replaces references to pipe with 205 | the direct equivalent of the pipe function. 206 | """ 207 | 208 | # This approach adapted from 209 | # adapted from https://github.com/robinhilliard/pipes/blob/master/pipeop/__init__.py 210 | ctx = func.__globals__ 211 | first_line_number = func.__code__.co_firstlineno 212 | 213 | source = getsource(func) 214 | 215 | # AST data structure representing parsed function code 216 | tree = parse(dedent(source)) 217 | 218 | # Fix line and column numbers so that debuggers still work 219 | increment_lineno(tree, first_line_number - 1) 220 | source_indent = sum([1 for _ in takewhile(str.isspace, source)]) + 1 221 | 222 | for node in walk(tree): 223 | if hasattr(node, "col_offset"): 224 | node.col_offset += source_indent 225 | 226 | # Update name of function or class to compile 227 | tree.body[0].name += "_fast_pipe" # type: ignore 228 | 229 | # remove the pipe decorator so that we don't recursively 230 | # call it again. The AST node for the decorator will be a 231 | # Call if it had braces, and a Name if it had no braces. 232 | # The location of the decorator function name in these 233 | # nodes is slightly different. 234 | tree.body[0].decorator_list = [ # type: ignore 235 | d 236 | for d in tree.body[0].decorator_list # type: ignore 237 | if isinstance(d, Call) 238 | and d.func.id != "fast_pipes" # type: ignore 239 | or isinstance(d, Name) 240 | and d.id != "fast_pipes" 241 | ] 242 | 243 | # Apply the visit_Call transformation 244 | tree = _PipeTransformer().visit(tree) 245 | 246 | # now compile the AST into an altered function or class definition 247 | try: 248 | code = compile( 249 | tree, 250 | filename=(ctx["__file__"] if "__file__" in ctx else "repl"), 251 | mode="exec", 252 | ) 253 | except SyntaxError as e: 254 | # The syntax is rearranged in a way that triggers a starred error correctly 255 | # Easier to adjust the error here than figure out how to raise it properly 256 | # in the AST visitor. 257 | # This is a bit hacky, but it's good enough for now. 258 | if e.msg == "can't use starred expression here" and ( 259 | e.text and "pipe(" in e.text 260 | ): 261 | e.msg = "pipe can't take a starred expression as an argument when fast_pipes is used." 262 | raise e 263 | 264 | # and execute the definition in the original context so that the 265 | # decorated function can access the same scopes as the original 266 | exec(code, ctx) 267 | 268 | # return the modified function or class - original is nevers called 269 | return ctx[tree.body[0].name] 270 | 271 | 272 | BridgeType = TypeVar("BridgeType") 273 | InputVal = TypeVar("InputVal") 274 | 275 | # Always overridden by the overloads but is 276 | # self consistent in the declared function 277 | stand_in_callable = Union[Callable[..., Any], None] 278 | 279 | 280 | Out0 = TypeVar("Out0") 281 | Out1 = TypeVar("Out1") 282 | Out2 = TypeVar("Out2") 283 | Out3 = TypeVar("Out3") 284 | Out4 = TypeVar("Out4") 285 | Out5 = TypeVar("Out5") 286 | Out6 = TypeVar("Out6") 287 | Out7 = TypeVar("Out7") 288 | Out8 = TypeVar("Out8") 289 | Out9 = TypeVar("Out9") 290 | Out10 = TypeVar("Out10") 291 | Out11 = TypeVar("Out11") 292 | Out12 = TypeVar("Out12") 293 | Out13 = TypeVar("Out13") 294 | Out14 = TypeVar("Out14") 295 | Out15 = TypeVar("Out15") 296 | Out16 = TypeVar("Out16") 297 | Out17 = TypeVar("Out17") 298 | Out18 = TypeVar("Out18") 299 | Out19 = TypeVar("Out19") 300 | 301 | 302 | def pipe_bridge( 303 | func: Callable[[BridgeType], Any] 304 | ) -> Callable[[BridgeType], BridgeType]: 305 | """ 306 | When debugging, you might want to use a function to see 307 | the current value in the pipe, but examination functions. 308 | may not return the value to let it continue down the chain. 309 | This wraps the function so that it does it's job, and then 310 | returns the original value to conitnue down the chain. 311 | For instance: 312 | ``` 313 | bridge(rich.print) 314 | ``` 315 | Will use the rich library's print function to look at the value, 316 | but then unlike calling `rich.print` directly in the pipe, 317 | will return the value to let it continue. 318 | """ 319 | 320 | def _inner(value: BridgeType) -> BridgeType: 321 | func(value) 322 | return value 323 | 324 | return _inner 325 | 326 | 327 | @overload 328 | def pipe(value: InputVal, op0: Callable[[InputVal], Out0], /) -> Out0: 329 | ... 330 | 331 | 332 | @overload 333 | def pipe( 334 | value: InputVal, op0: Callable[[InputVal], Out0], op1: Callable[[Out0], Out1], / 335 | ) -> Out1: 336 | ... 337 | 338 | 339 | @overload 340 | def pipe( 341 | value: InputVal, 342 | op0: Callable[[InputVal], Out0], 343 | op1: Callable[[Out0], Out1], 344 | op2: Callable[[Out1], Out2], 345 | /, 346 | ) -> Out2: 347 | ... 348 | 349 | 350 | @overload 351 | def pipe( 352 | value: InputVal, 353 | op0: Callable[[InputVal], Out0], 354 | op1: Callable[[Out0], Out1], 355 | op2: Callable[[Out1], Out2], 356 | op3: Callable[[Out2], Out3], 357 | /, 358 | ) -> Out3: 359 | ... 360 | 361 | 362 | @overload 363 | def pipe( 364 | value: InputVal, 365 | op0: Callable[[InputVal], Out0], 366 | op1: Callable[[Out0], Out1], 367 | op2: Callable[[Out1], Out2], 368 | op3: Callable[[Out2], Out3], 369 | op4: Callable[[Out3], Out4], 370 | /, 371 | ) -> Out4: 372 | ... 373 | 374 | 375 | @overload 376 | def pipe( 377 | value: InputVal, 378 | op0: Callable[[InputVal], Out0], 379 | op1: Callable[[Out0], Out1], 380 | op2: Callable[[Out1], Out2], 381 | op3: Callable[[Out2], Out3], 382 | op4: Callable[[Out3], Out4], 383 | op5: Callable[[Out4], Out5], 384 | /, 385 | ) -> Out5: 386 | ... 387 | 388 | 389 | @overload 390 | def pipe( 391 | value: InputVal, 392 | op0: Callable[[InputVal], Out0], 393 | op1: Callable[[Out0], Out1], 394 | op2: Callable[[Out1], Out2], 395 | op3: Callable[[Out2], Out3], 396 | op4: Callable[[Out3], Out4], 397 | op5: Callable[[Out4], Out5], 398 | op6: Callable[[Out5], Out6], 399 | /, 400 | ) -> Out6: 401 | ... 402 | 403 | 404 | @overload 405 | def pipe( 406 | value: InputVal, 407 | op0: Callable[[InputVal], Out0], 408 | op1: Callable[[Out0], Out1], 409 | op2: Callable[[Out1], Out2], 410 | op3: Callable[[Out2], Out3], 411 | op4: Callable[[Out3], Out4], 412 | op5: Callable[[Out4], Out5], 413 | op6: Callable[[Out5], Out6], 414 | op7: Callable[[Out6], Out7], 415 | /, 416 | ) -> Out7: 417 | ... 418 | 419 | 420 | @overload 421 | def pipe( 422 | value: InputVal, 423 | op0: Callable[[InputVal], Out0], 424 | op1: Callable[[Out0], Out1], 425 | op2: Callable[[Out1], Out2], 426 | op3: Callable[[Out2], Out3], 427 | op4: Callable[[Out3], Out4], 428 | op5: Callable[[Out4], Out5], 429 | op6: Callable[[Out5], Out6], 430 | op7: Callable[[Out6], Out7], 431 | op8: Callable[[Out7], Out8], 432 | /, 433 | ) -> Out8: 434 | ... 435 | 436 | 437 | @overload 438 | def pipe( 439 | value: InputVal, 440 | op0: Callable[[InputVal], Out0], 441 | op1: Callable[[Out0], Out1], 442 | op2: Callable[[Out1], Out2], 443 | op3: Callable[[Out2], Out3], 444 | op4: Callable[[Out3], Out4], 445 | op5: Callable[[Out4], Out5], 446 | op6: Callable[[Out5], Out6], 447 | op7: Callable[[Out6], Out7], 448 | op8: Callable[[Out7], Out8], 449 | op9: Callable[[Out8], Out9], 450 | /, 451 | ) -> Out9: 452 | ... 453 | 454 | 455 | @overload 456 | def pipe( 457 | value: InputVal, 458 | op0: Callable[[InputVal], Out0], 459 | op1: Callable[[Out0], Out1], 460 | op2: Callable[[Out1], Out2], 461 | op3: Callable[[Out2], Out3], 462 | op4: Callable[[Out3], Out4], 463 | op5: Callable[[Out4], Out5], 464 | op6: Callable[[Out5], Out6], 465 | op7: Callable[[Out6], Out7], 466 | op8: Callable[[Out7], Out8], 467 | op9: Callable[[Out8], Out9], 468 | op10: Callable[[Out9], Out10], 469 | /, 470 | ) -> Out10: 471 | ... 472 | 473 | 474 | @overload 475 | def pipe( 476 | value: InputVal, 477 | op0: Callable[[InputVal], Out0], 478 | op1: Callable[[Out0], Out1], 479 | op2: Callable[[Out1], Out2], 480 | op3: Callable[[Out2], Out3], 481 | op4: Callable[[Out3], Out4], 482 | op5: Callable[[Out4], Out5], 483 | op6: Callable[[Out5], Out6], 484 | op7: Callable[[Out6], Out7], 485 | op8: Callable[[Out7], Out8], 486 | op9: Callable[[Out8], Out9], 487 | op10: Callable[[Out9], Out10], 488 | op11: Callable[[Out10], Out11], 489 | /, 490 | ) -> Out11: 491 | ... 492 | 493 | 494 | @overload 495 | def pipe( 496 | value: InputVal, 497 | op0: Callable[[InputVal], Out0], 498 | op1: Callable[[Out0], Out1], 499 | op2: Callable[[Out1], Out2], 500 | op3: Callable[[Out2], Out3], 501 | op4: Callable[[Out3], Out4], 502 | op5: Callable[[Out4], Out5], 503 | op6: Callable[[Out5], Out6], 504 | op7: Callable[[Out6], Out7], 505 | op8: Callable[[Out7], Out8], 506 | op9: Callable[[Out8], Out9], 507 | op10: Callable[[Out9], Out10], 508 | op11: Callable[[Out10], Out11], 509 | op12: Callable[[Out11], Out12], 510 | /, 511 | ) -> Out12: 512 | ... 513 | 514 | 515 | @overload 516 | def pipe( 517 | value: InputVal, 518 | op0: Callable[[InputVal], Out0], 519 | op1: Callable[[Out0], Out1], 520 | op2: Callable[[Out1], Out2], 521 | op3: Callable[[Out2], Out3], 522 | op4: Callable[[Out3], Out4], 523 | op5: Callable[[Out4], Out5], 524 | op6: Callable[[Out5], Out6], 525 | op7: Callable[[Out6], Out7], 526 | op8: Callable[[Out7], Out8], 527 | op9: Callable[[Out8], Out9], 528 | op10: Callable[[Out9], Out10], 529 | op11: Callable[[Out10], Out11], 530 | op12: Callable[[Out11], Out12], 531 | op13: Callable[[Out12], Out13], 532 | /, 533 | ) -> Out13: 534 | ... 535 | 536 | 537 | @overload 538 | def pipe( 539 | value: InputVal, 540 | op0: Callable[[InputVal], Out0], 541 | op1: Callable[[Out0], Out1], 542 | op2: Callable[[Out1], Out2], 543 | op3: Callable[[Out2], Out3], 544 | op4: Callable[[Out3], Out4], 545 | op5: Callable[[Out4], Out5], 546 | op6: Callable[[Out5], Out6], 547 | op7: Callable[[Out6], Out7], 548 | op8: Callable[[Out7], Out8], 549 | op9: Callable[[Out8], Out9], 550 | op10: Callable[[Out9], Out10], 551 | op11: Callable[[Out10], Out11], 552 | op12: Callable[[Out11], Out12], 553 | op13: Callable[[Out12], Out13], 554 | op14: Callable[[Out13], Out14], 555 | /, 556 | ) -> Out14: 557 | ... 558 | 559 | 560 | @overload 561 | def pipe( 562 | value: InputVal, 563 | op0: Callable[[InputVal], Out0], 564 | op1: Callable[[Out0], Out1], 565 | op2: Callable[[Out1], Out2], 566 | op3: Callable[[Out2], Out3], 567 | op4: Callable[[Out3], Out4], 568 | op5: Callable[[Out4], Out5], 569 | op6: Callable[[Out5], Out6], 570 | op7: Callable[[Out6], Out7], 571 | op8: Callable[[Out7], Out8], 572 | op9: Callable[[Out8], Out9], 573 | op10: Callable[[Out9], Out10], 574 | op11: Callable[[Out10], Out11], 575 | op12: Callable[[Out11], Out12], 576 | op13: Callable[[Out12], Out13], 577 | op14: Callable[[Out13], Out14], 578 | op15: Callable[[Out14], Out15], 579 | /, 580 | ) -> Out15: 581 | ... 582 | 583 | 584 | @overload 585 | def pipe( 586 | value: InputVal, 587 | op0: Callable[[InputVal], Out0], 588 | op1: Callable[[Out0], Out1], 589 | op2: Callable[[Out1], Out2], 590 | op3: Callable[[Out2], Out3], 591 | op4: Callable[[Out3], Out4], 592 | op5: Callable[[Out4], Out5], 593 | op6: Callable[[Out5], Out6], 594 | op7: Callable[[Out6], Out7], 595 | op8: Callable[[Out7], Out8], 596 | op9: Callable[[Out8], Out9], 597 | op10: Callable[[Out9], Out10], 598 | op11: Callable[[Out10], Out11], 599 | op12: Callable[[Out11], Out12], 600 | op13: Callable[[Out12], Out13], 601 | op14: Callable[[Out13], Out14], 602 | op15: Callable[[Out14], Out15], 603 | op16: Callable[[Out15], Out16], 604 | /, 605 | ) -> Out16: 606 | ... 607 | 608 | 609 | @overload 610 | def pipe( 611 | value: InputVal, 612 | op0: Callable[[InputVal], Out0], 613 | op1: Callable[[Out0], Out1], 614 | op2: Callable[[Out1], Out2], 615 | op3: Callable[[Out2], Out3], 616 | op4: Callable[[Out3], Out4], 617 | op5: Callable[[Out4], Out5], 618 | op6: Callable[[Out5], Out6], 619 | op7: Callable[[Out6], Out7], 620 | op8: Callable[[Out7], Out8], 621 | op9: Callable[[Out8], Out9], 622 | op10: Callable[[Out9], Out10], 623 | op11: Callable[[Out10], Out11], 624 | op12: Callable[[Out11], Out12], 625 | op13: Callable[[Out12], Out13], 626 | op14: Callable[[Out13], Out14], 627 | op15: Callable[[Out14], Out15], 628 | op16: Callable[[Out15], Out16], 629 | op17: Callable[[Out16], Out17], 630 | /, 631 | ) -> Out17: 632 | ... 633 | 634 | 635 | @overload 636 | def pipe( 637 | value: InputVal, 638 | op0: Callable[[InputVal], Out0], 639 | op1: Callable[[Out0], Out1], 640 | op2: Callable[[Out1], Out2], 641 | op3: Callable[[Out2], Out3], 642 | op4: Callable[[Out3], Out4], 643 | op5: Callable[[Out4], Out5], 644 | op6: Callable[[Out5], Out6], 645 | op7: Callable[[Out6], Out7], 646 | op8: Callable[[Out7], Out8], 647 | op9: Callable[[Out8], Out9], 648 | op10: Callable[[Out9], Out10], 649 | op11: Callable[[Out10], Out11], 650 | op12: Callable[[Out11], Out12], 651 | op13: Callable[[Out12], Out13], 652 | op14: Callable[[Out13], Out14], 653 | op15: Callable[[Out14], Out15], 654 | op16: Callable[[Out15], Out16], 655 | op17: Callable[[Out16], Out17], 656 | op18: Callable[[Out17], Out18], 657 | /, 658 | ) -> Out18: 659 | ... 660 | 661 | 662 | def pipe(value: Any, op0: stand_in_callable = None, op1: stand_in_callable = None, op2: stand_in_callable = None, op3: stand_in_callable = None, op4: stand_in_callable = None, op5: stand_in_callable = None, op6: stand_in_callable = None, op7: stand_in_callable = None, op8: stand_in_callable = None, op9: stand_in_callable = None, op10: stand_in_callable = None, op11: stand_in_callable = None, op12: stand_in_callable = None, op13: stand_in_callable = None, op14: stand_in_callable = None, op15: stand_in_callable = None, op16: stand_in_callable = None, op17: stand_in_callable = None, op18: stand_in_callable = None, op19: stand_in_callable = None, /) -> Any: # type: ignore 663 | """ 664 | Pipe takes up to 20 functions and applies them to a value. 665 | """ 666 | 667 | if not op0: 668 | return value # fmt: skip 669 | 670 | elif not op1: 671 | return op0(value) # fmt: skip 672 | 673 | elif not op2: 674 | return op1(op0(value)) # fmt: skip 675 | 676 | elif not op3: 677 | return op2(op1(op0(value))) # fmt: skip 678 | 679 | elif not op4: 680 | return op3(op2(op1(op0(value)))) # fmt: skip 681 | 682 | elif not op5: 683 | return op4(op3(op2(op1(op0(value))))) # fmt: skip 684 | 685 | elif not op6: 686 | return op5(op4(op3(op2(op1(op0(value)))))) # fmt: skip 687 | 688 | elif not op7: 689 | return op6(op5(op4(op3(op2(op1(op0(value))))))) # fmt: skip 690 | 691 | elif not op8: 692 | return op7(op6(op5(op4(op3(op2(op1(op0(value)))))))) # fmt: skip 693 | 694 | elif not op9: 695 | return op8(op7(op6(op5(op4(op3(op2(op1(op0(value))))))))) # fmt: skip 696 | 697 | elif not op10: 698 | return op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(value)))))))))) # fmt: skip 699 | 700 | elif not op11: 701 | return op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(value))))))))))) # fmt: skip 702 | 703 | elif not op12: 704 | return op11(op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(value)))))))))))) # fmt: skip 705 | 706 | elif not op13: 707 | return op12(op11(op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(value))))))))))))) # fmt: skip 708 | 709 | elif not op14: 710 | return op13(op12(op11(op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(value)))))))))))))) # fmt: skip 711 | 712 | elif not op15: 713 | return op14(op13(op12(op11(op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(value))))))))))))))) # fmt: skip 714 | 715 | elif not op16: 716 | return op15(op14(op13(op12(op11(op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(value)))))))))))))))) # fmt: skip 717 | 718 | elif not op17: 719 | return op16(op15(op14(op13(op12(op11(op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(value))))))))))))))))) # fmt: skip 720 | 721 | elif not op18: 722 | return op17(op16(op15(op14(op13(op12(op11(op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(value)))))))))))))))))) # fmt: skip 723 | 724 | elif not op19: 725 | return op18(op17(op16(op15(op14(op13(op12(op11(op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(value))))))))))))))))))) # fmt: skip 726 | 727 | else: 728 | return op19(op18(op17(op16(op15(op14(op13(op12(op11(op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(value)))))))))))))))))))) # fmt: skip 729 | 730 | 731 | @overload 732 | def pipeline(op0: Callable[..., Out0], /) -> Callable[..., Out0]: 733 | ... 734 | 735 | 736 | @overload 737 | def pipeline( 738 | op0: Callable[..., Out0], op1: Callable[[Out0], Out1], / 739 | ) -> Callable[..., Out1]: 740 | ... 741 | 742 | 743 | @overload 744 | def pipeline( 745 | op0: Callable[..., Out0], 746 | op1: Callable[[Out0], Out1], 747 | op2: Callable[[Out1], Out2], 748 | /, 749 | ) -> Callable[..., Out2]: 750 | ... 751 | 752 | 753 | @overload 754 | def pipeline( 755 | op0: Callable[..., Out0], 756 | op1: Callable[[Out0], Out1], 757 | op2: Callable[[Out1], Out2], 758 | op3: Callable[[Out2], Out3], 759 | /, 760 | ) -> Callable[..., Out3]: 761 | ... 762 | 763 | 764 | @overload 765 | def pipeline( 766 | op0: Callable[..., Out0], 767 | op1: Callable[[Out0], Out1], 768 | op2: Callable[[Out1], Out2], 769 | op3: Callable[[Out2], Out3], 770 | op4: Callable[[Out3], Out4], 771 | /, 772 | ) -> Callable[..., Out4]: 773 | ... 774 | 775 | 776 | @overload 777 | def pipeline( 778 | op0: Callable[..., Out0], 779 | op1: Callable[[Out0], Out1], 780 | op2: Callable[[Out1], Out2], 781 | op3: Callable[[Out2], Out3], 782 | op4: Callable[[Out3], Out4], 783 | op5: Callable[[Out4], Out5], 784 | /, 785 | ) -> Callable[..., Out5]: 786 | ... 787 | 788 | 789 | @overload 790 | def pipeline( 791 | op0: Callable[..., Out0], 792 | op1: Callable[[Out0], Out1], 793 | op2: Callable[[Out1], Out2], 794 | op3: Callable[[Out2], Out3], 795 | op4: Callable[[Out3], Out4], 796 | op5: Callable[[Out4], Out5], 797 | op6: Callable[[Out5], Out6], 798 | /, 799 | ) -> Callable[..., Out6]: 800 | ... 801 | 802 | 803 | @overload 804 | def pipeline( 805 | op0: Callable[..., Out0], 806 | op1: Callable[[Out0], Out1], 807 | op2: Callable[[Out1], Out2], 808 | op3: Callable[[Out2], Out3], 809 | op4: Callable[[Out3], Out4], 810 | op5: Callable[[Out4], Out5], 811 | op6: Callable[[Out5], Out6], 812 | op7: Callable[[Out6], Out7], 813 | /, 814 | ) -> Callable[..., Out7]: 815 | ... 816 | 817 | 818 | @overload 819 | def pipeline( 820 | op0: Callable[..., Out0], 821 | op1: Callable[[Out0], Out1], 822 | op2: Callable[[Out1], Out2], 823 | op3: Callable[[Out2], Out3], 824 | op4: Callable[[Out3], Out4], 825 | op5: Callable[[Out4], Out5], 826 | op6: Callable[[Out5], Out6], 827 | op7: Callable[[Out6], Out7], 828 | op8: Callable[[Out7], Out8], 829 | /, 830 | ) -> Callable[..., Out8]: 831 | ... 832 | 833 | 834 | @overload 835 | def pipeline( 836 | op0: Callable[..., Out0], 837 | op1: Callable[[Out0], Out1], 838 | op2: Callable[[Out1], Out2], 839 | op3: Callable[[Out2], Out3], 840 | op4: Callable[[Out3], Out4], 841 | op5: Callable[[Out4], Out5], 842 | op6: Callable[[Out5], Out6], 843 | op7: Callable[[Out6], Out7], 844 | op8: Callable[[Out7], Out8], 845 | op9: Callable[[Out8], Out9], 846 | /, 847 | ) -> Callable[..., Out9]: 848 | ... 849 | 850 | 851 | @overload 852 | def pipeline( 853 | op0: Callable[..., Out0], 854 | op1: Callable[[Out0], Out1], 855 | op2: Callable[[Out1], Out2], 856 | op3: Callable[[Out2], Out3], 857 | op4: Callable[[Out3], Out4], 858 | op5: Callable[[Out4], Out5], 859 | op6: Callable[[Out5], Out6], 860 | op7: Callable[[Out6], Out7], 861 | op8: Callable[[Out7], Out8], 862 | op9: Callable[[Out8], Out9], 863 | op10: Callable[[Out9], Out10], 864 | /, 865 | ) -> Callable[..., Out10]: 866 | ... 867 | 868 | 869 | @overload 870 | def pipeline( 871 | op0: Callable[..., Out0], 872 | op1: Callable[[Out0], Out1], 873 | op2: Callable[[Out1], Out2], 874 | op3: Callable[[Out2], Out3], 875 | op4: Callable[[Out3], Out4], 876 | op5: Callable[[Out4], Out5], 877 | op6: Callable[[Out5], Out6], 878 | op7: Callable[[Out6], Out7], 879 | op8: Callable[[Out7], Out8], 880 | op9: Callable[[Out8], Out9], 881 | op10: Callable[[Out9], Out10], 882 | op11: Callable[[Out10], Out11], 883 | /, 884 | ) -> Callable[..., Out11]: 885 | ... 886 | 887 | 888 | @overload 889 | def pipeline( 890 | op0: Callable[..., Out0], 891 | op1: Callable[[Out0], Out1], 892 | op2: Callable[[Out1], Out2], 893 | op3: Callable[[Out2], Out3], 894 | op4: Callable[[Out3], Out4], 895 | op5: Callable[[Out4], Out5], 896 | op6: Callable[[Out5], Out6], 897 | op7: Callable[[Out6], Out7], 898 | op8: Callable[[Out7], Out8], 899 | op9: Callable[[Out8], Out9], 900 | op10: Callable[[Out9], Out10], 901 | op11: Callable[[Out10], Out11], 902 | op12: Callable[[Out11], Out12], 903 | /, 904 | ) -> Callable[..., Out12]: 905 | ... 906 | 907 | 908 | @overload 909 | def pipeline( 910 | op0: Callable[..., Out0], 911 | op1: Callable[[Out0], Out1], 912 | op2: Callable[[Out1], Out2], 913 | op3: Callable[[Out2], Out3], 914 | op4: Callable[[Out3], Out4], 915 | op5: Callable[[Out4], Out5], 916 | op6: Callable[[Out5], Out6], 917 | op7: Callable[[Out6], Out7], 918 | op8: Callable[[Out7], Out8], 919 | op9: Callable[[Out8], Out9], 920 | op10: Callable[[Out9], Out10], 921 | op11: Callable[[Out10], Out11], 922 | op12: Callable[[Out11], Out12], 923 | op13: Callable[[Out12], Out13], 924 | /, 925 | ) -> Callable[..., Out13]: 926 | ... 927 | 928 | 929 | @overload 930 | def pipeline( 931 | op0: Callable[..., Out0], 932 | op1: Callable[[Out0], Out1], 933 | op2: Callable[[Out1], Out2], 934 | op3: Callable[[Out2], Out3], 935 | op4: Callable[[Out3], Out4], 936 | op5: Callable[[Out4], Out5], 937 | op6: Callable[[Out5], Out6], 938 | op7: Callable[[Out6], Out7], 939 | op8: Callable[[Out7], Out8], 940 | op9: Callable[[Out8], Out9], 941 | op10: Callable[[Out9], Out10], 942 | op11: Callable[[Out10], Out11], 943 | op12: Callable[[Out11], Out12], 944 | op13: Callable[[Out12], Out13], 945 | op14: Callable[[Out13], Out14], 946 | /, 947 | ) -> Callable[..., Out14]: 948 | ... 949 | 950 | 951 | @overload 952 | def pipeline( 953 | op0: Callable[..., Out0], 954 | op1: Callable[[Out0], Out1], 955 | op2: Callable[[Out1], Out2], 956 | op3: Callable[[Out2], Out3], 957 | op4: Callable[[Out3], Out4], 958 | op5: Callable[[Out4], Out5], 959 | op6: Callable[[Out5], Out6], 960 | op7: Callable[[Out6], Out7], 961 | op8: Callable[[Out7], Out8], 962 | op9: Callable[[Out8], Out9], 963 | op10: Callable[[Out9], Out10], 964 | op11: Callable[[Out10], Out11], 965 | op12: Callable[[Out11], Out12], 966 | op13: Callable[[Out12], Out13], 967 | op14: Callable[[Out13], Out14], 968 | op15: Callable[[Out14], Out15], 969 | /, 970 | ) -> Callable[..., Out15]: 971 | ... 972 | 973 | 974 | @overload 975 | def pipeline( 976 | op0: Callable[..., Out0], 977 | op1: Callable[[Out0], Out1], 978 | op2: Callable[[Out1], Out2], 979 | op3: Callable[[Out2], Out3], 980 | op4: Callable[[Out3], Out4], 981 | op5: Callable[[Out4], Out5], 982 | op6: Callable[[Out5], Out6], 983 | op7: Callable[[Out6], Out7], 984 | op8: Callable[[Out7], Out8], 985 | op9: Callable[[Out8], Out9], 986 | op10: Callable[[Out9], Out10], 987 | op11: Callable[[Out10], Out11], 988 | op12: Callable[[Out11], Out12], 989 | op13: Callable[[Out12], Out13], 990 | op14: Callable[[Out13], Out14], 991 | op15: Callable[[Out14], Out15], 992 | op16: Callable[[Out15], Out16], 993 | /, 994 | ) -> Callable[..., Out16]: 995 | ... 996 | 997 | 998 | @overload 999 | def pipeline( 1000 | op0: Callable[..., Out0], 1001 | op1: Callable[[Out0], Out1], 1002 | op2: Callable[[Out1], Out2], 1003 | op3: Callable[[Out2], Out3], 1004 | op4: Callable[[Out3], Out4], 1005 | op5: Callable[[Out4], Out5], 1006 | op6: Callable[[Out5], Out6], 1007 | op7: Callable[[Out6], Out7], 1008 | op8: Callable[[Out7], Out8], 1009 | op9: Callable[[Out8], Out9], 1010 | op10: Callable[[Out9], Out10], 1011 | op11: Callable[[Out10], Out11], 1012 | op12: Callable[[Out11], Out12], 1013 | op13: Callable[[Out12], Out13], 1014 | op14: Callable[[Out13], Out14], 1015 | op15: Callable[[Out14], Out15], 1016 | op16: Callable[[Out15], Out16], 1017 | op17: Callable[[Out16], Out17], 1018 | /, 1019 | ) -> Callable[..., Out17]: 1020 | ... 1021 | 1022 | 1023 | @overload 1024 | def pipeline( 1025 | op0: Callable[..., Out0], 1026 | op1: Callable[[Out0], Out1], 1027 | op2: Callable[[Out1], Out2], 1028 | op3: Callable[[Out2], Out3], 1029 | op4: Callable[[Out3], Out4], 1030 | op5: Callable[[Out4], Out5], 1031 | op6: Callable[[Out5], Out6], 1032 | op7: Callable[[Out6], Out7], 1033 | op8: Callable[[Out7], Out8], 1034 | op9: Callable[[Out8], Out9], 1035 | op10: Callable[[Out9], Out10], 1036 | op11: Callable[[Out10], Out11], 1037 | op12: Callable[[Out11], Out12], 1038 | op13: Callable[[Out12], Out13], 1039 | op14: Callable[[Out13], Out14], 1040 | op15: Callable[[Out14], Out15], 1041 | op16: Callable[[Out15], Out16], 1042 | op17: Callable[[Out16], Out17], 1043 | op18: Callable[[Out17], Out18], 1044 | /, 1045 | ) -> Callable[..., Out18]: 1046 | ... 1047 | 1048 | 1049 | def pipeline(op0: Callable[..., Out0], op1: Union[Callable[[Out0], Out1], None] = None, op2: Union[Callable[[Out1], Out2], None] = None, op3: Union[Callable[[Out2], Out3], None] = None, op4: Union[Callable[[Out3], Out4], None] = None, op5: Union[Callable[[Out4], Out5], None] = None, op6: Union[Callable[[Out5], Out6], None] = None, op7: Union[Callable[[Out6], Out7], None] = None, op8: Union[Callable[[Out7], Out8], None] = None, op9: Union[Callable[[Out8], Out9], None] = None, op10: Union[Callable[[Out9], Out10], None] = None, op11: Union[Callable[[Out10], Out11], None] = None, op12: Union[Callable[[Out11], Out12], None] = None, op13: Union[Callable[[Out12], Out13], None] = None, op14: Union[Callable[[Out13], Out14], None] = None, op15: Union[Callable[[Out14], Out15], None] = None, op16: Union[Callable[[Out15], Out16], None] = None, op17: Union[Callable[[Out16], Out17], None] = None, op18: Union[Callable[[Out17], Out18], None] = None, op19: Union[Callable[[Out18], Out19], None] = None, /) -> Callable[..., Any]: # type: ignore 1050 | """ 1051 | Pipeline takes up to 20 functions and composites them into a single function. 1052 | """ 1053 | 1054 | if not op1: 1055 | 1056 | def _inner0(*args: Any, **kwargs: Any) -> Out0: 1057 | return op0(*args, **kwargs) # fmt: skip 1058 | 1059 | return _inner0 1060 | 1061 | elif not op2: 1062 | 1063 | def _inner1(*args: Any, **kwargs: Any) -> Out1: 1064 | return op1(op0(*args, **kwargs)) # fmt: skip 1065 | 1066 | return _inner1 1067 | 1068 | elif not op3: 1069 | 1070 | def _inner2(*args: Any, **kwargs: Any) -> Out2: 1071 | return op2(op1(op0(*args, **kwargs))) # fmt: skip 1072 | 1073 | return _inner2 1074 | 1075 | elif not op4: 1076 | 1077 | def _inner3(*args: Any, **kwargs: Any) -> Out3: 1078 | return op3(op2(op1(op0(*args, **kwargs)))) # fmt: skip 1079 | 1080 | return _inner3 1081 | 1082 | elif not op5: 1083 | 1084 | def _inner4(*args: Any, **kwargs: Any) -> Out4: 1085 | return op4(op3(op2(op1(op0(*args, **kwargs))))) # fmt: skip 1086 | 1087 | return _inner4 1088 | 1089 | elif not op6: 1090 | 1091 | def _inner5(*args: Any, **kwargs: Any) -> Out5: 1092 | return op5(op4(op3(op2(op1(op0(*args, **kwargs)))))) # fmt: skip 1093 | 1094 | return _inner5 1095 | 1096 | elif not op7: 1097 | 1098 | def _inner6(*args: Any, **kwargs: Any) -> Out6: 1099 | return op6(op5(op4(op3(op2(op1(op0(*args, **kwargs))))))) # fmt: skip 1100 | 1101 | return _inner6 1102 | 1103 | elif not op8: 1104 | 1105 | def _inner7(*args: Any, **kwargs: Any) -> Out7: 1106 | return op7(op6(op5(op4(op3(op2(op1(op0(*args, **kwargs)))))))) # fmt: skip 1107 | 1108 | return _inner7 1109 | 1110 | elif not op9: 1111 | 1112 | def _inner8(*args: Any, **kwargs: Any) -> Out8: 1113 | return op8(op7(op6(op5(op4(op3(op2(op1(op0(*args, **kwargs))))))))) # fmt: skip 1114 | 1115 | return _inner8 1116 | 1117 | elif not op10: 1118 | 1119 | def _inner9(*args: Any, **kwargs: Any) -> Out9: 1120 | return op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(*args, **kwargs)))))))))) # fmt: skip 1121 | 1122 | return _inner9 1123 | 1124 | elif not op11: 1125 | 1126 | def _inner10(*args: Any, **kwargs: Any) -> Out10: 1127 | return op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(*args, **kwargs))))))))))) # fmt: skip 1128 | 1129 | return _inner10 1130 | 1131 | elif not op12: 1132 | 1133 | def _inner11(*args: Any, **kwargs: Any) -> Out11: 1134 | return op11(op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(*args, **kwargs)))))))))))) # fmt: skip 1135 | 1136 | return _inner11 1137 | 1138 | elif not op13: 1139 | 1140 | def _inner12(*args: Any, **kwargs: Any) -> Out12: 1141 | return op12(op11(op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(*args, **kwargs))))))))))))) # fmt: skip 1142 | 1143 | return _inner12 1144 | 1145 | elif not op14: 1146 | 1147 | def _inner13(*args: Any, **kwargs: Any) -> Out13: 1148 | return op13(op12(op11(op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(*args, **kwargs)))))))))))))) # fmt: skip 1149 | 1150 | return _inner13 1151 | 1152 | elif not op15: 1153 | 1154 | def _inner14(*args: Any, **kwargs: Any) -> Out14: 1155 | return op14(op13(op12(op11(op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(*args, **kwargs))))))))))))))) # fmt: skip 1156 | 1157 | return _inner14 1158 | 1159 | elif not op16: 1160 | 1161 | def _inner15(*args: Any, **kwargs: Any) -> Out15: 1162 | return op15(op14(op13(op12(op11(op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(*args, **kwargs)))))))))))))))) # fmt: skip 1163 | 1164 | return _inner15 1165 | 1166 | elif not op17: 1167 | 1168 | def _inner16(*args: Any, **kwargs: Any) -> Out16: 1169 | return op16(op15(op14(op13(op12(op11(op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(*args, **kwargs))))))))))))))))) # fmt: skip 1170 | 1171 | return _inner16 1172 | 1173 | elif not op18: 1174 | 1175 | def _inner17(*args: Any, **kwargs: Any) -> Out17: 1176 | return op17(op16(op15(op14(op13(op12(op11(op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(*args, **kwargs)))))))))))))))))) # fmt: skip 1177 | 1178 | return _inner17 1179 | 1180 | elif not op19: 1181 | 1182 | def _inner18(*args: Any, **kwargs: Any) -> Out18: 1183 | return op18(op17(op16(op15(op14(op13(op12(op11(op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(*args, **kwargs))))))))))))))))))) # fmt: skip 1184 | 1185 | return _inner18 1186 | 1187 | else: 1188 | 1189 | def _inner19(*args: Any, **kwargs: Any) -> Out19: 1190 | return op19(op18(op17(op16(op15(op14(op13(op12(op11(op10(op9(op8(op7(op6(op5(op4(op3(op2(op1(op0(*args, **kwargs)))))))))))))))))))) # fmt: skip 1191 | 1192 | return _inner19 1193 | 1194 | 1195 | def arbitary_length_pipe(value: Any, *funcs: Callable[[Any], Any]) -> Any: 1196 | """ 1197 | Pipe that takes an arbitary amount of functions. 1198 | """ 1199 | for func in funcs: 1200 | value = func(value) 1201 | return value 1202 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajparsons/function-pipes/2141c9e917f127a147c3d4b7e492b3cd76bca62c/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_bridge.py: -------------------------------------------------------------------------------- 1 | from re import A 2 | 3 | from function_pipes import pipe, pipe_bridge 4 | 5 | 6 | def test_bridge(): 7 | """ 8 | test the pipe bridge lets you wrap a function that doesn't return a value 9 | """ 10 | 11 | n = None 12 | 13 | def func(v: int): 14 | nonlocal n 15 | n = v 16 | 17 | v = pipe(1, lambda x: x + 2, pipe_bridge(func), str) 18 | 19 | assert v == "3", "value has passed through" 20 | assert n == 3, "function also received the value" 21 | -------------------------------------------------------------------------------- /tests/test_errors.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from function_pipes import fast_pipes, pipe 3 | 4 | funcs = [str, str, str] 5 | 6 | 7 | def test_starred_error(): 8 | with pytest.raises( 9 | SyntaxError, 10 | match="pipe can't take a starred expression as an argument when fast_pipes is used.", 11 | ): 12 | 13 | @fast_pipes 14 | def test(): 15 | return pipe(1, *funcs) 16 | 17 | t = test() 18 | -------------------------------------------------------------------------------- /tests/test_fast.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from function_pipes import fast_pipes, pipe 4 | 5 | 6 | def add_one(value: Any) -> Any: 7 | """ 8 | Adds one to a value. 9 | """ 10 | return value + 1 11 | 12 | 13 | def times_twelve(value: Any) -> Any: 14 | """ 15 | Multiplies a value by 12. 16 | """ 17 | return value * 12 18 | 19 | 20 | @fast_pipes 21 | def pipe_version(): 22 | return pipe(12, add_one, times_twelve, times_twelve, add_one) 23 | 24 | 25 | def raw_version(): 26 | return add_one(times_twelve(times_twelve(add_one(12)))) 27 | 28 | 29 | def test_pipe_equiv(): 30 | assert ( 31 | pipe_version() == raw_version() 32 | ), "fast_pipe version is not equiv to basic version" 33 | 34 | 35 | @fast_pipes 36 | def pipe_version_with_lambda(): 37 | return pipe(12, add_one, times_twelve, times_twelve, add_one, lambda x: x / 2) 38 | 39 | 40 | def raw_version_with_function(): 41 | return add_one(times_twelve(times_twelve(add_one(12)))) / 2 42 | 43 | 44 | def test_pipe_lambda_equiv(): 45 | assert ( 46 | pipe_version() == raw_version() 47 | ), "fast_pipe version is not equivalent to raw version when lambda is used" 48 | 49 | 50 | def pipe_version_with_lambda_used_more_than_once(): 51 | return pipe(12, add_one, times_twelve, times_twelve, add_one, lambda x: x + x + 2) 52 | 53 | 54 | def raw_version_with_function_used_more_than_once(): 55 | v = add_one(times_twelve(times_twelve(add_one(12)))) 56 | return v + v + 2 57 | 58 | 59 | def test_pipe_lambda_equiv_multiple(): 60 | assert ( 61 | pipe_version_with_lambda_used_more_than_once() 62 | == raw_version_with_function_used_more_than_once() 63 | ), "fast_pipe version is not equivalent to raw version when lambda value is used multiple times" 64 | -------------------------------------------------------------------------------- /tests/test_functional_equiv.py: -------------------------------------------------------------------------------- 1 | import random 2 | from typing import Any, Callable 3 | 4 | from function_pipes import pipe 5 | 6 | pipe_allowed_size = 20 7 | 8 | 9 | def basic_pipe(value: Any, *funcs: Callable[[Any], Any]) -> Any: 10 | """ 11 | A basic pipe function. 12 | """ 13 | for func in funcs: 14 | value = func(value) 15 | return value 16 | 17 | 18 | def add_one(value: Any) -> Any: 19 | """ 20 | Adds one to a value. 21 | """ 22 | return value + 1 23 | 24 | 25 | def times_ten(value: Any) -> Any: 26 | """ 27 | Multiplies a value by ten. 28 | """ 29 | return value * 10 30 | 31 | 32 | def divide_by_two(value: Any) -> Any: 33 | """ 34 | Divides a value by two. 35 | """ 36 | return value / 2 37 | 38 | 39 | def test_basic_pipe(): 40 | """ 41 | Test basic pipe function. 42 | """ 43 | assert basic_pipe(1, add_one, times_ten, divide_by_two) == 10 44 | assert basic_pipe(1, add_one, times_ten) == 20 45 | assert basic_pipe(1, add_one) == 2 46 | assert basic_pipe(1) == 1 47 | 48 | 49 | def test_equiv_function(): 50 | """ 51 | test the pipe function works the same as the basic_pipe function 52 | """ 53 | # list of functions to apply 54 | funcs = [add_one, times_ten, divide_by_two] 55 | 56 | def get_random_function_from_funcs(): 57 | """ 58 | Get a random function from the list of functions. 59 | """ 60 | return random.choice(funcs) 61 | 62 | for n in range(1, pipe_allowed_size): 63 | # a list n long of random functions 64 | random_funcs = [get_random_function_from_funcs() for _ in range(n)] 65 | # get a random number 66 | random_number = random.randint(0, 100) 67 | # apply the functions to the number 68 | result = pipe(random_number, *random_funcs) 69 | # apply the functions to the number 70 | result_basic = basic_pipe(random_number, *random_funcs) 71 | # assert the results are the same 72 | assert result == result_basic, f"mismatch for {n}" 73 | -------------------------------------------------------------------------------- /tests/test_meta.py: -------------------------------------------------------------------------------- 1 | """ 2 | Run meta tests on package (apply to muliple packages) 3 | 4 | """ 5 | from pathlib import Path 6 | import function_pipes as package 7 | import toml 8 | 9 | 10 | def test_version_in_workflow(): 11 | """ 12 | Check if the current version is mentioned in the changelog 13 | """ 14 | package_init_version = package.__version__ 15 | path = Path(__file__).resolve().parents[1] / "CHANGELOG.md" 16 | change_log = path.read_text() 17 | format = f"## [{package_init_version}]" 18 | assert format in change_log 19 | 20 | 21 | def test_versions_are_in_sync(): 22 | """Checks if the pyproject.toml and package.__init__.py __version__ are in sync.""" 23 | 24 | path = Path(__file__).resolve().parents[1] / "pyproject.toml" 25 | pyproject = toml.loads(open(str(path), encoding="utf-8").read()) 26 | pyproject_version = pyproject["tool"]["poetry"]["version"] 27 | 28 | package_init_version = package.__version__ 29 | 30 | assert package_init_version == pyproject_version 31 | -------------------------------------------------------------------------------- /tests/test_speed.py: -------------------------------------------------------------------------------- 1 | """ 2 | Run meta tests on package (apply to muliple packages) 3 | 4 | """ 5 | import timeit 6 | from typing import Any, Callable, Union 7 | 8 | from function_pipes import arbitary_length_pipe, fast_pipes, pipe 9 | 10 | stand_in_callable = Union[Callable[..., Any], None] 11 | 12 | 13 | def add_one(value: Any) -> Any: 14 | """ 15 | Adds one to a value. 16 | """ 17 | return value + 1 18 | 19 | 20 | def times_12(value: Any) -> Any: 21 | """ 22 | Multiplies a value by 12. 23 | """ 24 | return value * 12 25 | 26 | 27 | def func_raw(): 28 | return add_one(times_12(times_12(add_one(12)))) 29 | 30 | 31 | def func_basic(): 32 | return arbitary_length_pipe(12, add_one, times_12, times_12, add_one) 33 | 34 | 35 | def func_pipe(): 36 | return pipe(12, add_one, times_12, times_12, add_one) 37 | 38 | 39 | @fast_pipes 40 | def func_fast(): 41 | return pipe(12, add_one, times_12, times_12, add_one) 42 | 43 | 44 | def get_speed(func: Callable[..., Any]): 45 | return timeit.repeat( 46 | func, 47 | number=100000, 48 | repeat=100, 49 | ) 50 | 51 | 52 | def test_speed(): 53 | raw_result = min(get_speed(func_raw)) 54 | fast_result = min(get_speed(func_fast)) 55 | pipe_result = min(get_speed(func_pipe)) 56 | basic_result = min(get_speed(func_basic)) 57 | 58 | assert ( 59 | raw_result < pipe_result 60 | ), "Raw should be faster than pipe - basic sense check" 61 | assert fast_result < pipe_result, "Fast should be faster than pipe" 62 | assert ( 63 | pipe_result < basic_result 64 | ), "Pipe should be faster than the simple implementation" 65 | -------------------------------------------------------------------------------- /typesafety/pipetype.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | 4 | def reveal_type(x: Any): 5 | return x 6 | 7 | 8 | from function_pipes import pipe 9 | 10 | 11 | def test(): 12 | p = pipe(1, lambda x: x + 2, str) 13 | reveal_type(p) # T: str 14 | 15 | 16 | def test_lambda_safe_type(): 17 | p = pipe(1, lambda x: x + 2) 18 | reveal_type(p) # T: int 19 | 20 | 21 | # fmt: off 22 | 23 | def test_lambda_error(): 24 | v = str() 25 | pipe( v, lambda x: x + 2) # E: Operator "+" not supported for types "str" and "Literal[2]" 26 | # fmt: on 27 | -------------------------------------------------------------------------------- /typesafety/quickstart.py: -------------------------------------------------------------------------------- 1 | from typing import TypedDict 2 | 3 | 4 | class Data(TypedDict, total=False): 5 | points: int 6 | 7 | 8 | def data_handler(data: Data) -> None: 9 | points = data.get("points") 10 | reveal_type(points) # T: int | None 11 | 12 | points = data.get("points", 0) 13 | reveal_type(points) # T: int 14 | 15 | # as we specified total=False in the Data type, the points key might be missing 16 | print(data["points"]) # E: Could not access item in TypedDict 17 | --------------------------------------------------------------------------------