├── .github └── workflows │ ├── publish_docs.yml │ └── tests.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── build_scripts └── generate_readme.py ├── changelog.rst ├── coverage.svg ├── docs ├── Makefile ├── requirements.txt └── source │ ├── _static │ ├── favico.png │ ├── headerbg.png │ ├── logo.png │ ├── logox.png │ └── override.css │ ├── changelog.rst │ ├── conf.py │ ├── decorators.rst │ ├── index.rst │ ├── maybe.rst │ ├── overview.rst │ ├── pipeutils.rst │ ├── xobject.rst │ └── xpartial.rst ├── pipetools ├── __init__.py ├── compat.py ├── debug.py ├── decorators.py ├── ds_builder.py ├── main.py └── utils.py ├── setup.py └── test_pipetools ├── __init__.py ├── test_decorators.py ├── test_ds_builder.py ├── test_main.py └── test_utils.py /.github/workflows/publish_docs.yml: -------------------------------------------------------------------------------- 1 | name: Publish docs 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | 9 | build-and-publish-docs: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Set up Python 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: '3.x' 19 | 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install -r docs/requirements.txt 24 | python setup.py develop 25 | 26 | - name: Build Sphinx documentation 27 | run: | 28 | sphinx-build -b html docs/source/ new-doc 29 | 30 | - name: Checkout gh-pages 31 | run: | 32 | git fetch origin gh-pages 33 | git checkout gh-pages 34 | 35 | - name: Update docs 36 | run: | 37 | git rm -rf doc 38 | mv new-doc doc 39 | 40 | - name: Commit new docs 41 | uses: EndBug/add-and-commit@v7 42 | with: 43 | default_author: github_actions 44 | branch: gh-pages 45 | message: 'doc update' 46 | add: 'doc' 47 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | run-tests: 12 | 13 | runs-on: ubuntu-latest 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | python-version: [3.7, 3.8, '3.x'] 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v2 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | python -m pip install flake8 pytest pytest-cov 31 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 32 | 33 | - name: Lint with flake8 34 | run: | 35 | # stop the build if there are Python syntax errors or undefined names 36 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 37 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 38 | flake8 . --count --exit-zero --max-complexity=6 --max-line-length=127 --statistics 39 | 40 | - name: Test with pytest 41 | run: | 42 | pytest --cov=pipetools 43 | 44 | build-docs: 45 | runs-on: ubuntu-latest 46 | 47 | steps: 48 | - uses: actions/checkout@v2 49 | - name: Set up Python 50 | uses: actions/setup-python@v2 51 | with: 52 | python-version: '3.x' 53 | 54 | - name: Install dependencies 55 | run: | 56 | python -m pip install --upgrade pip 57 | pip install -r docs/requirements.txt 58 | python setup.py develop 59 | 60 | - name: Test with pytest 61 | run: | 62 | pytest --cov=pipetools 63 | 64 | - name: Create coverage badge 65 | run: | 66 | coverage-badge -f -o coverage.svg 67 | 68 | - name: Update README 69 | run: | 70 | python build_scripts/generate_readme.py 71 | 72 | - name: Commit badge and README 73 | uses: EndBug/add-and-commit@v7 74 | with: 75 | default_author: github_actions 76 | message: '(README update)' 77 | add: '["coverage.svg", "README.rst"]' 78 | 79 | - name: Build Sphinx documentation 80 | run: | 81 | sphinx-build -b html docs/source/ doc 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pipetools.egg-info 2 | docs/build 3 | *.pyc 4 | .eggs 5 | .vscode 6 | .coverage -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012 0101 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include MANIFEST.in 2 | include README.rst 3 | 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | 2 | Pipetools 3 | ========= 4 | 5 | |tests-badge| |coverage-badge| |pypi-badge| 6 | 7 | .. |tests-badge| image:: https://github.com/0101/pipetools/actions/workflows/tests.yml/badge.svg 8 | :target: https://github.com/0101/pipetools/actions/workflows/tests.yml 9 | 10 | .. |coverage-badge| image:: https://raw.githubusercontent.com/0101/pipetools/master/coverage.svg 11 | :target: https://github.com/0101/pipetools/actions/workflows/tests.yml 12 | 13 | .. |pypi-badge| image:: https://img.shields.io/pypi/dm/pipetools.svg 14 | :target: https://pypi.org/project/pipetools/ 15 | 16 | `Complete documentation `_ 17 | 18 | ``pipetools`` enables function composition similar to using Unix pipes. 19 | 20 | It allows forward-composition and piping of arbitrary functions - no need to decorate them or do anything extra. 21 | 22 | It also packs a bunch of utils that make common operations more convenient and readable. 23 | 24 | Source is on github_. 25 | 26 | .. _github: https://github.com/0101/pipetools 27 | 28 | Why? 29 | ---- 30 | 31 | Piping and function composition are some of the most natural operations there are for 32 | plenty of programming tasks. Yet Python doesn't have a built-in way of performing them. 33 | That forces you to either deep nesting of function calls or adding extra **glue code**. 34 | 35 | 36 | Example 37 | ------- 38 | 39 | Say you want to create a list of python files in a given directory, ordered by 40 | filename length, as a string, each file on one line and also with line numbers: 41 | 42 | .. code-block:: pycon 43 | 44 | >>> print(pyfiles_by_length('../pipetools')) 45 | 1. ds_builder.py 46 | 2. __init__.py 47 | 3. compat.py 48 | 4. utils.py 49 | 5. main.py 50 | 51 | All the ingredients are already there, you just have to glue them together. You might write it like this: 52 | 53 | .. code-block:: python 54 | 55 | def pyfiles_by_length(directory): 56 | all_files = os.listdir(directory) 57 | py_files = [f for f in all_files if f.endswith('.py')] 58 | sorted_files = sorted(py_files, key=len, reverse=True) 59 | numbered = enumerate(py_files, 1) 60 | rows = ("{0}. {1}".format(i, f) for i, f in numbered) 61 | return '\n'.join(rows) 62 | 63 | Or perhaps like this: 64 | 65 | .. code-block:: python 66 | 67 | def pyfiles_by_length(directory): 68 | return '\n'.join('{0}. {1}'.format(*x) for x in enumerate(reversed(sorted( 69 | [f for f in os.listdir(directory) if f.endswith('.py')], key=len)), 1)) 70 | 71 | Or, if you're a mad scientist, you would probably do it like this: 72 | 73 | .. code-block:: python 74 | 75 | pyfiles_by_length = lambda d: (reduce('{0}\n{1}'.format, 76 | map(lambda x: '%d. %s' % x, enumerate(reversed(sorted( 77 | filter(lambda f: f.endswith('.py'), os.listdir(d)), key=len)))))) 78 | 79 | 80 | But *there should be one -- and preferably only one -- obvious way to do it*. 81 | 82 | So which one is it? Well, to redeem the situation, ``pipetools`` give you yet 83 | another possibility! 84 | 85 | .. code-block:: python 86 | 87 | pyfiles_by_length = (pipe 88 | | os.listdir 89 | | where(X.endswith('.py')) 90 | | sort_by(len).descending 91 | | (enumerate, X, 1) 92 | | foreach("{0}. {1}") 93 | | '\n'.join) 94 | 95 | *Why would I do that*, you ask? Comparing to the *native* Python code, it's 96 | 97 | - **Easier to read** -- minimal extra clutter 98 | - **Easier to understand** -- one-way data flow from one step to the next, nothing else to keep track of 99 | - **Easier to change** -- want more processing? just add a step to the pipeline 100 | - **Removes some bug opportunities** -- did you spot the bug in the first example? 101 | 102 | Of course it won't solve all your problems, but a great deal of code *can* 103 | be expressed as a pipeline, giving you the above benefits. Read on to see how it works! 104 | 105 | 106 | Installation 107 | ------------ 108 | 109 | .. code-block:: console 110 | 111 | $ pip install pipetools 112 | 113 | `Uh, what's that? `_ 114 | 115 | 116 | Usage 117 | ----- 118 | 119 | .. _the-pipe: 120 | 121 | The pipe 122 | """""""" 123 | The ``pipe`` object can be used to pipe functions together to 124 | form new functions, and it works like this: 125 | 126 | .. code-block:: python 127 | 128 | from pipetools import pipe 129 | 130 | f = pipe | a | b | c 131 | 132 | # is the same as: 133 | def f(x): 134 | return c(b(a(x))) 135 | 136 | 137 | A real example, sum of odd numbers from 0 to *x*: 138 | 139 | .. code-block:: python 140 | 141 | from functools import partial 142 | from pipetools import pipe 143 | 144 | odd_sum = pipe | range | partial(filter, lambda x: x % 2) | sum 145 | 146 | odd_sum(10) # -> 25 147 | 148 | 149 | Note that the chain up to the `sum` is lazy. 150 | 151 | 152 | Automatic partial application in the pipe 153 | """"""""""""""""""""""""""""""""""""""""" 154 | 155 | As partial application is often useful when piping things together, it is done 156 | automatically when the *pipe* encounters a tuple, so this produces the same 157 | result as the previous example: 158 | 159 | .. code-block:: python 160 | 161 | odd_sum = pipe | range | (filter, lambda x: x % 2) | sum 162 | 163 | As of ``0.1.9``, this is even more powerful, see `X-partial `_. 164 | 165 | 166 | Built-in tools 167 | """""""""""""" 168 | 169 | Pipetools contain a set of *pipe-utils* that solve some common tasks. For 170 | example there is a shortcut for the filter class from our example, called 171 | `where() `_: 172 | 173 | .. code-block:: python 174 | 175 | from pipetools import pipe, where 176 | 177 | odd_sum = pipe | range | where(lambda x: x % 2) | sum 178 | 179 | Well that might be a bit more readable, but not really a huge improvement, but 180 | wait! 181 | 182 | If a *pipe-util* is used as first or second item in the pipe (which happens 183 | quite often) the ``pipe`` at the beginning can be omitted: 184 | 185 | .. code-block:: python 186 | 187 | odd_sum = range | where(lambda x: x % 2) | sum 188 | 189 | 190 | See `pipe-utils' documentation `_. 191 | 192 | 193 | OK, but what about the ugly lambda? 194 | """"""""""""""""""""""""""""""""""" 195 | 196 | `where() `_, but also `foreach() `_, 197 | `sort_by() `_ and other `pipe-utils `_ can be 198 | quite useful, but require a function as an argument, which can either be a named 199 | function -- which is OK if it does something complicated -- but often it's 200 | something simple, so it's appropriate to use a ``lambda``. Except Python's 201 | lambdas are quite verbose for simple tasks and the code gets cluttered... 202 | 203 | **X object** to the rescue! 204 | 205 | .. code-block:: python 206 | 207 | from pipetools import where, X 208 | 209 | odd_sum = range | where(X % 2) | sum 210 | 211 | 212 | How 'bout that. 213 | 214 | `Read more about the X object and it's limitations. `_ 215 | 216 | 217 | .. _auto-string-formatting: 218 | 219 | Automatic string formatting 220 | """"""""""""""""""""""""""" 221 | 222 | Since it doesn't make sense to compose functions with strings, when a pipe (or a 223 | `pipe-util `_) encounters a string, it attempts to use it for 224 | `(advanced) formatting`_: 225 | 226 | .. code-block:: pycon 227 | 228 | >>> countdown = pipe | (range, 1) | reversed | foreach('{}...') | ' '.join | '{} boom' 229 | >>> countdown(5) 230 | '4... 3... 2... 1... boom' 231 | 232 | .. _(advanced) formatting: http://docs.python.org/library/string.html#formatstrings 233 | 234 | 235 | Feeding the pipe 236 | """""""""""""""" 237 | 238 | Sometimes it's useful to create a one-off pipe and immediately run some input 239 | through it. And since this is somewhat awkward (and not very readable, 240 | especially when the pipe spans multiple lines): 241 | 242 | .. code-block:: python 243 | 244 | result = (pipe | foo | bar | boo)(some_input) 245 | 246 | It can also be done using the ``>`` operator: 247 | 248 | .. code-block:: python 249 | 250 | result = some_input > pipe | foo | bar | boo 251 | 252 | .. note:: 253 | Note that the above method of input won't work if the input object 254 | defines `__gt__ `_ 255 | for *any* object - including the pipe. This can be the case for example with 256 | some objects from math libraries such as NumPy. If you experience strange 257 | results try falling back to the standard way of passing input into a pipe. 258 | 259 | 260 | But wait, there is more 261 | ----------------------- 262 | Checkout `the Maybe pipe `_, `partial application on steroids `_ 263 | or `automatic data structure creation `_ 264 | in the `full documentation `_. 265 | -------------------------------------------------------------------------------- /build_scripts/generate_readme.py: -------------------------------------------------------------------------------- 1 | """ 2 | A script for generating a README file from docs/overview page 3 | """ 4 | 5 | import codecs 6 | import re 7 | 8 | from pipetools import foreach, X, pipe 9 | 10 | 11 | DOC_ROOT = 'https://0101.github.io/pipetools/doc/' 12 | 13 | 14 | readme_template = """ 15 | Pipetools 16 | ========= 17 | 18 | |tests-badge| |coverage-badge| |pypi-badge| 19 | 20 | .. |tests-badge| image:: https://github.com/0101/pipetools/actions/workflows/tests.yml/badge.svg 21 | :target: https://github.com/0101/pipetools/actions/workflows/tests.yml 22 | 23 | .. |coverage-badge| image:: https://raw.githubusercontent.com/0101/pipetools/master/coverage.svg 24 | :target: https://github.com/0101/pipetools/actions/workflows/tests.yml 25 | 26 | .. |pypi-badge| image:: https://img.shields.io/pypi/dm/pipetools.svg 27 | :target: https://pypi.org/project/pipetools/ 28 | 29 | `Complete documentation <{0}>`_ 30 | 31 | {{0}} 32 | 33 | But wait, there is more 34 | ----------------------- 35 | Checkout `the Maybe pipe <{0}maybe>`_, `partial application on steroids <{0}xpartial>`_ 36 | or `automatic data structure creation <{0}pipeutils#automatic-data-structure-creation>`_ 37 | in the `full documentation <{0}#contents>`_. 38 | """.format(DOC_ROOT) 39 | 40 | 41 | link_template = u"`{text} <%s{url}>`_" % DOC_ROOT 42 | 43 | 44 | link_replacements = ( 45 | # :doc:`pipe-utils' documentation`. 46 | (r":doc:`([^<]*)<([^>]*)>`", {'url': r'\2.html', 'text': r'\1'}), 47 | 48 | # :func:`~pipetools.utils.where` 49 | (r":func:`~pipetools\.utils\.([^`]*)`", 50 | {'url': r'pipeutils.html#pipetools.utils.\1', 'text': r'\1()'}), 51 | 52 | ) > foreach([X[0] | re.compile, X[1] | link_template]) 53 | 54 | 55 | def create_readme(): 56 | with codecs.open('docs/source/overview.rst', 'r', 'utf-8') as overview: 57 | with codecs.open('README.rst', 'w+', 'utf-8') as readme: 58 | overview.read() > pipe | fix_links | readme_template | readme.write 59 | 60 | 61 | def fix_links(string): 62 | for pattern, replacement in link_replacements: 63 | string = pattern.sub(replacement, string) 64 | return string 65 | 66 | 67 | if __name__ == '__main__': 68 | 69 | create_readme() 70 | -------------------------------------------------------------------------------- /changelog.rst: -------------------------------------------------------------------------------- 1 | 2 | 1.1.0 3 | ---------- 4 | 2021-11-26 5 | 6 | * Added implementation for most of the magic methods on X object 7 | 8 | 9 | 1.0.1 10 | ---------- 11 | 2021-06-29 12 | 13 | * Fixed coverage badge in PyPI description 14 | 15 | 16 | 1.0.0 17 | ---------- 18 | 2021-06-29 19 | 20 | * Package classified as stable 21 | 22 | 23 | 0.3.6 24 | ---------- 25 | 2020-10-21 26 | 27 | * Fix import for Python >= 3.9 (#16) 28 | 29 | 30 | 0.3.5 31 | ---------- 32 | 2019-12-01 33 | 34 | * Fixed X object division in Python 3 35 | 36 | 37 | 0.3.4 38 | ---------- 39 | 2018-06-13 40 | 41 | * Fixed UnicodeDecodeError during installation 42 | 43 | 44 | 0.3.3 45 | ---------- 46 | 2018-05-30 47 | 48 | * Added take_until.including 49 | 50 | 51 | 0.3.2 52 | ---------- 53 | 2018-05-26 54 | 55 | * No crash in Python 2 when partially applying a non-standard callable 56 | * Regex conditions ignore None instead of throwing an exception 57 | * maybe can be inserted in the middle of a pipe without parentheses 58 | 59 | 60 | 0.3.1 61 | ---------- 62 | 2018-03-23 63 | 64 | * Added tee util 65 | * flatten will leave dictionaries (Mappings) alone 66 | 67 | 68 | 0.3.0 69 | ---------- 70 | 2016-08-03 71 | 72 | * added Python 3 support 73 | 74 | 75 | 0.2.7 76 | ---------- 77 | 2014-03-19 78 | 79 | * fixed checking if objects are iterable 80 | 81 | 82 | 0.2.6 83 | ---------- 84 | 2013-09-02 85 | 86 | * removed pipe_exception_handler (did more harm than good most of the time) 87 | 88 | 89 | 0.2.5 90 | ---------- 91 | 2013-08-13 92 | 93 | * Maybe returns None when called with None 94 | * not calling repr() on stuff if we don't need to 95 | 96 | 97 | 0.2.4 98 | ---------- 99 | 2013-07-13 100 | 101 | * added drop_first 102 | * fixed unicode formatting problem 103 | 104 | 105 | 0.2.3 106 | ---------- 107 | 2013-04-24 108 | 109 | * added sort_by.descending 110 | * group_by returns item iterator instead of a dictionary 111 | 112 | 113 | 0.2.2 114 | ---------- 115 | 2013-04-16 116 | 117 | * X objects create bound methods on classes 118 | * added support for X division 119 | 120 | 121 | 0.2.1 122 | ---------- 123 | 2013-02-10 124 | 125 | * added automatic regex conditions 126 | * renamed xcurry to xpartial (turns out currying is something else) 127 | 128 | 129 | 0.2.0 130 | ---------- 131 | 2012-11-14 132 | 133 | * added support for X >=, <=, - and ** operations 134 | * fixed static item handling in ds_builder 135 | 136 | 137 | 0.1.9 138 | ---------- 139 | 2012-11-05 140 | 141 | * added xcurry 142 | * improved XObject naming 143 | 144 | 145 | 0.1.8 146 | ---------- 147 | 2012-10-31 148 | 149 | * added as_kwargs 150 | * added take_until 151 | * X object implicit piping (without ~) 152 | * fixed naming X-objects so it doesn't fail with tuples 153 | 154 | 0.1.7 155 | ---------- 156 | 2012-10-25 157 | 158 | * friendlier debugging 159 | * added changelog 160 | -------------------------------------------------------------------------------- /coverage.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | coverage 17 | coverage 18 | 93% 19 | 93% 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pipetools.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pipetools.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/pipetools" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pipetools" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx==1.7.4 2 | jinja2<3.1.0 3 | coverage-badge==1.1.0 4 | pytest==6.2.4 5 | pytest-cov==2.12.1 6 | -------------------------------------------------------------------------------- /docs/source/_static/favico.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0101/pipetools/6cba9fadab07a16fd85eed16d5cffc609f84c62b/docs/source/_static/favico.png -------------------------------------------------------------------------------- /docs/source/_static/headerbg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0101/pipetools/6cba9fadab07a16fd85eed16d5cffc609f84c62b/docs/source/_static/headerbg.png -------------------------------------------------------------------------------- /docs/source/_static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0101/pipetools/6cba9fadab07a16fd85eed16d5cffc609f84c62b/docs/source/_static/logo.png -------------------------------------------------------------------------------- /docs/source/_static/logox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0101/pipetools/6cba9fadab07a16fd85eed16d5cffc609f84c62b/docs/source/_static/logox.png -------------------------------------------------------------------------------- /docs/source/_static/override.css: -------------------------------------------------------------------------------- 1 | @import url('pyramid.css'); 2 | 3 | body {font-family: "Segoe UI", sans-serif;} 4 | 5 | 6 | div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6, 7 | div.sphinxsidebar h3, div.sphinxsidebar h4 { 8 | font-family: "Segoe UI", "Droid Sans", sans-serif;} 9 | 10 | div.body .section {max-width: 840px;} 11 | 12 | em.property {font-style: normal; color: #8D6445;} 13 | dt em, dd cite, cite {color:#329D29; font-style: normal;} 14 | /*p em {color: #6D581E;}*/ 15 | dd { 16 | background: #F7F7F7; 17 | background: rgba(100, 100, 100, .05); 18 | border-radius: 8px; 19 | padding: 2px 10px 1px; 20 | margin-left: 0; 21 | margin-bottom: 30px; 22 | 23 | } 24 | h2 {border-bottom: 2px solid #D2D2D2;} 25 | div.header {background: #19162f url('headerbg.png') repeat-x top;} 26 | div.logo {padding: 0;} 27 | div.logo a {display: inline-block;} 28 | div.body h3 {font-weight:bold; margin-bottom: 0; padding-bottom: 0;} 29 | 30 | p {margin-top: 3px;} 31 | div.body ul, div.body ol {margin-bottom: 30px;} 32 | 33 | tt, pre {font-family: Consolas, monospace; font-size: 1em;} 34 | 35 | tt.docutils.literal {background-color: rgba(0, 0, 0, 0.07); padding: 1px 4px; border-radius:4px;} 36 | a tt.docutils.literal, 37 | h1 tt.docutils.literal, 38 | h2 tt.docutils.literal, 39 | h3 tt.docutils.literal {background-color: transparent;} 40 | 41 | a.internal.reference em {color: #1B61D6;} 42 | 43 | .sphinxsidebarwrapper {overflow: hidden;} 44 | 45 | div.highlight {background-color: transparent;} 46 | 47 | .highlight pre, .highlight-python pre { 48 | background-color: #FFFFFF; 49 | background-color: #222222; 50 | color: #e5e5e5; 51 | border: none; 52 | /*box-shadow: 2px 3px 6px rgba(0, 0, 0, 0.3);*/ 53 | border-radius: 5px; 54 | } 55 | -------------------------------------------------------------------------------- /docs/source/changelog.rst: -------------------------------------------------------------------------------- 1 | Change log 2 | ========== 3 | 4 | .. include:: ../../changelog.rst 5 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # pipetools documentation build configuration file, created by 4 | # sphinx-quickstart on Sun Sep 30 12:47:08 2012. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import pipetools 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = ['sphinx.ext.autodoc'] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'pipetools' 44 | copyright = u'2012, 0101' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The full version, including alpha/beta/rc tags. 51 | release = pipetools.__versionstr__ 52 | # The short X.Y version. 53 | version = release[:-2] 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = [] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'monokai' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | 90 | # -- Options for HTML output --------------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. See the documentation for 93 | # a list of builtin themes. 94 | html_theme = 'pyramid' 95 | 96 | html_style = 'override.css' 97 | 98 | # Theme options are theme-specific and customize the look and feel of a theme 99 | # further. For a list of options available for each theme, see the 100 | # documentation. 101 | #html_theme_options = {} 102 | 103 | # Add any paths that contain custom themes here, relative to this directory. 104 | #html_theme_path = [] 105 | 106 | # The name for this set of Sphinx documents. If None, it defaults to 107 | # " v documentation". 108 | #html_title = None 109 | 110 | # A shorter title for the navigation bar. Default is the same as html_title. 111 | #html_short_title = None 112 | 113 | # The name of an image file (relative to this directory) to place at the top 114 | # of the sidebar. 115 | html_logo = 'source/_static/logo.png' 116 | 117 | # The name of an image file (within the static path) to use as favicon of the 118 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 119 | # pixels large. 120 | html_favicon = 'source/_static/favico.png' 121 | 122 | # Add any paths that contain custom static files (such as style sheets) here, 123 | # relative to this directory. They are copied after the builtin static files, 124 | # so a file named "default.css" will overwrite the builtin "default.css". 125 | html_static_path = ['_static'] 126 | 127 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 128 | # using the given strftime format. 129 | #html_last_updated_fmt = '%b %d, %Y' 130 | 131 | # If true, SmartyPants will be used to convert quotes and dashes to 132 | # typographically correct entities. 133 | #html_use_smartypants = True 134 | 135 | # Custom sidebar templates, maps document names to template names. 136 | #html_sidebars = {} 137 | 138 | # Additional templates that should be rendered to pages, maps page names to 139 | # template names. 140 | #html_additional_pages = {} 141 | 142 | # If false, no module index is generated. 143 | #html_domain_indices = True 144 | 145 | # If false, no index is generated. 146 | #html_use_index = True 147 | 148 | # If true, the index is split into individual pages for each letter. 149 | #html_split_index = False 150 | 151 | # If true, links to the reST sources are added to the pages. 152 | #html_show_sourcelink = True 153 | 154 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 155 | #html_show_sphinx = True 156 | 157 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 158 | #html_show_copyright = True 159 | 160 | # If true, an OpenSearch description file will be output, and all pages will 161 | # contain a tag referring to it. The value of this option must be the 162 | # base URL from which the finished HTML is served. 163 | #html_use_opensearch = '' 164 | 165 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 166 | #html_file_suffix = None 167 | 168 | # Output file base name for HTML help builder. 169 | htmlhelp_basename = 'pipetoolsdoc' 170 | 171 | 172 | # -- Options for LaTeX output -------------------------------------------------- 173 | 174 | latex_elements = { 175 | # The paper size ('letterpaper' or 'a4paper'). 176 | #'papersize': 'letterpaper', 177 | 178 | # The font size ('10pt', '11pt' or '12pt'). 179 | #'pointsize': '10pt', 180 | 181 | # Additional stuff for the LaTeX preamble. 182 | #'preamble': '', 183 | } 184 | 185 | # Grouping the document tree into LaTeX files. List of tuples 186 | # (source start file, target name, title, author, documentclass [howto/manual]). 187 | latex_documents = [ 188 | ('index', 'pipetools.tex', u'pipetools Documentation', 189 | u'0101', 'manual'), 190 | ] 191 | 192 | # The name of an image file (relative to this directory) to place at the top of 193 | # the title page. 194 | #latex_logo = None 195 | 196 | # For "manual" documents, if this is true, then toplevel headings are parts, 197 | # not chapters. 198 | #latex_use_parts = False 199 | 200 | # If true, show page references after internal links. 201 | #latex_show_pagerefs = False 202 | 203 | # If true, show URL addresses after external links. 204 | #latex_show_urls = False 205 | 206 | # Documents to append as an appendix to all manuals. 207 | #latex_appendices = [] 208 | 209 | # If false, no module index is generated. 210 | #latex_domain_indices = True 211 | 212 | 213 | # -- Options for manual page output -------------------------------------------- 214 | 215 | # One entry per manual page. List of tuples 216 | # (source start file, name, description, authors, manual section). 217 | man_pages = [ 218 | ('index', 'pipetools', u'pipetools Documentation', 219 | [u'0101'], 1) 220 | ] 221 | 222 | # If true, show URL addresses after external links. 223 | #man_show_urls = False 224 | 225 | 226 | # -- Options for Texinfo output ------------------------------------------------ 227 | 228 | # Grouping the document tree into Texinfo files. List of tuples 229 | # (source start file, target name, title, author, 230 | # dir menu entry, description, category) 231 | texinfo_documents = [ 232 | ('index', 'pipetools', u'pipetools Documentation', 233 | u'0101', 'pipetools', 'One line description of project.', 234 | 'Miscellaneous'), 235 | ] 236 | 237 | # Documents to append as an appendix to all manuals. 238 | #texinfo_appendices = [] 239 | 240 | # If false, no module index is generated. 241 | #texinfo_domain_indices = True 242 | 243 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 244 | #texinfo_show_urls = 'footnote' 245 | -------------------------------------------------------------------------------- /docs/source/decorators.rst: -------------------------------------------------------------------------------- 1 | :mod:`decorators` module 2 | ======================== 3 | 4 | This module contains decorators that handle some common tasks for :doc:`pipeutils` 5 | 6 | .. automodule:: pipetools.decorators 7 | :members: 8 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to pipetools' documentation! 2 | ==================================== 3 | 4 | .. include:: overview.rst 5 | 6 | 7 | .. _contents: 8 | 9 | But wait, there is more 10 | ----------------------- 11 | 12 | .. toctree:: 13 | :maxdepth: 2 14 | 15 | pipeutils 16 | xobject 17 | xpartial 18 | maybe 19 | decorators 20 | changelog 21 | 22 | 23 | 24 | Indices and tables 25 | ================== 26 | 27 | * :ref:`genindex` 28 | * :ref:`modindex` 29 | * :ref:`search` 30 | 31 | -------------------------------------------------------------------------------- /docs/source/maybe.rst: -------------------------------------------------------------------------------- 1 | The Maybe Pipe 2 | ============== 3 | 4 | ``maybe`` is just like :ref:`pipe `, except if any of the piped functions 5 | returns ``None`` the execution is stopped and ``None`` is returned immediately. 6 | 7 | Example:: 8 | 9 | >>> from pipetools import maybe, X 10 | >>> f = maybe | (re.match, r'^number-(\d+)$') | X.group(1) | int 11 | >>> f('number-7') 12 | 7 13 | >>> f('something else') 14 | None 15 | 16 | 17 | Can be used together with :func:`~pipetools.utils.unless` to great effect:: 18 | 19 | content = 'file.txt' > maybe | unless(IOError, open) | X.read() 20 | -------------------------------------------------------------------------------- /docs/source/overview.rst: -------------------------------------------------------------------------------- 1 | ``pipetools`` enables function composition similar to using Unix pipes. 2 | 3 | It allows forward-composition and piping of arbitrary functions - no need to decorate them or do anything extra. 4 | 5 | It also packs a bunch of utils that make common operations more convenient and readable. 6 | 7 | Source is on github_. 8 | 9 | .. _github: https://github.com/0101/pipetools 10 | 11 | Why? 12 | ---- 13 | 14 | Piping and function composition are some of the most natural operations there are for 15 | plenty of programming tasks. Yet Python doesn't have a built-in way of performing them. 16 | That forces you to either deep nesting of function calls or adding extra **glue code**. 17 | 18 | 19 | Example 20 | ------- 21 | 22 | Say you want to create a list of python files in a given directory, ordered by 23 | filename length, as a string, each file on one line and also with line numbers: 24 | 25 | .. code-block:: pycon 26 | 27 | >>> print(pyfiles_by_length('../pipetools')) 28 | 1. ds_builder.py 29 | 2. __init__.py 30 | 3. compat.py 31 | 4. utils.py 32 | 5. main.py 33 | 34 | All the ingredients are already there, you just have to glue them together. You might write it like this: 35 | 36 | .. code-block:: python 37 | 38 | def pyfiles_by_length(directory): 39 | all_files = os.listdir(directory) 40 | py_files = [f for f in all_files if f.endswith('.py')] 41 | sorted_files = sorted(py_files, key=len, reverse=True) 42 | numbered = enumerate(py_files, 1) 43 | rows = ("{0}. {1}".format(i, f) for i, f in numbered) 44 | return '\n'.join(rows) 45 | 46 | Or perhaps like this: 47 | 48 | .. code-block:: python 49 | 50 | def pyfiles_by_length(directory): 51 | return '\n'.join('{0}. {1}'.format(*x) for x in enumerate(reversed(sorted( 52 | [f for f in os.listdir(directory) if f.endswith('.py')], key=len)), 1)) 53 | 54 | Or, if you're a mad scientist, you would probably do it like this: 55 | 56 | .. code-block:: python 57 | 58 | pyfiles_by_length = lambda d: (reduce('{0}\n{1}'.format, 59 | map(lambda x: '%d. %s' % x, enumerate(reversed(sorted( 60 | filter(lambda f: f.endswith('.py'), os.listdir(d)), key=len)))))) 61 | 62 | 63 | But *there should be one -- and preferably only one -- obvious way to do it*. 64 | 65 | So which one is it? Well, to redeem the situation, ``pipetools`` give you yet 66 | another possibility! 67 | 68 | .. code-block:: python 69 | 70 | pyfiles_by_length = (pipe 71 | | os.listdir 72 | | where(X.endswith('.py')) 73 | | sort_by(len).descending 74 | | (enumerate, X, 1) 75 | | foreach("{0}. {1}") 76 | | '\n'.join) 77 | 78 | *Why would I do that*, you ask? Comparing to the *native* Python code, it's 79 | 80 | - **Easier to read** -- minimal extra clutter 81 | - **Easier to understand** -- one-way data flow from one step to the next, nothing else to keep track of 82 | - **Easier to change** -- want more processing? just add a step to the pipeline 83 | - **Removes some bug opportunities** -- did you spot the bug in the first example? 84 | 85 | Of course it won't solve all your problems, but a great deal of code *can* 86 | be expressed as a pipeline, giving you the above benefits. Read on to see how it works! 87 | 88 | 89 | Installation 90 | ------------ 91 | 92 | .. code-block:: console 93 | 94 | $ pip install pipetools 95 | 96 | `Uh, what's that? `_ 97 | 98 | 99 | Usage 100 | ----- 101 | 102 | .. _the-pipe: 103 | 104 | The pipe 105 | """""""" 106 | The ``pipe`` object can be used to pipe functions together to 107 | form new functions, and it works like this: 108 | 109 | .. code-block:: python 110 | 111 | from pipetools import pipe 112 | 113 | f = pipe | a | b | c 114 | 115 | # is the same as: 116 | def f(x): 117 | return c(b(a(x))) 118 | 119 | 120 | A real example, sum of odd numbers from 0 to *x*: 121 | 122 | .. code-block:: python 123 | 124 | from functools import partial 125 | from pipetools import pipe 126 | 127 | odd_sum = pipe | range | partial(filter, lambda x: x % 2) | sum 128 | 129 | odd_sum(10) # -> 25 130 | 131 | 132 | Note that the chain up to the `sum` is lazy. 133 | 134 | 135 | Automatic partial application in the pipe 136 | """"""""""""""""""""""""""""""""""""""""" 137 | 138 | As partial application is often useful when piping things together, it is done 139 | automatically when the *pipe* encounters a tuple, so this produces the same 140 | result as the previous example: 141 | 142 | .. code-block:: python 143 | 144 | odd_sum = pipe | range | (filter, lambda x: x % 2) | sum 145 | 146 | As of ``0.1.9``, this is even more powerful, see :doc:`X-partial `. 147 | 148 | 149 | Built-in tools 150 | """""""""""""" 151 | 152 | Pipetools contain a set of *pipe-utils* that solve some common tasks. For 153 | example there is a shortcut for the filter class from our example, called 154 | :func:`~pipetools.utils.where`: 155 | 156 | .. code-block:: python 157 | 158 | from pipetools import pipe, where 159 | 160 | odd_sum = pipe | range | where(lambda x: x % 2) | sum 161 | 162 | Well that might be a bit more readable, but not really a huge improvement, but 163 | wait! 164 | 165 | If a *pipe-util* is used as first or second item in the pipe (which happens 166 | quite often) the ``pipe`` at the beginning can be omitted: 167 | 168 | .. code-block:: python 169 | 170 | odd_sum = range | where(lambda x: x % 2) | sum 171 | 172 | 173 | See :doc:`pipe-utils' documentation`. 174 | 175 | 176 | OK, but what about the ugly lambda? 177 | """"""""""""""""""""""""""""""""""" 178 | 179 | :func:`~pipetools.utils.where`, but also :func:`~pipetools.utils.foreach`, 180 | :func:`~pipetools.utils.sort_by` and other :doc:`pipe-utils` can be 181 | quite useful, but require a function as an argument, which can either be a named 182 | function -- which is OK if it does something complicated -- but often it's 183 | something simple, so it's appropriate to use a ``lambda``. Except Python's 184 | lambdas are quite verbose for simple tasks and the code gets cluttered... 185 | 186 | **X object** to the rescue! 187 | 188 | .. code-block:: python 189 | 190 | from pipetools import where, X 191 | 192 | odd_sum = range | where(X % 2) | sum 193 | 194 | 195 | How 'bout that. 196 | 197 | :doc:`Read more about the X object and it's limitations.` 198 | 199 | 200 | .. _auto-string-formatting: 201 | 202 | Automatic string formatting 203 | """"""""""""""""""""""""""" 204 | 205 | Since it doesn't make sense to compose functions with strings, when a pipe (or a 206 | :doc:`pipe-util`) encounters a string, it attempts to use it for 207 | `(advanced) formatting`_: 208 | 209 | .. code-block:: pycon 210 | 211 | >>> countdown = pipe | (range, 1) | reversed | foreach('{}...') | ' '.join | '{} boom' 212 | >>> countdown(5) 213 | '4... 3... 2... 1... boom' 214 | 215 | .. _(advanced) formatting: http://docs.python.org/library/string.html#formatstrings 216 | 217 | 218 | Feeding the pipe 219 | """""""""""""""" 220 | 221 | Sometimes it's useful to create a one-off pipe and immediately run some input 222 | through it. And since this is somewhat awkward (and not very readable, 223 | especially when the pipe spans multiple lines): 224 | 225 | .. code-block:: python 226 | 227 | result = (pipe | foo | bar | boo)(some_input) 228 | 229 | It can also be done using the ``>`` operator: 230 | 231 | .. code-block:: python 232 | 233 | result = some_input > pipe | foo | bar | boo 234 | 235 | .. note:: 236 | Note that the above method of input won't work if the input object 237 | defines `__gt__ `_ 238 | for *any* object - including the pipe. This can be the case for example with 239 | some objects from math libraries such as NumPy. If you experience strange 240 | results try falling back to the standard way of passing input into a pipe. 241 | -------------------------------------------------------------------------------- /docs/source/pipeutils.rst: -------------------------------------------------------------------------------- 1 | pipe-utils 2 | ========== 3 | 4 | Generic piping utilities. 5 | 6 | Other functions can be piped to them, from both sides, without having to use the 7 | ``pipe`` object. The resulting function then also inherits this functionality. 8 | 9 | Built-in 10 | -------- 11 | 12 | Even though these are defined in the ``pipetools.utils`` module, they're 13 | importable directly from ``pipetools`` for convenience. 14 | 15 | All of these that take a function as an argument can automatically partially 16 | apply the given function with positional and/or keyword arguments, for example:: 17 | 18 | foreach(some_func, foo, bar=None) 19 | 20 | Is the same as:: 21 | 22 | foreach(partial(some_func, foo, bar=None)) 23 | 24 | (As of ``0.1.9`` this uses :doc:`xpartial`) 25 | 26 | 27 | They also automatically convert the :doc:`X object` to an actual 28 | function. 29 | 30 | List of built-in utils 31 | ---------------------- 32 | 33 | .. automodule:: pipetools.utils 34 | :members: 35 | 36 | 37 | Make your own 38 | ------------- 39 | 40 | You can make your own, but you generally shouldn't need to, since you can pipe 41 | any functions you like. 42 | 43 | But if you feel like there's a generally reusable *pipe-util* missing, feel 44 | free to add it via a pull request on github_. 45 | 46 | .. _github: https://github.com/0101/pipetools 47 | 48 | How to do it? Just write the function and stick the 49 | :func:`~pipetools.decorators.pipe_util` decorator on it. 50 | 51 | And optionally also :func:`~pipetools.decorators.auto_string_formatter` (see 52 | :ref:`auto-string-formatting`) or 53 | :func:`~pipetools.decorators.data_structure_builder` 54 | (see :ref:`auto-ds-creation`) if it makes sense. 55 | 56 | 57 | .. _auto-ds-creation: 58 | 59 | Automatic data-structure creation 60 | --------------------------------- 61 | 62 | Some of the utils, most importantly :func:`foreach`, offer a shortcut for 63 | creating basic python data structures - ``list``, ``tuple`` and ``dict``. 64 | 65 | It works like this (the ``| list`` at the end is just so we can see the result, 66 | otherwise it would just give us ````):: 67 | 68 | >>> range(5) > foreach({X: X * 2}) | list 69 | [{0: 0}, {1: 2}, {2: 4}, {3: 6}, {4: 8}] 70 | 71 | >>> range(5) > foreach([X, X * '★']) | list 72 | [[0, ''], [1, '★'], [2, '★★'], [3, '★★★'], [4, '★★★★']] 73 | 74 | It can also be combined with string formatting:: 75 | 76 | >>> names = [('John', 'Matrix'), ('Jack', 'Slater')] 77 | >>> names > foreach({'name': "{0} {1}", 'initials': '{0[0]}. {1[0]}.'}) | list 78 | [{'initials': 'J. M.', 'name': 'John Matrix'}, 79 | {'initials': 'J. S.', 'name': 'Jack Slater'}] 80 | 81 | .. _auto-regex: 82 | 83 | Automatic regex conditions 84 | -------------------------- 85 | If you use a string instead of a function as a condition in 86 | :func:`~pipetools.utils.where`, :func:`~pipetools.utils.where_not`, 87 | :func:`~pipetools.utils.select_first` or :func:`~pipetools.utils.take_until` the 88 | string will be used as a regex to match the input against. This will, of course, 89 | work only if the items of the input sequence are strings. 90 | 91 | Essentially:: 92 | 93 | where(r'^some\-regexp?$') 94 | 95 | is equivalent to:: 96 | 97 | where(re.match, r'^some\-regexp?$') 98 | 99 | As of ``0.3.2`` this also filters out ``None`` values instead of throwing an 100 | exception. Making it equivalent to:: 101 | 102 | where(maybe | (re.match, r'^some\-regexp?$')) 103 | 104 | If you want to easily add this functionality to your own functions, you can use 105 | the :func:`~pipetools.decorators.regex_condition` decorator. 106 | -------------------------------------------------------------------------------- /docs/source/xobject.rst: -------------------------------------------------------------------------------- 1 | The X object 2 | ============ 3 | 4 | The ``X`` object is a shortcut for writing simple, one-argument lambdas. 5 | 6 | It's roughly equivalent to this: ``lambda x: x`` 7 | 8 | Example:: 9 | 10 | ~X.some_attr.some_func(1, 2, 3).whatever 11 | # produces: 12 | lambda x: x.some_attr.some_func(1, 2, 3).whatever 13 | 14 | ~(X > 5) 15 | # produces: 16 | lambda x: x > 5 17 | 18 | 19 | Any of the :ref:`supported operations ` performed on an 20 | ``X`` object yield another one that remembers what was done to it, so they 21 | can be chained. 22 | 23 | .. _x-tilde 24 | The ``~`` operator creates the actual function that can be called (without 25 | creating just another ``X`` instance). However, this can be omitted within 26 | *pipe* and :doc:`pipeutils` context where ``X`` is converted to a function 27 | automatically:: 28 | 29 | users > where(X.is_staff) | foreach(X.first_name) | X[:10] 30 | 31 | 32 | But you can make use of ``X`` even outside *pipetools*, you just have to 33 | remember to prefix it with ``~`` 34 | 35 | :: 36 | 37 | my_list = list(users) 38 | my_list.sort(key=~X.last_login.date()) 39 | 40 | 41 | 42 | .. _x-supported-operations: 43 | 44 | Currently supported operations 45 | ------------------------------ 46 | 47 | Most Python operators are supported now, but if you're still 48 | missing something you can add it yourself or create an issue on github_. 49 | 50 | .. _github: https://github.com/0101/pipetools 51 | 52 | 53 | * attribute access (``__getattr__``):: 54 | 55 | X.attr 56 | getattr(X, 'something') 57 | 58 | 59 | * comparisons: ``==``, ``!=``, ``<``, ``<=``, ``>``, ``>=``:: 60 | 61 | X == something 62 | X <= 3 63 | 64 | 65 | * unary arithmetic operators (``+``, ``-``):: 66 | 67 | +X 68 | -X 69 | 70 | 71 | * binary arithmetic operators (``+``, ``-``, ``*``, ``**``, ``@``, ``/``, ``//``, ``%``):: 72 | 73 | X - 3 74 | X + " ...that's what she said!" 75 | 3 - X # works in both directions 76 | 77 | 78 | * bitwise operators (``<<``, ``>>``, ``&``, ``^``):: 79 | 80 | 1 >> X 81 | X & 64 82 | 83 | 84 | 85 | Current limitations 86 | ------------------- 87 | 88 | * call (``__call__``) will only work if X is not in the arguments:: 89 | 90 | # will work 91 | X.method() 92 | X(some, agrs, some=kwargs) 93 | 94 | # will not work 95 | obj.method(X) 96 | some_function(X, some=X) 97 | X.do(X) 98 | 99 | 100 | * item access or slicing (``__getitem__``) will only work if X is not in the 101 | in the index key:: 102 | 103 | # will work 104 | X['key'] 105 | X[0] 106 | X[:10] 107 | X[::-1] 108 | 109 | # will not work 110 | foo[X] 111 | bar[X:] 112 | baz[::X] 113 | X[X.columns] 114 | 115 | 116 | * is contained in / contains (``in``) 117 | 118 | Unfortunately, ``X in container`` can't be done (because the magic method is 119 | called on the container) so there's a special method for that:: 120 | 121 | X._in_(container) 122 | 123 | The opposite form ``item in X`` has not been implemented either. 124 | 125 | 126 | * special methods (``~``, ``|``) 127 | 128 | They have been given :ref:`special meanings ` in pipetools, 129 | so could no more be used as bitwise operations. 130 | 131 | 132 | * logical operators (``and``, ``or``, ``not``) will not work; 133 | they are not exposed as magic methods in Python. 134 | 135 | 136 | * await operator (``await X``) has not been implemented 137 | -------------------------------------------------------------------------------- /docs/source/xpartial.rst: -------------------------------------------------------------------------------- 1 | X-partial 2 | ========= 3 | 4 | More powerful partial application with the :doc:`X object ` 5 | 6 | .. autofunction:: pipetools.xpartial 7 | -------------------------------------------------------------------------------- /pipetools/__init__.py: -------------------------------------------------------------------------------- 1 | from pipetools.utils import foreach 2 | 3 | __version__ = VERSION = 1, 1, 0 4 | __versionstr__ = VERSION > foreach(str) | '.'.join 5 | 6 | from pipetools.main import pipe, X, maybe, xpartial 7 | from pipetools.utils import * 8 | 9 | # prevent namespace pollution 10 | import pipetools.compat 11 | for symbol in dir(pipetools.compat): 12 | if globals().get(symbol) is getattr(pipetools.compat, symbol): 13 | globals().pop(symbol) 14 | -------------------------------------------------------------------------------- /pipetools/compat.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | if sys.version < '3': 4 | from itertools import imap as map 5 | from itertools import ifilter as filter 6 | range = xrange # noqa 7 | text_type = unicode # noqa 8 | string_types = basestring # noqa 9 | dict_items = lambda d: d.iteritems() 10 | else: 11 | from builtins import map, filter, range 12 | text_type = str 13 | string_types = str 14 | dict_items = lambda d: d.items() 15 | -------------------------------------------------------------------------------- /pipetools/debug.py: -------------------------------------------------------------------------------- 1 | from itertools import chain 2 | 3 | from pipetools.compat import map, dict_items 4 | 5 | 6 | def set_name(name, f): 7 | try: 8 | f.__pipetools__name__ = name 9 | except (AttributeError, UnicodeEncodeError): 10 | pass 11 | return f 12 | 13 | 14 | def get_name(f): 15 | from pipetools.main import Pipe 16 | pipetools_name = getattr(f, '__pipetools__name__', None) 17 | if pipetools_name: 18 | return pipetools_name() if callable(pipetools_name) else pipetools_name 19 | 20 | if isinstance(f, Pipe): 21 | return repr(f) 22 | 23 | return f.__name__ if hasattr(f, '__name__') else repr(f) 24 | 25 | 26 | def repr_args(*args, **kwargs): 27 | return ', '.join(chain( 28 | map('{0!r}'.format, args), 29 | map('{0[0]}={0[1]!r}'.format, dict_items(kwargs)))) 30 | -------------------------------------------------------------------------------- /pipetools/decorators.py: -------------------------------------------------------------------------------- 1 | import re 2 | from functools import partial, wraps 3 | 4 | from pipetools.debug import repr_args, set_name, get_name 5 | from pipetools.ds_builder import DSBuilder, NoBuilder 6 | from pipetools.main import pipe, XObject, StringFormatter, xpartial, maybe 7 | from pipetools.compat import string_types, dict_items 8 | 9 | 10 | def pipe_util(func): 11 | """ 12 | Decorator that handles X objects and partial application for pipe-utils. 13 | """ 14 | @wraps(func) 15 | def pipe_util_wrapper(function, *args, **kwargs): 16 | if isinstance(function, XObject): 17 | function = ~function 18 | 19 | original_function = function 20 | 21 | if args or kwargs: 22 | function = xpartial(function, *args, **kwargs) 23 | 24 | name = lambda: '%s(%s)' % (get_name(func), ', '.join( 25 | filter(None, (get_name(original_function), repr_args(*args, **kwargs))))) 26 | 27 | f = func(function) 28 | 29 | result = pipe | set_name(name, f) 30 | 31 | # if the util defines an 'attrs' mapping, copy it as attributes 32 | # to the result 33 | attrs = getattr(f, 'attrs', {}) 34 | for k, v in dict_items(attrs): 35 | setattr(result, k, v) 36 | 37 | return result 38 | 39 | return pipe_util_wrapper 40 | 41 | 42 | def auto_string_formatter(func): 43 | """ 44 | Decorator that handles automatic string formatting. 45 | 46 | By converting a string argument to a function that does formatting on said 47 | string. 48 | """ 49 | @wraps(func) 50 | def auto_string_formatter_wrapper(function, *args, **kwargs): 51 | if isinstance(function, string_types): 52 | function = StringFormatter(function) 53 | 54 | return func(function, *args, **kwargs) 55 | 56 | return auto_string_formatter_wrapper 57 | 58 | 59 | def data_structure_builder(func): 60 | """ 61 | Decorator to handle automatic data structure creation for pipe-utils. 62 | """ 63 | @wraps(func) 64 | def ds_builder_wrapper(function, *args, **kwargs): 65 | try: 66 | function = DSBuilder(function) 67 | except NoBuilder: 68 | pass 69 | return func(function, *args, **kwargs) 70 | 71 | return ds_builder_wrapper 72 | 73 | 74 | def regex_condition(func): 75 | """ 76 | If a condition is given as string instead of a function, it is turned 77 | into a regex-matching function. 78 | """ 79 | @wraps(func) 80 | def regex_condition_wrapper(condition, *args, **kwargs): 81 | if isinstance(condition, string_types): 82 | condition = maybe | partial(re.match, condition) 83 | return func(condition, *args, **kwargs) 84 | return regex_condition_wrapper 85 | -------------------------------------------------------------------------------- /pipetools/ds_builder.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from pipetools.main import XObject, StringFormatter 4 | from pipetools.compat import string_types, dict_items 5 | 6 | 7 | class NoBuilder(ValueError): 8 | pass 9 | 10 | 11 | def DSBuilder(definition): 12 | builder = select_builder(definition) 13 | if builder: 14 | return builder(definition) 15 | raise NoBuilder("Don't know how to build %s" % type(definition)) 16 | 17 | 18 | def SequenceBuilder(cls, definition): 19 | return lambda x: cls(ds_item(d, x) for d in definition) 20 | 21 | 22 | def DictBuilder(definition): 23 | return lambda x: dict( 24 | (ds_item(key_def, x), ds_item(val_def, x)) 25 | for key_def, val_def in dict_items(definition)) 26 | 27 | 28 | builders = { 29 | tuple: partial(SequenceBuilder, tuple), 30 | list: partial(SequenceBuilder, list), 31 | dict: DictBuilder, 32 | } 33 | 34 | 35 | def select_builder(definition): 36 | for cls, builder in dict_items(builders): 37 | if isinstance(definition, cls): 38 | return builder 39 | 40 | 41 | def ds_item(definition, data): 42 | if isinstance(definition, XObject): 43 | return (~definition)(data) 44 | if isinstance(definition, string_types): 45 | return StringFormatter(definition)(data) 46 | if callable(definition): 47 | return definition(data) 48 | try: 49 | return DSBuilder(definition)(data) 50 | except NoBuilder: 51 | # static item 52 | return definition 53 | -------------------------------------------------------------------------------- /pipetools/main.py: -------------------------------------------------------------------------------- 1 | try: 2 | from collections.abc import Iterable 3 | except ImportError: 4 | from collections import Iterable 5 | 6 | from functools import partial, wraps, WRAPPER_ASSIGNMENTS 7 | 8 | from pipetools.debug import get_name, set_name, repr_args 9 | from pipetools.compat import text_type, string_types, dict_items 10 | 11 | 12 | class Pipe(object): 13 | """ 14 | Pipe-style combinator. 15 | 16 | Example:: 17 | 18 | p = pipe | F | G | H 19 | 20 | p(x) == H(G(F(x))) 21 | 22 | """ 23 | def __init__(self, func=None): 24 | self.func = func 25 | self.__name__ = 'Pipe' 26 | 27 | def __str__(self): 28 | return get_name(self.func) 29 | 30 | __repr__ = __str__ 31 | 32 | @staticmethod 33 | def compose(first, second): 34 | name = lambda: '{0} | {1}'.format(get_name(first), get_name(second)) 35 | 36 | def composite(*args, **kwargs): 37 | return second(first(*args, **kwargs)) 38 | return set_name(name, composite) 39 | 40 | @classmethod 41 | def bind(cls, first, second, new_cls=None): 42 | return (new_cls or cls)( 43 | first if second is None else 44 | second if first is None else 45 | cls.compose(first, second)) 46 | 47 | def __or__(self, next_func): 48 | # Handle multiple pipes in pipe definition and also changing pipe type to e.g. Maybe 49 | # this is needed because of evaluation order 50 | pipe_in_a_pipe = isinstance(next_func, Pipe) and next_func.func is None 51 | new_cls = type(next_func) if pipe_in_a_pipe else None 52 | next = None if pipe_in_a_pipe else prepare_function_for_pipe(next_func) 53 | return self.bind(self.func, next, new_cls) 54 | 55 | def __ror__(self, prev_func): 56 | return self.bind(prepare_function_for_pipe(prev_func), self.func) 57 | 58 | def __lt__(self, thing): 59 | return self.func(thing) if self.func else thing 60 | 61 | def __call__(self, *args, **kwargs): 62 | return self.func(*args, **kwargs) 63 | 64 | def __get__(self, instance, owner): 65 | return partial(self, instance) if instance else self 66 | 67 | 68 | pipe = Pipe() 69 | 70 | 71 | class Maybe(Pipe): 72 | 73 | @staticmethod 74 | def compose(first, second): 75 | name = lambda: '{0} ?| {1}'.format(get_name(first), get_name(second)) 76 | 77 | def composite(*args, **kwargs): 78 | result = first(*args, **kwargs) 79 | return None if result is None else second(result) 80 | return set_name(name, composite) 81 | 82 | def __call__(self, *args, **kwargs): 83 | if len(args) == 1 and args[0] is None and not kwargs: 84 | return None 85 | return self.func(*args, **kwargs) 86 | 87 | def __lt__(self, thing): 88 | return ( 89 | None if thing is None else 90 | self.func(thing) if self.func else 91 | thing) 92 | 93 | 94 | maybe = Maybe() 95 | 96 | 97 | def prepare_function_for_pipe(thing): 98 | if isinstance(thing, XObject): 99 | return ~thing 100 | if isinstance(thing, tuple): 101 | return xpartial(*thing) 102 | if isinstance(thing, string_types): 103 | return StringFormatter(thing) 104 | if callable(thing): 105 | return thing 106 | raise ValueError('Cannot pipe %s' % thing) 107 | 108 | 109 | def StringFormatter(template): 110 | 111 | f = text_type(template).format 112 | 113 | def format(content): 114 | if isinstance(content, dict): 115 | return f(**content) 116 | if _iterable(content): 117 | return f(*content) 118 | return f(content) 119 | 120 | return set_name(lambda: "format('%s')" % template[:20], format) 121 | 122 | 123 | def _iterable(obj): 124 | "Iterable but not a string" 125 | return isinstance(obj, Iterable) and not isinstance(obj, string_types) 126 | 127 | 128 | class XObject(object): 129 | 130 | def __init__(self, func=None): 131 | self._func = func 132 | set_name(lambda: get_name(func) if func else 'X', self) 133 | 134 | def __repr__(self): 135 | return get_name(self) 136 | 137 | def __invert__(self): 138 | return self._func or set_name('X', lambda x: x) 139 | 140 | def bind(self, name, func): 141 | set_name(name, func) 142 | return XObject((self._func | func) if self._func else (pipe | func)) 143 | 144 | def __call__(self, *args, **kwargs): 145 | name = lambda: 'X(%s)' % repr_args(*args, **kwargs) 146 | return self.bind(name, lambda x: x(*args, **kwargs)) 147 | 148 | def __hash__(self): 149 | return super(XObject, self).__hash__() 150 | 151 | def __eq__(self, other): 152 | return self.bind(lambda: 'X == {0!r}'.format(other), lambda x: x == other) 153 | 154 | def __getattr__(self, name): 155 | return self.bind(lambda: 'X.{0}'.format(name), lambda x: getattr(x, name)) 156 | 157 | def __getitem__(self, item): 158 | return self.bind(lambda: 'X[{0!r}]'.format(item), lambda x: x[item]) 159 | 160 | def __gt__(self, other): 161 | return self.bind(lambda: 'X > {0!r}'.format(other), lambda x: x > other) 162 | 163 | def __ge__(self, other): 164 | return self.bind(lambda: 'X >= {0!r}'.format(other), lambda x: x >= other) 165 | 166 | def __lt__(self, other): 167 | return self.bind(lambda: 'X < {0!r}'.format(other), lambda x: x < other) 168 | 169 | def __le__(self, other): 170 | return self.bind(lambda: 'X <= {0!r}'.format(other), lambda x: x <= other) 171 | 172 | def __ne__(self, other): 173 | return self.bind(lambda: 'X != {0!r}'.format(other), lambda x: x != other) 174 | 175 | def __pos__(self): 176 | return self.bind(lambda: '+X', lambda x: +x) 177 | 178 | def __neg__(self): 179 | return self.bind(lambda: '-X', lambda x: -x) 180 | 181 | def __mul__(self, other): 182 | return self.bind(lambda: 'X * {0!r}'.format(other), lambda x: x * other) 183 | 184 | def __rmul__(self, other): 185 | return self.bind(lambda: '{0!r} * X'.format(other), lambda x: other * x) 186 | 187 | def __matmul__(self, other): 188 | # prevent syntax error on legacy interpretors 189 | from operator import matmul 190 | return self.bind(lambda: 'X @ {0!r}'.format(other), lambda x: matmul(x, other)) 191 | 192 | def __rmatmul__(self, other): 193 | from operator import matmul 194 | return self.bind(lambda: '{0!r} @ X'.format(other), lambda x: matmul(other, x)) 195 | 196 | def __div__(self, other): 197 | return self.bind(lambda: 'X / {0!r}'.format(other), lambda x: x / other) 198 | 199 | def __rdiv__(self, other): 200 | return self.bind(lambda: '{0!r} / X'.format(other), lambda x: other / x) 201 | 202 | def __truediv__(self, other): 203 | return self.bind(lambda: 'X / {0!r}'.format(other), lambda x: x / other) 204 | 205 | def __rtruediv__(self, other): 206 | return self.bind(lambda: '{0!r} / X'.format(other), lambda x: other / x) 207 | 208 | def __floordiv__(self, other): 209 | return self.bind(lambda: 'X // {0!r}'.format(other), lambda x: x // other) 210 | 211 | def __rfloordiv__(self, other): 212 | return self.bind(lambda: '{0!r} // X'.format(other), lambda x: other // x) 213 | 214 | def __mod__(self, other): 215 | return self.bind(lambda: 'X % {0!r}'.format(other), lambda x: x % other) 216 | 217 | def __rmod__(self, other): 218 | return self.bind(lambda: '{0!r} % X'.format(other), lambda x: other % x) 219 | 220 | def __add__(self, other): 221 | return self.bind(lambda: 'X + {0!r}'.format(other), lambda x: x + other) 222 | 223 | def __radd__(self, other): 224 | return self.bind(lambda: '{0!r} + X'.format(other), lambda x: other + x) 225 | 226 | def __sub__(self, other): 227 | return self.bind(lambda: 'X - {0!r}'.format(other), lambda x: x - other) 228 | 229 | def __rsub__(self, other): 230 | return self.bind(lambda: '{0!r} - X'.format(other), lambda x: other - x) 231 | 232 | def __pow__(self, other): 233 | return self.bind(lambda: 'X ** {0!r}'.format(other), lambda x: x ** other) 234 | 235 | def __rpow__(self, other): 236 | return self.bind(lambda: '{0!r} ** X'.format(other), lambda x: other ** x) 237 | 238 | def __lshift__(self, other): 239 | return self.bind(lambda: 'X << {0!r}'.format(other), lambda x: x << other) 240 | 241 | def __rlshift__(self, other): 242 | return self.bind(lambda: '{0!r} << X'.format(other), lambda x: other << x) 243 | 244 | def __rshift__(self, other): 245 | return self.bind(lambda: 'X >> {0!r}'.format(other), lambda x: x >> other) 246 | 247 | def __rrshift__(self, other): 248 | return self.bind(lambda: '{0!r} >> X'.format(other), lambda x: other >> x) 249 | 250 | def __and__(self, other): 251 | return self.bind(lambda: 'X & {0!r}'.format(other), lambda x: x & other) 252 | 253 | def __rand__(self, other): 254 | return self.bind(lambda: '{0!r} & X'.format(other), lambda x: other & x) 255 | 256 | def __xor__(self, other): 257 | return self.bind(lambda: 'X ^ {0!r}'.format(other), lambda x: x ^ other) 258 | 259 | def __rxor__(self, other): 260 | return self.bind(lambda: '{0!r} ^ X'.format(other), lambda x: other ^ x) 261 | 262 | def __ror__(self, func): 263 | return pipe | func | self 264 | 265 | def __or__(self, func): 266 | if isinstance(func, Pipe): 267 | return func.__ror__(self) 268 | return pipe | self | func 269 | 270 | def _in_(self, y): 271 | return self.bind(lambda: 'X._in_({0!r})'.format(y), lambda x: x in y) 272 | 273 | 274 | X = XObject() 275 | 276 | 277 | def xpartial(func, *xargs, **xkwargs): 278 | """ 279 | Like :func:`functools.partial`, but can take an :class:`XObject` 280 | placeholder that will be replaced with the first positional argument 281 | when the partially applied function is called. 282 | 283 | Useful when the function's positional arguments' order doesn't fit your 284 | situation, e.g.: 285 | 286 | >>> reverse_range = xpartial(range, X, 0, -1) 287 | >>> reverse_range(5) 288 | [5, 4, 3, 2, 1] 289 | 290 | It can also be used to transform the positional argument to a keyword 291 | argument, which can come in handy inside a *pipe*:: 292 | 293 | xpartial(objects.get, id=X) 294 | 295 | Also the XObjects are evaluated, which can be used for some sort of 296 | destructuring of the argument:: 297 | 298 | xpartial(somefunc, name=X.name, number=X.contacts['number']) 299 | 300 | Lastly, unlike :func:`functools.partial`, this creates a regular function 301 | which will bind to classes (like the ``curry`` function from 302 | ``django.utils.functional``). 303 | """ 304 | any_x = any(isinstance(a, XObject) for a in xargs + tuple(xkwargs.values())) 305 | use = lambda x, value: (~x)(value) if isinstance(x, XObject) else x 306 | 307 | @wraps(func, assigned=filter(partial(hasattr, func), WRAPPER_ASSIGNMENTS)) 308 | def xpartially_applied(*func_args, **func_kwargs): 309 | if any_x: 310 | if not func_args: 311 | raise ValueError('Function "%s" partially applied with an ' 312 | 'X placeholder but called with no positional arguments.' 313 | % get_name(func)) 314 | first = func_args[0] 315 | rest = func_args[1:] 316 | args = tuple(use(x, first) for x in xargs) + rest 317 | kwargs = dict((k, use(x, first)) for k, x in dict_items(xkwargs)) 318 | kwargs.update(func_kwargs) 319 | else: 320 | args = xargs + func_args 321 | kwargs = dict(xkwargs, **func_kwargs) 322 | return func(*args, **kwargs) 323 | 324 | name = lambda: '%s(%s)' % (get_name(func), repr_args(*xargs, **xkwargs)) 325 | return set_name(name, xpartially_applied) 326 | -------------------------------------------------------------------------------- /pipetools/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | try: 3 | from collections.abc import Mapping 4 | except ImportError: 5 | from collections import Mapping 6 | 7 | from functools import partial, wraps 8 | from itertools import islice, takewhile, dropwhile 9 | import operator 10 | 11 | from pipetools.compat import map, filter, range, dict_items 12 | from pipetools.debug import set_name, repr_args, get_name 13 | from pipetools.decorators import data_structure_builder, regex_condition 14 | from pipetools.decorators import pipe_util, auto_string_formatter 15 | from pipetools.main import pipe, X, _iterable 16 | 17 | 18 | KEY, VALUE = X[0], X[1] 19 | 20 | 21 | @pipe_util 22 | @auto_string_formatter 23 | @data_structure_builder 24 | def foreach(function): 25 | """ 26 | Returns a function that takes an iterable and returns an iterator over the 27 | results of calling `function` on each item of the iterable. 28 | 29 | >>> range(5) > foreach(factorial) | list 30 | [1, 1, 2, 6, 24] 31 | """ 32 | return partial(map, function) 33 | 34 | 35 | @pipe_util 36 | def foreach_do(function): 37 | """ 38 | Like :func:`foreach` but is evaluated immediately and doesn't return 39 | anything. 40 | 41 | For the occasion that you just want to do some side-effects:: 42 | 43 | open('addresses.txt') > foreach(geocode) | foreach_do(launch_missile) 44 | 45 | -- With :func:`foreach` nothing would happen (except an itetrator being 46 | created) 47 | """ 48 | def f(iterable): 49 | for item in iterable: 50 | function(item) 51 | 52 | return f 53 | 54 | 55 | @pipe_util 56 | @regex_condition 57 | def where(condition): 58 | """ 59 | Pipe-able lazy filter. 60 | 61 | >>> odd_range = range | where(X % 2) | list 62 | >>> odd_range(10) 63 | [1, 3, 5, 7, 9] 64 | 65 | """ 66 | return partial(filter, condition) 67 | 68 | 69 | @pipe_util 70 | @regex_condition 71 | def where_not(condition): 72 | """ 73 | Inverted :func:`where`. 74 | """ 75 | return partial(filter, pipe | condition | operator.not_) 76 | 77 | 78 | @pipe_util 79 | @data_structure_builder 80 | def sort_by(function): 81 | """ 82 | Sorts an incoming sequence by using the given `function` as key. 83 | 84 | >>> range(10) > sort_by(-X) 85 | [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] 86 | 87 | Supports automatic data-structure creation:: 88 | 89 | users > sort_by([X.last_name, X.first_name]) 90 | 91 | There is also a shortcut for ``sort_by(X)`` called ``sort``: 92 | 93 | >>> [4, 5, 8, -3, 0] > sort 94 | [-3, 0, 4, 5, 8] 95 | 96 | And (as of ``0.2.3``) a shortcut for reversing the sort: 97 | 98 | >>> 'asdfaSfa' > sort_by(X.lower()).descending 99 | ['s', 'S', 'f', 'f', 'd', 'a', 'a', 'a'] 100 | """ 101 | f = partial(sorted, key=function) 102 | f.attrs = {'descending': _descending_sort_by(function)} 103 | return f 104 | 105 | 106 | @pipe_util 107 | def _descending_sort_by(function): 108 | return partial(sorted, key=function, reverse=True) 109 | 110 | 111 | sort = sort_by(X) 112 | 113 | 114 | @pipe_util 115 | @auto_string_formatter 116 | @data_structure_builder 117 | def debug_print(function): 118 | """ 119 | Prints function applied on input and returns the input. 120 | 121 | :: 122 | 123 | foo = (pipe 124 | | something 125 | | debug_print(X.get_status()) 126 | | something_else 127 | | foreach(debug_print("attr is: {0.attr}")) 128 | | etc) 129 | """ 130 | def debug(thing): 131 | print(function(thing)) 132 | return thing 133 | return debug 134 | 135 | 136 | @pipe_util 137 | def tee(function): 138 | """ 139 | Sends a copy of the input into function - like a T junction. 140 | """ 141 | def _tee(thing): 142 | function(thing) 143 | return thing 144 | return _tee 145 | 146 | 147 | @pipe_util 148 | def as_args(function): 149 | """ 150 | Applies the sequence in the input as positional arguments to `function`. 151 | 152 | :: 153 | 154 | some_lists > as_args(izip) 155 | """ 156 | return lambda x: function(*x) 157 | 158 | 159 | @pipe_util 160 | def as_kwargs(function): 161 | """ 162 | Applies the dictionary in the input as keyword arguments to `function`. 163 | """ 164 | return lambda x: function(**x) 165 | 166 | 167 | def take_first(count): 168 | """ 169 | Assumes an iterable on the input, returns an iterable with first `count` 170 | items from the input (or possibly less, if there isn't that many). 171 | 172 | >>> range(9000) > where(X % 100 == 0) | take_first(5) | tuple 173 | (0, 100, 200, 300, 400) 174 | 175 | """ 176 | def _take_first(iterable): 177 | return islice(iterable, count) 178 | return pipe | set_name('take_first(%s)' % count, _take_first) 179 | 180 | 181 | def drop_first(count): 182 | """ 183 | Assumes an iterable on the input, returns an iterable with identical items 184 | except for the first `count`. 185 | 186 | >>> range(10) > drop_first(5) | tuple 187 | (5, 6, 7, 8, 9) 188 | """ 189 | def _drop_first(iterable): 190 | g = (x for x in range(1, count + 1)) 191 | return dropwhile( 192 | lambda i: unless(StopIteration, lambda: next(g))(), iterable) 193 | return pipe | set_name('drop_first(%s)' % count, _drop_first) 194 | 195 | 196 | def unless(exception_class_or_tuple, func, *args, **kwargs): 197 | """ 198 | When `exception_class_or_tuple` occurs while executing `func`, it will 199 | be caught and ``None`` will be returned. 200 | 201 | >>> f = where(X > 10) | list | unless(IndexError, X[0]) 202 | >>> f([5, 8, 12, 4]) 203 | 12 204 | >>> f([1, 2, 3]) 205 | None 206 | """ 207 | @pipe_util 208 | @auto_string_formatter 209 | @data_structure_builder 210 | def construct_unless(function): 211 | # a wrapper so we can re-use the decorators 212 | def _unless(*args, **kwargs): 213 | try: 214 | return function(*args, **kwargs) 215 | except exception_class_or_tuple: 216 | pass 217 | return _unless 218 | 219 | name = lambda: 'unless(%s, %s)' % (exception_class_or_tuple, ', '.join( 220 | filter(None, (get_name(func), repr_args(*args, **kwargs))))) 221 | 222 | return set_name(name, construct_unless(func, *args, **kwargs)) 223 | 224 | 225 | @pipe_util 226 | @regex_condition 227 | def select_first(condition): 228 | """ 229 | Returns first item from input sequence that satisfies `condition`. Or 230 | ``None`` if none does. 231 | 232 | >>> ['py', 'pie', 'pi'] > select_first(X.startswith('pi')) 233 | 'pie' 234 | 235 | As of ``0.2.1`` you can also 236 | :ref:`directly use regular expressions ` and write the above 237 | as: 238 | 239 | >>> ['py', 'pie', 'pi'] > select_first('^pi') 240 | 'pie' 241 | 242 | There is also a shortcut for ``select_first(X)`` called ``first_of``: 243 | 244 | >>> first_of(['', None, 0, 3, 'something']) 245 | 3 246 | >>> first_of([]) 247 | None 248 | """ 249 | return where(condition) | unless(StopIteration, next) 250 | 251 | 252 | first_of = select_first(X) 253 | 254 | 255 | @pipe_util 256 | @auto_string_formatter 257 | @data_structure_builder 258 | def group_by(function): 259 | """ 260 | Groups input sequence by `function`. 261 | 262 | Returns an iterator over a sequence of tuples where the first item is a 263 | result of `function` and the second one a list of items matching this 264 | result. 265 | 266 | Ordering of the resulting iterator is undefined, but ordering of the items 267 | in the groups is preserved. 268 | 269 | >>> [1, 2, 3, 4, 5, 6] > group_by(X % 2) | list 270 | [(0, [2, 4, 6]), (1, [1, 3, 5])] 271 | """ 272 | def _group_by(seq): 273 | result = {} 274 | for item in seq: 275 | result.setdefault(function(item), []).append(item) 276 | return dict_items(result) 277 | 278 | return _group_by 279 | 280 | 281 | def _flatten(x): 282 | if not _iterable(x) or isinstance(x, Mapping): 283 | yield x 284 | else: 285 | for y in x: 286 | for z in _flatten(y): 287 | yield z 288 | 289 | 290 | def flatten(*args): 291 | """ 292 | Flattens an arbitrarily deep nested iterable(s). 293 | 294 | >>> [[[[[[1]]], 2], range(2) > foreach(X + 3)]] > flatten | list 295 | [1, 2, 3, 4] 296 | 297 | Does not treat strings and (as of ``0.3.1``) mappings (dictionaries) 298 | as iterables so these are left alone. 299 | 300 | >>> ('hello', [{'how': 'are'}, [['you']]]) > flatten | list 301 | ['hello', {'how': 'are'}, 'you'] 302 | 303 | Also turns non-iterables into iterables which is convenient when you 304 | are not sure about the input. 305 | 306 | >>> 'stuff' > flatten | list 307 | ['stuff'] 308 | """ 309 | return _flatten(args) 310 | flatten = wraps(flatten)(pipe | flatten) 311 | 312 | 313 | def count(iterable): 314 | """ 315 | Returns the number of items in `iterable`. 316 | """ 317 | return sum(1 for whatever in iterable) 318 | count = wraps(count)(pipe | count) 319 | 320 | 321 | @pipe_util 322 | @regex_condition 323 | def take_until(condition): 324 | """ 325 | >>> [1, 4, 6, 4, 1] > take_until(X > 5) | list 326 | [1, 4] 327 | 328 | >>> [1, 4, 6, 4, 1] > take_until(X > 5).including | list 329 | [1, 4, 6] 330 | """ 331 | f = partial(takewhile, pipe | condition | operator.not_) 332 | f.attrs = {'including': take_until_including(condition)} 333 | return f 334 | 335 | 336 | @pipe_util 337 | @regex_condition 338 | def take_until_including(condition): 339 | """ 340 | >>> [1, 4, 6, 4, 1] > take_until_including(X > 5) | list 341 | [1, 4, 6] 342 | """ 343 | def take_until_including_(interable): 344 | for i in interable: 345 | if not condition(i): 346 | yield i 347 | else: 348 | yield i 349 | break 350 | return take_until_including_ 351 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import io 2 | import sys 3 | from setuptools import setup 4 | from setuptools.command.test import test as TestCommand 5 | 6 | from pipetools import xpartial, X 7 | import pipetools 8 | 9 | 10 | class PyTest(TestCommand): 11 | 12 | def finalize_options(self): 13 | TestCommand.finalize_options(self) 14 | self.test_args = [] 15 | self.test_suite = True 16 | 17 | def run_tests(self): 18 | # import here, cause outside the eggs aren't loaded 19 | import pytest 20 | error_code = pytest.main(self.test_args) 21 | sys.exit(error_code) 22 | 23 | 24 | setup( 25 | name='pipetools', 26 | version=pipetools.__versionstr__, 27 | description=('A library that enables function composition similar to ' 28 | 'using Unix pipes.'), 29 | long_description='README.rst' > xpartial(io.open, X, encoding="utf-8") | X.read(), 30 | author='Petr Pokorny', 31 | author_email='petr@innit.cz', 32 | license='MIT', 33 | url='https://0101.github.io/pipetools/', 34 | packages=['pipetools'], 35 | include_package_data=True, 36 | install_requires=( 37 | 'setuptools>=0.6b1', 38 | ), 39 | tests_require=( 40 | 'pytest', 41 | ), 42 | cmdclass={'test': PyTest}, 43 | 44 | classifiers=[ 45 | 'Development Status :: 5 - Production/Stable', 46 | 'Intended Audience :: Developers', 47 | 'License :: OSI Approved :: MIT License', 48 | 'Operating System :: OS Independent', 49 | 'Programming Language :: Python :: 2', 50 | 'Programming Language :: Python :: 3', 51 | 'Topic :: Utilities', 52 | ] 53 | ) 54 | -------------------------------------------------------------------------------- /test_pipetools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0101/pipetools/6cba9fadab07a16fd85eed16d5cffc609f84c62b/test_pipetools/__init__.py -------------------------------------------------------------------------------- /test_pipetools/test_decorators.py: -------------------------------------------------------------------------------- 1 | from pipetools import foreach, sort_by, X, unless 2 | from pipetools.compat import range 3 | 4 | 5 | def my_func(*args, **kwargs): 6 | pass 7 | 8 | 9 | def test_pipe_util_xpartial(): 10 | f = range | foreach(range, X, 0, -1) | foreach(list) | list 11 | assert f(3, 5) == [[3, 2, 1], [4, 3, 2, 1]] 12 | 13 | 14 | class TestPipeUtilsRepr: 15 | 16 | def test_basic(self): 17 | f = foreach(my_func) 18 | assert repr(f) == 'foreach(my_func)' 19 | 20 | def test_partially_applied(self): 21 | f = foreach(my_func, 42, kwarg=2) 22 | assert repr(f) == 'foreach(my_func, 42, kwarg=2)' 23 | 24 | def test_string_formatting(self): 25 | f = foreach("{0} asdf {1} jk;l") 26 | assert repr(f) == "foreach('{0} asdf {1} jk;l')" 27 | 28 | def test_ds_builder(self): 29 | f = sort_by([X.attr, X * 2]) 30 | assert repr(f) == 'sort_by([X.attr, X * 2])' 31 | 32 | def test_repr_doesnt_get_called_when_not_necessary(self): 33 | 34 | class Something(object): 35 | 36 | def __repr__(self): 37 | assert False, "__repr__ called when not necessary" 38 | 39 | foreach(Something()) 40 | unless(Exception, Something()) 41 | -------------------------------------------------------------------------------- /test_pipetools/test_ds_builder.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pipetools.ds_builder import DSBuilder 4 | from pipetools.main import X 5 | from pipetools.compat import range 6 | 7 | 8 | def test_build_tuple(): 9 | 10 | f = DSBuilder((X, X * 2)) 11 | 12 | assert f('olol') == ('olol', 'olololol') 13 | 14 | 15 | def test_build_list(): 16 | 17 | f = DSBuilder([X, X * 2]) 18 | 19 | assert f('olol') == ['olol', 'olololol'] 20 | 21 | 22 | def test_build_dict(): 23 | 24 | f = DSBuilder({X: X * 2}) 25 | 26 | assert f('olol') == {'olol': 'olololol'} 27 | 28 | 29 | def test_constant_items(): 30 | 31 | f = DSBuilder({42: X}) 32 | 33 | assert f('value') == {42: 'value'} 34 | 35 | 36 | def test_format(): 37 | 38 | f = DSBuilder(['{0} or not {0}?', 'not {0}']) 39 | 40 | assert f('to be') == ['to be or not to be?', 'not to be'] 41 | 42 | 43 | def test_nested(): 44 | 45 | f = DSBuilder({'seq': [X * y for y in range(4)]}) 46 | 47 | assert f(2) == {'seq': [0, 2, 4, 6]} 48 | 49 | 50 | def test_no_builder(): 51 | 52 | with pytest.raises(ValueError): 53 | DSBuilder('not a DS') 54 | -------------------------------------------------------------------------------- /test_pipetools/test_main.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import pytest 3 | 4 | from pipetools import pipe, X, maybe, xpartial 5 | from pipetools.main import StringFormatter 6 | from pipetools.compat import range 7 | 8 | 9 | class Bunch: 10 | def __init__(self, **kwargs): 11 | self.__dict__.update(kwargs) 12 | 13 | 14 | class TestPipe(object): 15 | 16 | pipe = property(lambda self: pipe) 17 | 18 | def test_pipe(self): 19 | 20 | p = self.pipe | str | (lambda x: x * 2) 21 | 22 | assert p(5) == '55' 23 | 24 | def test_pipe_right(self): 25 | f = sum | self.pipe | str 26 | 27 | assert f([1, 2, 3]) == '6' 28 | 29 | def test_pipe_input(self): 30 | result = [1, 2, 3] > self.pipe | sum 31 | 32 | assert result == 6 33 | 34 | def test_pipe_right_X(self): 35 | f = X[0] | self.pipe | str 36 | 37 | assert f([1, 2, 3]) == '1' 38 | 39 | def test_string_formatting(self): 40 | f = self.pipe | 'The answer is {0}.' 41 | 42 | assert f(42) == 'The answer is 42.' 43 | 44 | def test_unicode_formatting(self): 45 | f = self.pipe | u'That will be £ {0}, please.' 46 | assert f(42) == u'That will be £ 42, please.' 47 | 48 | def test_makes_a_bound_method(self): 49 | 50 | class SomeClass(object): 51 | attr = 'foo bar' 52 | method = X.attr.split() | reversed | ' '.join 53 | 54 | assert SomeClass().method() == 'bar foo' 55 | 56 | 57 | class TestX: 58 | 59 | def test_basic(self): 60 | 61 | f = ~X.startswith('Hello') 62 | 63 | assert f('Hello world') 64 | assert not f('Goodbye world') 65 | 66 | def test_chained(self): 67 | 68 | f = ~X.get('item', '').startswith('Hello') 69 | 70 | assert f({'item': 'Hello world'}) 71 | assert not f({'item': 'Goodbye world'}) 72 | assert not f({}) 73 | 74 | def test_passthrough(self): 75 | 76 | f = ~X 77 | 78 | assert f(42) == 42 79 | 80 | def test_mod(self): 81 | 82 | f = ~(X % 2) 83 | g = ~(9 % X) 84 | 85 | assert f(3) 86 | assert not g(3) 87 | assert g(2) 88 | assert not f(2) 89 | 90 | def test_gt(self): 91 | 92 | f = ~(X > 5) 93 | g = ~(6 > X) 94 | 95 | assert f(6) 96 | assert not g(6) 97 | assert g(5) 98 | assert not f(5) 99 | 100 | def test_gte(self): 101 | 102 | f = ~(X >= 5) 103 | g = ~(4 >= X) 104 | 105 | assert f(5) 106 | assert not g(5) 107 | assert g(4) 108 | assert not f(4) 109 | 110 | def test_lt(self): 111 | 112 | f = ~(X < 5) 113 | g = ~(4 < X) 114 | 115 | assert f(4) 116 | assert not g(4) 117 | assert g(5) 118 | assert not f(5) 119 | 120 | def test_lte(self): 121 | 122 | f = ~(X <= 5) 123 | g = ~(6 <= X) 124 | 125 | assert f(5) 126 | assert not g(5) 127 | assert g(6) 128 | assert not f(6) 129 | 130 | def test_chained_gt(self): 131 | 132 | f = ~(X.thing > 5) 133 | 134 | assert f(Bunch(thing=6)) 135 | assert not f(Bunch(thing=4)) 136 | 137 | def test_index(self): 138 | 139 | f = ~(X['item']) 140 | 141 | assert f({'item': 42}) == 42 142 | 143 | def test_eq(self): 144 | 145 | f = ~(X == 42) 146 | 147 | assert f(42) 148 | assert not f('whatever') 149 | 150 | def test_neq(self): 151 | 152 | f = ~(X != 42) 153 | 154 | assert not f(42) 155 | assert f('whatever') 156 | 157 | def test_pos(self): 158 | 159 | f = ~+X 160 | 161 | assert f(4) == 4 162 | 163 | def test_neg(self): 164 | 165 | f = ~-X 166 | 167 | assert f(5) == -5 168 | 169 | def test_pipe_right(self): 170 | 171 | f = str | X[0] 172 | 173 | assert f(10) == '1' 174 | 175 | def test_pipe_left(self): 176 | 177 | f = X[0] | int 178 | 179 | assert f('10') == 1 180 | 181 | def test_call(self): 182 | 183 | f = ~X(42) 184 | 185 | assert f(lambda n: n / 2) == 21 186 | 187 | def test_mul(self): 188 | 189 | f = ~(X * 3) 190 | g = ~(3 * X) 191 | 192 | assert f(10) == g(10) == 30 193 | assert f('x') == g('x') == 'xxx' 194 | 195 | def test_add(self): 196 | assert (~(X + 2))(40) == 42 197 | assert (~('4' + X))('2') == '42' 198 | assert (~([4] + X))([2]) == [4, 2] 199 | 200 | def test_sub(self): 201 | assert (~(X - 3))(5) == (5 - 3) 202 | assert (~(5 - X))(3) == (5 - 3) 203 | 204 | def test_pow(self): 205 | assert (~(X ** 3))(5) == (5 ** 3) 206 | assert (~(5 ** X))(3) == (5 ** 3) 207 | 208 | def test_div(self): 209 | assert (~(X / 2))(4) == 2 210 | assert (~(4 / X))(2) == 2 211 | 212 | def test_floor_dev(self): 213 | assert (~(X // 2))(5) == 2 214 | assert (~(5 // X))(2) == 2 215 | 216 | def test_mod(self): 217 | assert (~(X % 5))('%.2f') == '5.00' 218 | assert (~(5 % X))(2) == 1 219 | 220 | def test_lshift(self): 221 | assert (~(X << 2))(5) == 20 222 | assert (~(2 << X))(5) == 64 223 | 224 | def test_rshift(self): 225 | assert (~(X >> 1))(5) == 2 226 | assert (~(5 >> X))(1) == 2 227 | 228 | def test_xor(self): 229 | assert (~(X ^ 2))(3) == 1 230 | assert (~(1 ^ X))(3) == 2 231 | 232 | def test_and(self): 233 | assert (~(X & 2))(3) == 2 234 | assert (~(1 & X))(3) == 1 235 | 236 | def test_in(self): 237 | container = 'asdf' 238 | 239 | f = ~X._in_(container) 240 | 241 | assert f('a') 242 | assert not f('b') 243 | 244 | def test_repr(self): 245 | f = ~X.attr(1, 2, three='four') 246 | assert repr(f) == "X.attr | X(1, 2, three='four')" 247 | 248 | def test_repr_unicode(self): 249 | f = ~(X + u"Žluťoučký kůň") 250 | # in this case I'll just consider not throwing an error a success 251 | assert repr(f) 252 | 253 | def test_repr_tuple(self): 254 | f = ~(X + (1, 2)) 255 | assert repr(f) == "X + (1, 2)" 256 | 257 | 258 | class TestStringFormatter: 259 | 260 | def test_format_tuple(self): 261 | f = StringFormatter('{0} + {0} = {1}') 262 | assert f((1, 2)) == '1 + 1 = 2' 263 | 264 | def test_format_list(self): 265 | f = StringFormatter('{0} + {0} = {1}') 266 | assert f([1, 2]) == '1 + 1 = 2' 267 | 268 | def test_format_generator(self): 269 | f = StringFormatter('{0} + {0} = {1}') 270 | assert f(range(1, 3)) == '1 + 1 = 2' 271 | 272 | def test_format_dict(self): 273 | f = StringFormatter('{a} and {b}') 274 | assert f(dict(a='A', b='B')) == 'A and B' 275 | 276 | def test_format_one_arg(self): 277 | f = StringFormatter('This is {0}!!1') 278 | assert f('Spartah') == 'This is Spartah!!1' 279 | 280 | def test_unicode(self): 281 | f = StringFormatter('Asdf {0}') 282 | assert f(u'Žluťoučký kůň') == u'Asdf Žluťoučký kůň' 283 | 284 | def test_unicode2(self): 285 | f = StringFormatter(u'Asdf {0}') 286 | assert f(u'Žluťoučký kůň') == u'Asdf Žluťoučký kůň' 287 | 288 | 289 | class TestMaybe(TestPipe): 290 | 291 | # maybe should also pass default pipe tests 292 | pipe = property(lambda self: maybe) 293 | 294 | def test_maybe_basic(self): 295 | f = maybe | (lambda: None) | X * 2 296 | 297 | assert f() is None 298 | 299 | def test_none_input(self): 300 | assert (None > maybe | sum) is None 301 | 302 | def test_none_input_call(self): 303 | assert (maybe | sum)(None) is None 304 | 305 | 306 | class TestPipeInAPipe: 307 | 308 | def test_maybe_in_a_pipe_catches_none(self): 309 | f = pipe | str | int | (lambda x: None) | maybe | X.hello 310 | assert f(3) is None 311 | 312 | def test_maybe_in_a_pipe_goes_through(self): 313 | f = pipe | str | int | maybe | (X * 2) 314 | assert f(3) == 6 315 | 316 | def test_maybe_in_a_pipe_not_active_before_its_place(self): 317 | f = pipe | str | (lambda x: None) | int | maybe | X.hello 318 | with pytest.raises(TypeError): 319 | f(3) 320 | 321 | def test_pipe_in_a_pipe_because_why_not(self): 322 | f = pipe | str | pipe | int 323 | assert f(3) == 3 324 | 325 | 326 | def dummy(*args, **kwargs): 327 | return args, kwargs 328 | 329 | 330 | class TestXPartial: 331 | 332 | def test_should_behave_like_partial(self): 333 | xf = xpartial(dummy, 1, kwarg='kwarg') 334 | assert xf(2, foo='bar') == ((1, 2), {'kwarg': 'kwarg', 'foo': 'bar'}) 335 | 336 | def test_x_placeholder(self): 337 | xf = xpartial(dummy, X, 2) 338 | assert xf(1) == ((1, 2), {}) 339 | 340 | def test_x_kw_placeholder(self): 341 | xf = xpartial(dummy, kwarg=X) 342 | assert xf(1) == ((), {'kwarg': 1}) 343 | 344 | def test_x_destructuring(self): 345 | xf = xpartial(dummy, X['name'], number=X['number']) 346 | d = {'name': "Fred", 'number': 42, 'something': 'else'} 347 | assert xf(d) == (('Fred',), {'number': 42}) 348 | 349 | def test_repr(self): 350 | xf = xpartial(dummy, X, 3, something=X['something']) 351 | assert repr(X | xf) == "X | dummy(X, 3, something=X['something'])" 352 | 353 | def test_should_raise_error_when_not_given_an_argument(self): 354 | # -- when created with a placeholder 355 | xf = xpartial(dummy, something=X) 356 | with pytest.raises(ValueError): 357 | xf() 358 | 359 | def test_can_xpartial_any_callable(self): 360 | class my_callable(object): 361 | def __call__(self, x): 362 | return "hello %s" % x 363 | 364 | f = xpartial(my_callable(), (X + "!")) 365 | assert f("x") == "hello x!" 366 | -------------------------------------------------------------------------------- /test_pipetools/test_utils.py: -------------------------------------------------------------------------------- 1 | from pipetools import X, sort_by, take_first, foreach, where, select_first, group_by 2 | from pipetools import unless, flatten, take_until, as_kwargs, drop_first, tee 3 | from pipetools.compat import range 4 | 5 | 6 | class TestPipeUtil: 7 | 8 | def test_pipability(self): 9 | f = range | foreach(X) | sum 10 | 11 | result = f(4) 12 | assert result == 6 13 | 14 | def test_input(self): 15 | result = range(5) > where(X % 2) | list 16 | assert result == [1, 3] 17 | 18 | 19 | class TestSortBy: 20 | 21 | def test_x(self): 22 | 23 | result = sort_by(-X[1])(zip('what', [1, 2, 3, 4])) 24 | 25 | assert result == [ 26 | ('t', 4), 27 | ('a', 3), 28 | ('h', 2), 29 | ('w', 1), 30 | ] 31 | 32 | def test_descending(self): 33 | 34 | result = zip('what', [1, 2, 3, 4]) > sort_by(X[1]).descending 35 | 36 | assert result == [ 37 | ('t', 4), 38 | ('a', 3), 39 | ('h', 2), 40 | ('w', 1), 41 | ] 42 | 43 | 44 | class TestTakeFirst: 45 | 46 | def test_take_first(self): 47 | assert [0, 1, 2] == list(take_first(3)(range(10))) 48 | 49 | 50 | class TestTupleMaker: 51 | 52 | def test_make_tuple(self): 53 | result = [1, 2, 3] > foreach((X, X % 2)) | list 54 | assert result == [(1, 1), (2, 0), (3, 1)] 55 | 56 | 57 | class TestListMaker: 58 | 59 | def test_make_list(self): 60 | result = [1, 2, 3] > foreach([X, X % 2]) | list 61 | assert result == [[1, 1], [2, 0], [3, 1]] 62 | 63 | 64 | class TestDictMaker: 65 | 66 | def test_make_dict(self): 67 | result = [1, 2] > foreach({'num': X, 'str': str}) | list 68 | assert result == [{'num': 1, 'str': '1'}, {'num': 2, 'str': '2'}] 69 | 70 | 71 | class TestSelectFirst: 72 | 73 | def test_select_first(self): 74 | result = select_first(X % 2 == 0)([3, 4, 5, 6]) 75 | assert result == 4 76 | 77 | def test_select_first_none(self): 78 | result = select_first(X == 2)([0, 1, 0, 1]) 79 | assert result is None 80 | 81 | def test_select_first_empty(self): 82 | assert select_first(X)([]) is None 83 | 84 | 85 | class TestAutoStringFormatter: 86 | 87 | def test_foreach_format(self): 88 | result = [1, 2] > foreach("Number {0}") | list 89 | assert result == ['Number 1', 'Number 2'] 90 | 91 | 92 | class TestUnless: 93 | 94 | def test_ok(self): 95 | f = unless(AttributeError, foreach(X.lower())) | list 96 | assert f("ABC") == ['a', 'b', 'c'] 97 | 98 | def test_with_exception(self): 99 | f = unless(AttributeError, foreach(X.lower()) | list) 100 | assert f(['A', 'B', 37]) is None 101 | 102 | def test_with_exception_in_foreach(self): 103 | f = foreach(unless(AttributeError, X.lower())) | list 104 | assert f(['A', 'B', 37]) == ['a', 'b', None] 105 | 106 | def test_partial_ok(self): 107 | f = unless(TypeError, enumerate, start=3) | list 108 | assert f('abc') == [(3, 'a'), (4, 'b'), (5, 'c')] 109 | 110 | def test_partial_exc(self): 111 | f = unless(TypeError, enumerate, start=3) 112 | assert f(42) is None 113 | 114 | def test_X_ok(self): 115 | f = unless(TypeError, X * 'x') 116 | assert f(3) == 'xxx' 117 | 118 | def test_X_exception(self): 119 | f = unless(TypeError, X * 'x') 120 | assert f('x') is None 121 | 122 | 123 | class TestFlatten: 124 | 125 | def test_flatten(self): 126 | assert (list(flatten([1, [2, 3], (4, ('five', 6))])) 127 | == [1, 2, 3, 4, 'five', 6]) 128 | 129 | def test_flatten_args(self): 130 | assert (list(flatten(1, [2, 3], (4, ('five', 6)))) 131 | == [1, 2, 3, 4, 'five', 6]) 132 | 133 | def test_flatten_dict(self): 134 | assert (list(flatten([[{'a': 1}], {'b': 2}, 'c'], {'d': 3})) 135 | == [{'a': 1}, {'b': 2}, 'c', {'d': 3}]) 136 | 137 | 138 | class TestTakeUntil: 139 | 140 | def test_basic(self): 141 | f = take_until(X > 5) 142 | assert list(f([1, 2, 3, 1, 6, 1, 3])) == [1, 2, 3, 1] 143 | 144 | def test_including(self): 145 | f = take_until(X > 5).including 146 | assert ([1, 2, 3, 1, 6, 1, 3] > f | list) == [1, 2, 3, 1, 6] 147 | 148 | def test_including_all(self): 149 | f = take_until(X > 50).including 150 | assert ([1, 2, 3, 1, 6, 1, 3] > f | list) == [1, 2, 3, 1, 6, 1, 3] 151 | 152 | 153 | class TestAsKwargs: 154 | 155 | def test_as_kwargs(self): 156 | d = {'foo': 4, 'bar': 2} 157 | assert as_kwargs(lambda **kw: kw)(d) == d 158 | 159 | 160 | class TestRegexCondidion: 161 | 162 | def test_where_regex(self): 163 | data = [ 164 | 'foo bar', 165 | 'boo far', 166 | 'foolproof', 167 | ] 168 | assert (data > where(r'^foo') | list) == [ 169 | 'foo bar', 170 | 'foolproof', 171 | ] 172 | 173 | def test_select_first_regex(self): 174 | data = [ 175 | 'foo bar', 176 | 'boo far', 177 | 'foolproof', 178 | ] 179 | assert (data > select_first(r'^b.*r$')) == 'boo far' 180 | 181 | def test_none_doesnt_match(self): 182 | data = [ 183 | 'foo bar', 184 | 'boo far', 185 | None, 186 | 'foolproof', 187 | ] 188 | assert (data > where(r'^foo') | list) == [ 189 | 'foo bar', 190 | 'foolproof', 191 | ] 192 | 193 | 194 | class TestGroupBy: 195 | 196 | def test_basic(self): 197 | src = [1, 2, 3, 4, 5, 6] 198 | assert (src > group_by(X % 2) | dict) == {0: [2, 4, 6], 1: [1, 3, 5]} 199 | 200 | 201 | class TestDropFirst: 202 | 203 | def test_list(self): 204 | src = [1, 2, 3, 4, 5, 6] 205 | assert (src > drop_first(3) | list) == [4, 5, 6] 206 | 207 | def test_iterable(self): 208 | assert (range(10000) > drop_first(9999) | list) == [9999] 209 | 210 | 211 | class TestTee: 212 | 213 | def test_tee(self): 214 | store = [] 215 | 216 | result = "input" > X | tee(X | reversed | "".join | store.append) | X[2:] 217 | 218 | assert store == ["tupni"] 219 | assert result == "put" 220 | --------------------------------------------------------------------------------