├── .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 |
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 |
--------------------------------------------------------------------------------