├── .editorconfig ├── .gitignore ├── .travis.yml ├── History.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── docs ├── Makefile ├── api.rst ├── conf.py ├── examples.rst ├── history.rst ├── index.rst ├── install.rst └── make.bat ├── examples ├── http_requests.py └── pipeline.py ├── paco ├── __init__.py ├── apply.py ├── assertions.py ├── compose.py ├── concurrent.py ├── constant.py ├── curry.py ├── decorator.py ├── defer.py ├── dropwhile.py ├── each.py ├── every.py ├── filter.py ├── filterfalse.py ├── flat_map.py ├── gather.py ├── generator.py ├── interval.py ├── map.py ├── observer.py ├── once.py ├── partial.py ├── pipe.py ├── race.py ├── reduce.py ├── repeat.py ├── run.py ├── series.py ├── some.py ├── throttle.py ├── thunk.py ├── timeout.py ├── times.py ├── until.py ├── wait.py ├── whilst.py └── wraps.py ├── requirements-dev.txt ├── setup.py ├── tests ├── __init__.py ├── apply_test.py ├── assertions_test.py ├── compose_test.py ├── concurrent_test.py ├── constant_test.py ├── curry_test.py ├── decorator_test.py ├── defer_test.py ├── dropwhile_test.py ├── each_test.py ├── every_test.py ├── filter_test.py ├── filterfalse_test.py ├── flat_map_test.py ├── gather_test.py ├── helpers.py ├── interval_test.py ├── map_test.py ├── observer_test.py ├── once_test.py ├── partial_test.py ├── pipe_test.py ├── race_test.py ├── reduce_test.py ├── repeat_test.py ├── run_test.py ├── series_test.py ├── some_test.py ├── throttle_test.py ├── thunk_test.py ├── timeout_test.py ├── times_test.py ├── until_test.py ├── wait_test.py ├── whilst_test.py └── wraps_test.py └── tox.ini /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{py,rst,txt}] 4 | indent_style = space 5 | indent_size = 4 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | end_of_line = LF 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | end_of_line = LF 14 | 15 | [Makefile] 16 | indent_style = tab 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | *.tar.gz 27 | 28 | # OS specific 29 | .DS_Store 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *,cover 50 | .hypothesis/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # IPython Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | venv/ 87 | ENV/ 88 | 89 | # Spyder project settings 90 | .spyderproject 91 | 92 | # Rope project settings 93 | .ropeproject 94 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "3.4" 5 | - "3.5" 6 | - "3.6" 7 | - "3.7-dev" 8 | - "3.8-dev" 9 | - "nightly" 10 | 11 | sudo: false 12 | 13 | matrix: 14 | allow_failures: 15 | - python: "3.7-dev" 16 | - python: "3.8-dev" 17 | - python: nightly 18 | 19 | install: 20 | - pip install -r requirements-dev.txt 21 | 22 | script: 23 | - make lint 24 | - make test 25 | - make coverage 26 | 27 | after_success: 28 | coveralls 29 | -------------------------------------------------------------------------------- /History.rst: -------------------------------------------------------------------------------- 1 | 2 | History 3 | ======= 4 | 5 | 0.2.4 / 2019-08-28 6 | ------------------ 7 | 8 | * Merge pull request #46 from lmicra/patch-1 9 | * Update map.py 10 | * Merge pull request #43 from dylanjw/fix_doc_typo 11 | * Fix typo: series, not searies 12 | 13 | v0.2.3 / 2018-10-23 14 | ------------------- 15 | 16 | * Merge pull request #42 from dylanjw/fix_syntax_error 17 | * Use getattr to avoid async keyword 18 | 19 | 0.2.2 / 2018-10-09 20 | ------------------ 21 | 22 | * Merge pull request #40 from thatmattbone/master 23 | * add loop param to paco.interval() 24 | * fix(setup): use space based indentation 25 | * fix(travis): use cpython 3.7-dev release 26 | * refactor(errors): use "paco" prefix in exception messages 27 | * chore(History): add version changes 28 | 29 | v0.2.1 / 2018-03-21 30 | ------------------- 31 | 32 | * fix(#37): allow overload function signatures with variadic arguments 33 | * refactor(timeout_test): remove print statement 34 | * fix(docs): bad link to API reference 35 | * refactor(docs): remove codesponsor 36 | 37 | v0.2.0 / 2017-10-21 38 | ------------------- 39 | 40 | * refactor(api): API breaking change that modifies behavior by raising any legit exception generated by a coroutine. 41 | * feat(examples): add examples file 42 | * feat(v2): v2 pre-release, propagate raise exception if return_exceptions is False 43 | * refactor(tests): add map error exception assertion test 44 | * Merge branch 'master' of https://github.com/h2non/paco 45 | * refactor(tests): add map error exception assertion test 46 | * feat(docs): add sponsor banner 47 | * feat(docs): add sponsor banner 48 | * feat(LICENSE): update copyright year 49 | * Update setup.py 50 | 51 | v0.1.11 / 2017-01-28 52 | -------------------- 53 | 54 | * feat(api): add ``paco.interval`` function. 55 | 56 | v0.1.10 / 2017-01-11 57 | -------------------- 58 | 59 | * fix(each.py,map.py): fixed `return_exceptions` kwarg 60 | * fix(setup.py): add author email 61 | * fix(Makefile): remove package file 62 | 63 | v0.1.9 / 2017-01-06 64 | ------------------- 65 | 66 | * feat(api): add identity function 67 | * feat(#31): add thunk function 68 | * feat(package): add wheel package distribution 69 | * refactor(wraps): simplify implementation 70 | * fix(History): remove indentation 71 | 72 | v0.1.8 / 2016-12-29 73 | ------------------- 74 | 75 | * feat(requirements): force upgrade setuptools 76 | * feat(#29): support async iterable and generators 77 | * fix(docs): link to examples 78 | * chore(travis): use Python 3.6 stable release 79 | 80 | 0.1.7 / 2016-12-18 81 | ------------------ 82 | 83 | * feat(#26): add curry function. 84 | 85 | 0.1.6 / 2016-12-11 86 | ------------------ 87 | 88 | * feat(pipe): isolate pipe operator overload code 89 | * refactor: decorator and util functions 90 | * feat(#11): timeout limit context manager. 91 | * refactor(core): several minor refactors 92 | * fix(docs): comment out latex sphinx settings 93 | * fix(docs): use current package version 94 | * Documentation examples improvements (#27) 95 | * feat(history): update 96 | * feat: add pool length magic method 97 | 98 | 0.1.5 (2016-12-04) 99 | ------------------ 100 | 101 | * fix(#25): allow empty iterables in iterators functions, such as ``map``, ``filter``, ``reduce``. 102 | 103 | 0.1.4 (2016-11-28) 104 | ------------------ 105 | 106 | * fix(#24): explicitly pass loop instance to ``asyncio.wait``. 107 | 108 | 0.1.3 (2016-10-27) 109 | ------------------ 110 | 111 | * feat(#17): add ``flat_map`` function. 112 | * feat(#18): add pipeline-style operator overloading composition. 113 | 114 | 0.1.2 (2016-10-25) 115 | ------------------ 116 | 117 | * fix(setup.py): fix pip installation. 118 | * refactor(api): minor refactors in several functions and tests. 119 | 120 | 0.1.1 (2016-10-24) 121 | ------------------ 122 | 123 | * refactor(name): use new project name. 124 | 125 | 0.1.0 (2016-10-23) 126 | ------------------ 127 | 128 | * First version (beta) 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2017 Tomás Aparicio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst LICENSE History.rst requirements-dev.txt 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | OK_COLOR=\033[32;01m 2 | NO_COLOR=\033[0m 3 | 4 | all: test 5 | 6 | export PYTHONPATH:=${PWD} 7 | version=`python -c 'import paco; print(paco.__version__)'` 8 | filename=paco-`python -c 'import paco; print(paco.__version__)'`.tar.gz 9 | 10 | apidocs: 11 | @sphinx-apidoc -f --follow-links -H "API documentation" -o docs/source paco 12 | 13 | htmldocs: 14 | @rm -rf docs/_build 15 | $(MAKE) -C docs html 16 | 17 | lint: 18 | @echo "$(OK_COLOR)==> Linting code...$(NO_COLOR)" 19 | @flake8 --exclude examples . 20 | 21 | test: clean lint 22 | @echo "$(OK_COLOR)==> Runnings tests...$(NO_COLOR)" 23 | @py.test -s -v --capture=sys --cov paco --cov-report term-missing 24 | 25 | coverage: 26 | @coverage run --source paco -m py.test 27 | @coverage report 28 | 29 | bump: 30 | @bumpversion --commit --tag --current-version $(version) patch paco/__init__.py --allow-dirty 31 | 32 | bump-minor: 33 | @bumpversion --commit --tag --current-version $(version) minor paco/__init__.py --allow-dirty 34 | 35 | push-tag: 36 | @echo "$(OK_COLOR)==> Pushing tag to remote...$(NO_COLOR)" 37 | @git push origin "v$(version)" 38 | 39 | clean: 40 | @echo "$(OK_COLOR)==> Cleaning up files that are already in .gitignore...$(NO_COLOR)" 41 | @for pattern in `cat .gitignore`; do find . -name "$$pattern" -delete; done 42 | 43 | release: clean bump push-tag publish 44 | @echo "$(OK_COLOR)==> Done! $(NO_COLOR)" 45 | 46 | publish: 47 | @echo "$(OK_COLOR)==> Releasing package ...$(NO_COLOR)" 48 | @python setup.py register 49 | @python setup.py sdist upload 50 | @python setup.py bdist_wheel --universal upload 51 | @rm -fr build dist .egg paco.egg-info 52 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | paco |PyPI| |Coverage Status| |Documentation Status| |Stability| |Quality| |Versions| 2 | ===================================================================================== 3 | 4 | Small and idiomatic utility library for coroutine-driven asynchronous generic programming in Python. 5 | 6 | Built on top of `asyncio`_, ``paco`` provides missing capabilities from Python `stdlib` 7 | in order to write asynchronous cooperative multitasking in a nice-ish way. 8 | Also, paco aims to port some of `functools`_ and `itertools`_ standard functions to the asynchronous world. 9 | 10 | ``paco`` can be your utility belt to deal with asynchronous, I/O-bound, non-blocking concurrent code in a cleaner and idiomatic way. 11 | 12 | Features 13 | -------- 14 | 15 | - Simple and idiomatic API, extending Python ``stdlib`` with async coroutines gotchas. 16 | - Built-in configurable control-flow concurrency support (throttle). 17 | - No fancy abstractions: it just works with the plain asynchronous coroutines. 18 | - Useful iterables, decorators, functors and convenient helpers. 19 | - Coroutine-based functional helpers: ``compose``, ``throttle``, ``partial``, ``timeout``, ``times``, ``until``, ``race``... 20 | - Asynchronous coroutines port of Python built-in functions: `filter`, `map`, `dropwhile`, `filterfalse`, `reduce`... 21 | - Supports asynchronous iterables and generators (`PEP0525`_) 22 | - Concurrent iterables and higher-order functions. 23 | - Better ``asyncio.gather()`` and ``asyncio.wait()`` with optional concurrency control and ordered results. 24 | - Works with both `async/await`_ and `yield from`_ coroutines syntax. 25 | - Reliable coroutine timeout limit handler via context manager. 26 | - Designed for intensive I/O bound concurrent non-blocking tasks. 27 | - Good interoperability with ``asyncio`` and Python ``stdlib`` functions. 28 | - `Composable pipelines`_ of functors via ``|`` operator overloading. 29 | - Small and dependency free. 30 | - Compatible with Python +3.4. 31 | 32 | Installation 33 | ------------ 34 | 35 | Using ``pip`` package manager: 36 | 37 | .. code-block:: bash 38 | 39 | pip install --upgrade paco 40 | 41 | Or install the latest sources from Github: 42 | 43 | .. code-block:: bash 44 | 45 | pip install -e git+git://github.com/h2non/paco.git#egg=paco 46 | 47 | 48 | API 49 | --- 50 | 51 | - paco.ConcurrentExecutor_ 52 | - paco.apply_ 53 | - paco.compose_ 54 | - paco.concurrent_ 55 | - paco.constant_ 56 | - paco.curry_ 57 | - paco.defer_ 58 | - paco.dropwhile_ 59 | - paco.each_ 60 | - paco.every_ 61 | - paco.filter_ 62 | - paco.filterfalse_ 63 | - paco.flat_map_ 64 | - paco.gather_ 65 | - paco.identity_ 66 | - paco.interval_ 67 | - paco.map_ 68 | - paco.once_ 69 | - paco.partial_ 70 | - paco.race_ 71 | - paco.reduce_ 72 | - paco.repeat_ 73 | - paco.run_ 74 | - paco.series_ 75 | - paco.some_ 76 | - paco.throttle_ 77 | - paco.thunk_ 78 | - paco.timeout_ 79 | - paco.TimeoutLimit_ 80 | - paco.times_ 81 | - paco.until_ 82 | - paco.wait_ 83 | - paco.whilst_ 84 | - paco.wraps_ 85 | 86 | 87 | .. _paco.ConcurrentExecutor: http://paco.readthedocs.io/en/latest/api.html#paco.ConcurrentExecutor 88 | .. _paco.apply: http://paco.readthedocs.io/en/latest/api.html#paco.apply 89 | .. _paco.compose: http://paco.readthedocs.io/en/latest/api.html#paco.compose 90 | .. _paco.concurrent: http://paco.readthedocs.io/en/latest/api.html#paco.concurrent 91 | .. _paco.constant: http://paco.readthedocs.io/en/latest/api.html#paco.constant 92 | .. _paco.curry: http://paco.readthedocs.io/en/latest/api.html#paco.curry 93 | .. _paco.defer: http://paco.readthedocs.io/en/latest/api.html#paco.defer 94 | .. _paco.dropwhile: http://paco.readthedocs.io/en/latest/api.html#paco.dropwhile 95 | .. _paco.each: http://paco.readthedocs.io/en/latest/api.html#paco.each 96 | .. _paco.every: http://paco.readthedocs.io/en/latest/api.html#paco.every 97 | .. _paco.filter: http://paco.readthedocs.io/en/latest/api.html#paco.filter 98 | .. _paco.filterfalse: http://paco.readthedocs.io/en/latest/api.html#paco.filterfalse 99 | .. _paco.flat_map: http://paco.readthedocs.io/en/latest/api.html#paco.flat_map 100 | .. _paco.gather: http://paco.readthedocs.io/en/latest/api.html#paco.gather 101 | .. _paco.identity: http://paco.readthedocs.io/en/latest/api.html#paco.identity 102 | .. _paco.interval: http://paco.readthedocs.io/en/latest/api.html#paco.interval 103 | .. _paco.map: http://paco.readthedocs.io/en/latest/api.html#paco.map 104 | .. _paco.once: http://paco.readthedocs.io/en/latest/api.html#paco.once 105 | .. _paco.partial: http://paco.readthedocs.io/en/latest/api.html#paco.partial 106 | .. _paco.race: http://paco.readthedocs.io/en/latest/api.html#paco.race 107 | .. _paco.reduce: http://paco.readthedocs.io/en/latest/api.html#paco.reduce 108 | .. _paco.repeat: http://paco.readthedocs.io/en/latest/api.html#paco.repeat 109 | .. _paco.run: http://paco.readthedocs.io/en/latest/api.html#paco.run 110 | .. _paco.series: http://paco.readthedocs.io/en/latest/api.html#paco.series 111 | .. _paco.some: http://paco.readthedocs.io/en/latest/api.html#paco.some 112 | .. _paco.throttle: http://paco.readthedocs.io/en/latest/api.html#paco.throttle 113 | .. _paco.thunk: http://paco.readthedocs.io/en/latest/api.html#paco.thunk 114 | .. _paco.timeout: http://paco.readthedocs.io/en/latest/api.html#paco.timeout 115 | .. _paco.TimeoutLimit: http://paco.readthedocs.io/en/latest/api.html#paco.TimeoutLimit 116 | .. _paco.times: http://paco.readthedocs.io/en/latest/api.html#paco.times 117 | .. _paco.until: http://paco.readthedocs.io/en/latest/api.html#paco.until 118 | .. _paco.wait: http://paco.readthedocs.io/en/latest/api.html#paco.wait 119 | .. _paco.whilst: http://paco.readthedocs.io/en/latest/api.html#paco.whilst 120 | .. _paco.wraps: http://paco.readthedocs.io/en/latest/api.html#paco.wraps 121 | 122 | 123 | Examples 124 | ^^^^^^^^ 125 | 126 | Asynchronously and concurrently execute multiple HTTP requests. 127 | 128 | .. code-block:: python 129 | 130 | import paco 131 | import aiohttp 132 | 133 | async def fetch(url): 134 | async with aiohttp.ClientSession() as session: 135 | async with session.get(url) as res: 136 | return res 137 | 138 | async def fetch_urls(): 139 | urls = [ 140 | 'https://www.google.com', 141 | 'https://www.yahoo.com', 142 | 'https://www.bing.com', 143 | 'https://www.baidu.com', 144 | 'https://duckduckgo.com', 145 | ] 146 | 147 | # Map concurrent executor with concurrent limit of 3 148 | responses = await paco.map(fetch, urls, limit=3) 149 | 150 | for res in responses: 151 | print('Status:', res.status) 152 | 153 | # Run in event loop 154 | paco.run(fetch_urls()) 155 | 156 | 157 | 158 | Concurrent pipeline-style composition of transform functors over an iterable object. 159 | 160 | .. code-block:: python 161 | 162 | import paco 163 | 164 | async def filterer(x): 165 | return x < 8 166 | 167 | async def mapper(x): 168 | return x * 2 169 | 170 | async def drop(x): 171 | return x < 10 172 | 173 | async def reducer(acc, x): 174 | return acc + x 175 | 176 | async def task(numbers): 177 | return await (numbers 178 | | paco.filter(filterer) 179 | | paco.map(mapper) 180 | | paco.dropwhile(drop) 181 | | paco.reduce(reducer, initializer=0)) 182 | 183 | # Run in event loop 184 | number = paco.run(task((1, 2, 3, 4, 5, 6, 7, 8, 9, 10))) 185 | print('Number:', number) # => 36 186 | 187 | License 188 | ------- 189 | 190 | MIT - Tomas Aparicio 191 | 192 | .. _asynchronous: http://python.org 193 | .. _asyncio: https://docs.python.org/3.5/library/asyncio.html 194 | .. _Python: http://python.org 195 | .. _annotated API reference: https://h2non.github.io/paco 196 | .. _async/await: https://www.python.org/dev/peps/pep-0492/ 197 | .. _yield from: https://www.python.org/dev/peps/pep-0380/ 198 | .. _Composable pipelines: #examples 199 | .. _itertools: https://docs.python.org/3/library/itertools.html 200 | .. _functools: https://docs.python.org/3/library/functools.html 201 | .. _PEP0525: https://www.python.org/dev/peps/pep-0525/ 202 | 203 | .. |PyPI| image:: https://img.shields.io/pypi/v/paco.svg?maxAge=2592000?style=flat-square 204 | :target: https://pypi.python.org/pypi/paco 205 | .. |Coverage Status| image:: https://coveralls.io/repos/github/h2non/paco/badge.svg?branch=master 206 | :target: https://coveralls.io/github/h2non/paco?branch=master 207 | .. |Documentation Status| image:: https://img.shields.io/badge/docs-latest-green.svg?style=flat 208 | :target: http://paco.readthedocs.io/en/latest/?badge=latest 209 | .. |Quality| image:: https://codeclimate.com/github/h2non/paco/badges/gpa.svg 210 | :target: https://codeclimate.com/github/h2non/paco 211 | .. |Stability| image:: https://img.shields.io/pypi/status/paco.svg 212 | :target: https://pypi.python.org/pypi/paco 213 | .. |Versions| image:: https://img.shields.io/pypi/pyversions/paco.svg 214 | :target: https://pypi.python.org/pypi/paco 215 | -------------------------------------------------------------------------------- /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) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help 18 | help: 19 | @echo "Please use \`make ' where is one of" 20 | @echo " html to make standalone HTML files" 21 | @echo " dirhtml to make HTML files named index.html in directories" 22 | @echo " singlehtml to make a single large HTML file" 23 | @echo " pickle to make pickle files" 24 | @echo " json to make JSON files" 25 | @echo " htmlhelp to make HTML files and a HTML help project" 26 | @echo " qthelp to make HTML files and a qthelp project" 27 | @echo " applehelp to make an Apple Help Book" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " epub3 to make an epub3" 31 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 32 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 33 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 34 | @echo " text to make text files" 35 | @echo " man to make manual pages" 36 | @echo " texinfo to make Texinfo files" 37 | @echo " info to make Texinfo files and run them through makeinfo" 38 | @echo " gettext to make PO message catalogs" 39 | @echo " changes to make an overview of all changed/added/deprecated items" 40 | @echo " xml to make Docutils-native XML files" 41 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 42 | @echo " linkcheck to check all external links for integrity" 43 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 44 | @echo " coverage to run coverage check of the documentation (if enabled)" 45 | @echo " dummy to check syntax errors of document sources" 46 | 47 | .PHONY: clean 48 | clean: 49 | rm -rf $(BUILDDIR)/* 50 | 51 | .PHONY: html 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | .PHONY: dirhtml 58 | dirhtml: 59 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 60 | @echo 61 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 62 | 63 | .PHONY: singlehtml 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | .PHONY: pickle 70 | pickle: 71 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 72 | @echo 73 | @echo "Build finished; now you can process the pickle files." 74 | 75 | .PHONY: json 76 | json: 77 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 78 | @echo 79 | @echo "Build finished; now you can process the JSON files." 80 | 81 | .PHONY: htmlhelp 82 | htmlhelp: 83 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 84 | @echo 85 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 86 | ".hhp project file in $(BUILDDIR)/htmlhelp." 87 | 88 | .PHONY: qthelp 89 | qthelp: 90 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 91 | @echo 92 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 93 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 94 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pook`.qhcp" 95 | @echo "To view the help file:" 96 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pook`.qhc" 97 | 98 | .PHONY: applehelp 99 | applehelp: 100 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 101 | @echo 102 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 103 | @echo "N.B. You won't be able to view it unless you put it in" \ 104 | "~/Library/Documentation/Help or install it in your application" \ 105 | "bundle." 106 | 107 | .PHONY: devhelp 108 | devhelp: 109 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 110 | @echo 111 | @echo "Build finished." 112 | @echo "To view the help file:" 113 | @echo "# mkdir -p $$HOME/.local/share/devhelp/pook`" 114 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pook`" 115 | @echo "# devhelp" 116 | 117 | .PHONY: epub 118 | epub: 119 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 120 | @echo 121 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 122 | 123 | .PHONY: epub3 124 | epub3: 125 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 126 | @echo 127 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 128 | 129 | .PHONY: latex 130 | latex: 131 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 132 | @echo 133 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 134 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 135 | "(use \`make latexpdf' here to do that automatically)." 136 | 137 | .PHONY: latexpdf 138 | latexpdf: 139 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 140 | @echo "Running LaTeX files through pdflatex..." 141 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 142 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 143 | 144 | .PHONY: latexpdfja 145 | latexpdfja: 146 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 147 | @echo "Running LaTeX files through platex and dvipdfmx..." 148 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 149 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 150 | 151 | .PHONY: text 152 | text: 153 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 154 | @echo 155 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 156 | 157 | .PHONY: man 158 | man: 159 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 160 | @echo 161 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 162 | 163 | .PHONY: texinfo 164 | texinfo: 165 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 166 | @echo 167 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 168 | @echo "Run \`make' in that directory to run these through makeinfo" \ 169 | "(use \`make info' here to do that automatically)." 170 | 171 | .PHONY: info 172 | info: 173 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 174 | @echo "Running Texinfo files through makeinfo..." 175 | make -C $(BUILDDIR)/texinfo info 176 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 177 | 178 | .PHONY: gettext 179 | gettext: 180 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 181 | @echo 182 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 183 | 184 | .PHONY: changes 185 | changes: 186 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 187 | @echo 188 | @echo "The overview file is in $(BUILDDIR)/changes." 189 | 190 | .PHONY: linkcheck 191 | linkcheck: 192 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 193 | @echo 194 | @echo "Link check complete; look for any errors in the above output " \ 195 | "or in $(BUILDDIR)/linkcheck/output.txt." 196 | 197 | .PHONY: doctest 198 | doctest: 199 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 200 | @echo "Testing of doctests in the sources finished, look at the " \ 201 | "results in $(BUILDDIR)/doctest/output.txt." 202 | 203 | .PHONY: coverage 204 | coverage: 205 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 206 | @echo "Testing of coverage in the sources finished, look at the " \ 207 | "results in $(BUILDDIR)/coverage/python.txt." 208 | 209 | .PHONY: xml 210 | xml: 211 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 212 | @echo 213 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 214 | 215 | .PHONY: pseudoxml 216 | pseudoxml: 217 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 218 | @echo 219 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 220 | 221 | .PHONY: dummy 222 | dummy: 223 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy 224 | @echo 225 | @echo "Build finished. Dummy builder generates no files." 226 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API documentation 2 | ================= 3 | 4 | .. toctree:: 5 | 6 | paco.ConcurrentExecutor 7 | paco.apply 8 | paco.compose 9 | paco.concurrent 10 | paco.constant 11 | paco.curry 12 | paco.defer 13 | paco.dropwhile 14 | paco.each 15 | paco.every 16 | paco.filter 17 | paco.filterfalse 18 | paco.flat_map 19 | paco.gather 20 | paco.identity 21 | paco.interval 22 | paco.map 23 | paco.once 24 | paco.partial 25 | paco.race 26 | paco.reduce 27 | paco.repeat 28 | paco.run 29 | paco.series 30 | paco.some 31 | paco.throttle 32 | paco.thunk 33 | paco.timeout 34 | paco.TimeoutLimit 35 | paco.times 36 | paco.until 37 | paco.wait 38 | paco.whilst 39 | paco.wraps 40 | 41 | .. automodule:: paco 42 | :members: 43 | :undoc-members: 44 | :show-inheritance: 45 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import sys 4 | import sphinx_rtd_theme 5 | 6 | sys.path.insert(0, os.path.abspath('..')) 7 | import paco # noqa 8 | 9 | # -*- coding: utf-8 -*- 10 | # 11 | # documentation build configuration file, created by 12 | # sphinx-quickstart on Tue Oct 4 18:59:54 2016. 13 | # 14 | # This file is execfile()d with the current directory set to its 15 | # containing dir. 16 | # 17 | # Note that not all possible configuration values are present in this 18 | # autogenerated file. 19 | # 20 | # All configuration values have a default; values that are commented out 21 | # serve to show the default. 22 | 23 | # If extensions (or modules to document with autodoc) are in another directory, 24 | # add these directories to sys.path here. If the directory is relative to the 25 | # documentation root, use os.path.abspath to make it absolute, like shown here. 26 | # 27 | # import os 28 | # import sys 29 | # sys.path.insert(0, os.path.abspath('.')) 30 | 31 | # -- General configuration ------------------------------------------------ 32 | 33 | # If your documentation needs a minimal Sphinx version, state it here. 34 | # 35 | # needs_sphinx = '1.0' 36 | 37 | # Add any Sphinx extension module names here, as strings. They can be 38 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 39 | # ones. 40 | extensions = [ 41 | 'sphinx.ext.autodoc', 42 | 'sphinx.ext.doctest', 43 | 'sphinx.ext.intersphinx', 44 | 'sphinx.ext.viewcode', 45 | 'sphinx.ext.napoleon' 46 | ] 47 | 48 | # Napoleon settings 49 | napoleon_google_docstring = True 50 | napoleon_numpy_docstring = False 51 | napoleon_include_private_with_doc = False 52 | napoleon_include_special_with_doc = True 53 | napoleon_use_admonition_for_examples = False 54 | napoleon_use_admonition_for_notes = False 55 | napoleon_use_admonition_for_references = False 56 | napoleon_use_ivar = False 57 | napoleon_use_param = True 58 | napoleon_use_rtype = True 59 | 60 | # Add any paths that contain templates here, relative to this directory. 61 | templates_path = ['_templates'] 62 | 63 | # The suffix(es) of source filenames. 64 | # You can specify multiple suffix as a list of string: 65 | # 66 | # source_suffix = ['.rst', '.md'] 67 | source_suffix = '.rst' 68 | 69 | # The encoding of source files. 70 | # 71 | # source_encoding = 'utf-8-sig' 72 | 73 | # The master toctree document. 74 | master_doc = 'index' 75 | 76 | # General information about the project. 77 | project = 'paco' 78 | copyright = '2016, Tomas Aparicio' 79 | author = 'Tomas Aparicio' 80 | 81 | # The version info for the project you're documenting, acts as replacement for 82 | # |version| and |release|, also used in various other places throughout the 83 | # built documents. 84 | # 85 | # The short X.Y version. 86 | version = paco.__version__ 87 | # The full version, including alpha/beta/rc tags. 88 | release = paco.__version__ 89 | 90 | # The language for content autogenerated by Sphinx. Refer to documentation 91 | # for a list of supported languages. 92 | # 93 | # This is also used if you do content translation via gettext catalogs. 94 | # Usually you set "language" from the command line for these cases. 95 | language = None 96 | 97 | # There are two options for replacing |today|: either, you set today to some 98 | # non-false value, then it is used: 99 | # 100 | # today = '' 101 | # 102 | # Else, today_fmt is used as the format for a strftime call. 103 | # 104 | # today_fmt = '%B %d, %Y' 105 | 106 | # List of patterns, relative to source directory, that match files and 107 | # directories to ignore when looking for source files. 108 | # This patterns also effect to html_static_path and html_extra_path 109 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 110 | 111 | # The reST default role (used for this markup: `text`) to use for all 112 | # documents. 113 | # 114 | # default_role = None 115 | 116 | # If true, '()' will be appended to :func: etc. cross-reference text. 117 | # 118 | # add_function_parentheses = True 119 | 120 | # If true, the current module name will be prepended to all description 121 | # unit titles (such as .. function::). 122 | # 123 | # add_module_names = True 124 | 125 | # If true, sectionauthor and moduleauthor directives will be shown in the 126 | # output. They are ignored by default. 127 | # 128 | # show_authors = False 129 | 130 | # The name of the Pygments (syntax highlighting) style to use. 131 | pygments_style = 'sphinx' 132 | 133 | # A list of ignored prefixes for module index sorting. 134 | # modindex_common_prefix = [] 135 | 136 | # If true, keep warnings as "system message" paragraphs in the built documents. 137 | # keep_warnings = False 138 | 139 | # If true, `todo` and `todoList` produce output, else they produce nothing. 140 | todo_include_todos = False 141 | 142 | 143 | # -- Options for HTML output ---------------------------------------------- 144 | 145 | # The theme to use for HTML and HTML Help pages. See the documentation for 146 | # a list of builtin themes. 147 | # 148 | # html_theme = 'alabaster' 149 | html_theme = 'sphinx_rtd_theme' 150 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 151 | 152 | # Theme options are theme-specific and customize the look and feel of a theme 153 | # further. For a list of options available for each theme, see the 154 | # documentation. 155 | # 156 | # html_theme_options = {} 157 | 158 | # Add any paths that contain custom themes here, relative to this directory. 159 | # html_theme_path = [] 160 | 161 | # The name for this set of Sphinx documents. 162 | # " v documentation" by default. 163 | # 164 | # html_title = ' v0.1.0' 165 | 166 | # A shorter title for the navigation bar. Default is the same as html_title. 167 | # 168 | # html_short_title = None 169 | 170 | # The name of an image file (relative to this directory) to place at the top 171 | # of the sidebar. 172 | # 173 | # html_logo = None 174 | 175 | # The name of an image file (relative to this directory) to use as a favicon of 176 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 177 | # 32x32 pixels large. 178 | # 179 | # html_favicon = None 180 | 181 | # Add any paths that contain custom static files (such as style sheets) here, 182 | # relative to this directory. They are copied after the builtin static files, 183 | # so a file named "default.css" will overwrite the builtin "default.css". 184 | html_static_path = ['_static'] 185 | 186 | # Add any extra paths that contain custom files (such as robots.txt or 187 | # .htaccess) here, relative to this directory. These files are copied 188 | # directly to the root of the documentation. 189 | # 190 | # html_extra_path = [] 191 | 192 | # If not None, a 'Last updated on:' timestamp is inserted at every page 193 | # bottom, using the given strftime format. 194 | # The empty string is equivalent to '%b %d, %Y'. 195 | # 196 | # html_last_updated_fmt = None 197 | 198 | # If true, SmartyPants will be used to convert quotes and dashes to 199 | # typographically correct entities. 200 | # 201 | # html_use_smartypants = True 202 | 203 | # Custom sidebar templates, maps document names to template names. 204 | # 205 | # html_sidebars = {} 206 | 207 | # Additional templates that should be rendered to pages, maps page names to 208 | # template names. 209 | # 210 | # html_additional_pages = {} 211 | 212 | # If false, no module index is generated. 213 | # 214 | # html_domain_indices = True 215 | 216 | # If false, no index is generated. 217 | # 218 | # html_use_index = True 219 | 220 | # If true, the index is split into individual pages for each letter. 221 | # 222 | # html_split_index = False 223 | 224 | # If true, links to the reST sources are added to the pages. 225 | # 226 | # html_show_sourcelink = True 227 | 228 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 229 | # 230 | # html_show_sphinx = True 231 | 232 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 233 | # 234 | # html_show_copyright = True 235 | 236 | # If true, an OpenSearch description file will be output, and all pages will 237 | # contain a tag referring to it. The value of this option must be the 238 | # base URL from which the finished HTML is served. 239 | # 240 | # html_use_opensearch = '' 241 | 242 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 243 | # html_file_suffix = None 244 | 245 | # Language to be used for generating the HTML full-text search index. 246 | # Sphinx supports the following languages: 247 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 248 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' 249 | # 250 | # html_search_language = 'en' 251 | 252 | # A dictionary with options for the search language support, empty by default. 253 | # 'ja' uses this config value. 254 | # 'zh' user can custom change `jieba` dictionary path. 255 | # 256 | # html_search_options = {'type': 'default'} 257 | 258 | # The name of a javascript file (relative to the configuration directory) that 259 | # implements a search results scorer. If empty, the default will be used. 260 | # 261 | # html_search_scorer = 'scorer.js' 262 | 263 | # Output file base name for HTML help builder. 264 | htmlhelp_basename = 'doc' 265 | 266 | # -- Options for LaTeX output --------------------------------------------- 267 | 268 | # latex_elements = { 269 | # # The paper size ('letterpaper' or 'a4paper'). 270 | # # 271 | # # 'papersize': 'letterpaper', 272 | # 273 | # # The font size ('10pt', '11pt' or '12pt'). 274 | # # 275 | # # 'pointsize': '10pt', 276 | # 277 | # # Additional stuff for the LaTeX preamble. 278 | # # 279 | # # 'preamble': '', 280 | # 281 | # # Latex figure (float) alignment 282 | # # 283 | # # 'figure_align': 'htbp', 284 | # } 285 | 286 | # Grouping the document tree into LaTeX files. List of tuples 287 | # (source start file, target name, title, 288 | # author, documentclass [howto, manual, or own class]). 289 | # latex_documents = [ 290 | # (master_doc, '.tex', ' Documentation', 291 | # 'Tomas Aparicio', 'manual'), 292 | # ] 293 | 294 | # The name of an image file (relative to this directory) to place at the top of 295 | # the title page. 296 | # 297 | # latex_logo = None 298 | 299 | # For "manual" documents, if this is true, then toplevel headings are parts, 300 | # not chapters. 301 | # 302 | # latex_use_parts = False 303 | 304 | # If true, show page references after internal links. 305 | # 306 | # latex_show_pagerefs = False 307 | 308 | # If true, show URL addresses after external links. 309 | # 310 | # latex_show_urls = False 311 | 312 | # Documents to append as an appendix to all manuals. 313 | # 314 | # latex_appendices = [] 315 | 316 | # It false, will not define \strong, \code, itleref, \crossref ... but only 317 | # \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added 318 | # packages. 319 | # 320 | # latex_keep_old_macro_names = True 321 | 322 | # If false, no module index is generated. 323 | # 324 | # latex_domain_indices = True 325 | 326 | 327 | # -- Options for manual page output --------------------------------------- 328 | 329 | # One entry per manual page. List of tuples 330 | # (source start file, name, description, authors, manual section). 331 | man_pages = [ 332 | (master_doc, 'paco', ' Documentation', 333 | [author], 1) 334 | ] 335 | 336 | # If true, show URL addresses after external links. 337 | # 338 | # man_show_urls = False 339 | 340 | 341 | # -- Options for Texinfo output ------------------------------------------- 342 | 343 | # Grouping the document tree into Texinfo files. List of tuples 344 | # (source start file, target name, title, author, 345 | # dir menu entry, description, category) 346 | texinfo_documents = [ 347 | (master_doc, 'paco', ' Documentation', 348 | author, '', 'One line description of project.', 349 | 'Miscellaneous'), 350 | ] 351 | 352 | # Documents to append as an appendix to all manuals. 353 | # 354 | # texinfo_appendices = [] 355 | 356 | # If false, no module index is generated. 357 | # 358 | # texinfo_domain_indices = True 359 | 360 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 361 | # 362 | # texinfo_show_urls = 'footnote' 363 | 364 | # If true, do not generate a @detailmenu in the "Top" node's menu. 365 | # 366 | # texinfo_no_detailmenu = False 367 | 368 | # Example configuration for intersphinx: refer to the Python standard library. 369 | intersphinx_mapping = {'http://docs.python.org/': None} 370 | -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | Examples 2 | -------- 3 | 4 | 5 | Asynchronously and concurrently execute multiple HTTP requests. 6 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 7 | 8 | .. literalinclude:: ../examples/http_requests.py 9 | 10 | 11 | Concurrent pipeline-style chain composition of functors over any iterable object. 12 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 13 | 14 | .. literalinclude:: ../examples/pipeline.py 15 | -------------------------------------------------------------------------------- /docs/history.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../History.rst 2 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. pook documentation master file, created by 2 | sphinx-quickstart on Tue Oct 4 18:59:54 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. include:: ../README.rst 7 | 8 | Contents 9 | -------- 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | examples 15 | api 16 | history 17 | 18 | 19 | Indices and tables 20 | ================== 21 | 22 | * :ref:`genindex` 23 | * :ref:`modindex` 24 | * :ref:`search` 25 | -------------------------------------------------------------------------------- /docs/install.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | PyPI 5 | ---- 6 | 7 | You can install the last stable release from PyPI using pip:: 8 | 9 | $ pip install paco 10 | 11 | GitHub 12 | ------ 13 | 14 | Or install the latest sources from Github:: 15 | 16 | $ pip install -e git+git://github.com/h2non/paco.git#egg= 17 | 18 | Also you can donwload a source code package from `Github `_ and install it using setuptools:: 19 | 20 | $ tar xvf -{version}.tar.gz 21 | $ cd 22 | $ python setup.py install 23 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 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. epub3 to make an epub3 31 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 32 | echo. text to make text files 33 | echo. man to make manual pages 34 | echo. texinfo to make Texinfo files 35 | echo. gettext to make PO message catalogs 36 | echo. changes to make an overview over all changed/added/deprecated items 37 | echo. xml to make Docutils-native XML files 38 | echo. pseudoxml to make pseudoxml-XML files for display purposes 39 | echo. linkcheck to check all external links for integrity 40 | echo. doctest to run all doctests embedded in the documentation if enabled 41 | echo. coverage to run coverage check of the documentation if enabled 42 | echo. dummy to check syntax errors of document sources 43 | goto end 44 | ) 45 | 46 | if "%1" == "clean" ( 47 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 48 | del /q /s %BUILDDIR%\* 49 | goto end 50 | ) 51 | 52 | 53 | REM Check if sphinx-build is available and fallback to Python version if any 54 | %SPHINXBUILD% 1>NUL 2>NUL 55 | if errorlevel 9009 goto sphinx_python 56 | goto sphinx_ok 57 | 58 | :sphinx_python 59 | 60 | set SPHINXBUILD=python -m sphinx.__init__ 61 | %SPHINXBUILD% 2> nul 62 | if errorlevel 9009 ( 63 | echo. 64 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 65 | echo.installed, then set the SPHINXBUILD environment variable to point 66 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 67 | echo.may add the Sphinx directory to PATH. 68 | echo. 69 | echo.If you don't have Sphinx installed, grab it from 70 | echo.http://sphinx-doc.org/ 71 | exit /b 1 72 | ) 73 | 74 | :sphinx_ok 75 | 76 | 77 | if "%1" == "html" ( 78 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 79 | if errorlevel 1 exit /b 1 80 | echo. 81 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 82 | goto end 83 | ) 84 | 85 | if "%1" == "dirhtml" ( 86 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 87 | if errorlevel 1 exit /b 1 88 | echo. 89 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 90 | goto end 91 | ) 92 | 93 | if "%1" == "singlehtml" ( 94 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 95 | if errorlevel 1 exit /b 1 96 | echo. 97 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 98 | goto end 99 | ) 100 | 101 | if "%1" == "pickle" ( 102 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 103 | if errorlevel 1 exit /b 1 104 | echo. 105 | echo.Build finished; now you can process the pickle files. 106 | goto end 107 | ) 108 | 109 | if "%1" == "json" ( 110 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 111 | if errorlevel 1 exit /b 1 112 | echo. 113 | echo.Build finished; now you can process the JSON files. 114 | goto end 115 | ) 116 | 117 | if "%1" == "htmlhelp" ( 118 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 119 | if errorlevel 1 exit /b 1 120 | echo. 121 | echo.Build finished; now you can run HTML Help Workshop with the ^ 122 | .hhp project file in %BUILDDIR%/htmlhelp. 123 | goto end 124 | ) 125 | 126 | if "%1" == "qthelp" ( 127 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 128 | if errorlevel 1 exit /b 1 129 | echo. 130 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 131 | .qhcp project file in %BUILDDIR%/qthelp, like this: 132 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\pook.qhcp 133 | echo.To view the help file: 134 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\pook.ghc 135 | goto end 136 | ) 137 | 138 | if "%1" == "devhelp" ( 139 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 140 | if errorlevel 1 exit /b 1 141 | echo. 142 | echo.Build finished. 143 | goto end 144 | ) 145 | 146 | if "%1" == "epub" ( 147 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 148 | if errorlevel 1 exit /b 1 149 | echo. 150 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 151 | goto end 152 | ) 153 | 154 | if "%1" == "epub3" ( 155 | %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 156 | if errorlevel 1 exit /b 1 157 | echo. 158 | echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. 159 | goto end 160 | ) 161 | 162 | if "%1" == "latex" ( 163 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 164 | if errorlevel 1 exit /b 1 165 | echo. 166 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdf" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "latexpdfja" ( 181 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 182 | cd %BUILDDIR%/latex 183 | make all-pdf-ja 184 | cd %~dp0 185 | echo. 186 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 187 | goto end 188 | ) 189 | 190 | if "%1" == "text" ( 191 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 192 | if errorlevel 1 exit /b 1 193 | echo. 194 | echo.Build finished. The text files are in %BUILDDIR%/text. 195 | goto end 196 | ) 197 | 198 | if "%1" == "man" ( 199 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 200 | if errorlevel 1 exit /b 1 201 | echo. 202 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 203 | goto end 204 | ) 205 | 206 | if "%1" == "texinfo" ( 207 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 208 | if errorlevel 1 exit /b 1 209 | echo. 210 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 211 | goto end 212 | ) 213 | 214 | if "%1" == "gettext" ( 215 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 216 | if errorlevel 1 exit /b 1 217 | echo. 218 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 219 | goto end 220 | ) 221 | 222 | if "%1" == "changes" ( 223 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 224 | if errorlevel 1 exit /b 1 225 | echo. 226 | echo.The overview file is in %BUILDDIR%/changes. 227 | goto end 228 | ) 229 | 230 | if "%1" == "linkcheck" ( 231 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 232 | if errorlevel 1 exit /b 1 233 | echo. 234 | echo.Link check complete; look for any errors in the above output ^ 235 | or in %BUILDDIR%/linkcheck/output.txt. 236 | goto end 237 | ) 238 | 239 | if "%1" == "doctest" ( 240 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 241 | if errorlevel 1 exit /b 1 242 | echo. 243 | echo.Testing of doctests in the sources finished, look at the ^ 244 | results in %BUILDDIR%/doctest/output.txt. 245 | goto end 246 | ) 247 | 248 | if "%1" == "coverage" ( 249 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 250 | if errorlevel 1 exit /b 1 251 | echo. 252 | echo.Testing of coverage in the sources finished, look at the ^ 253 | results in %BUILDDIR%/coverage/python.txt. 254 | goto end 255 | ) 256 | 257 | if "%1" == "xml" ( 258 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 259 | if errorlevel 1 exit /b 1 260 | echo. 261 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 262 | goto end 263 | ) 264 | 265 | if "%1" == "pseudoxml" ( 266 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 267 | if errorlevel 1 exit /b 1 268 | echo. 269 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 270 | goto end 271 | ) 272 | 273 | if "%1" == "dummy" ( 274 | %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy 275 | if errorlevel 1 exit /b 1 276 | echo. 277 | echo.Build finished. Dummy builder generates no files. 278 | goto end 279 | ) 280 | 281 | :end 282 | -------------------------------------------------------------------------------- /examples/http_requests.py: -------------------------------------------------------------------------------- 1 | import paco 2 | import aiohttp 3 | 4 | 5 | async def fetch(url): 6 | async with aiohttp.ClientSession() as session: 7 | async with session.get(url) as res: 8 | return res 9 | 10 | 11 | async def fetch_urls(): 12 | urls = [ 13 | 'https://www.google.com', 14 | 'https://www.yahoo.com', 15 | 'https://www.bing.com', 16 | 'https://www.baidu.com', 17 | 'https://duckduckgo.com', 18 | ] 19 | 20 | # Map concurrent executor with concurrent limit of 3 21 | responses = await paco.map(fetch, urls, limit=3) 22 | 23 | for res in responses: 24 | print('Status:', res.status) 25 | 26 | 27 | # Run in event loop 28 | paco.run(fetch_urls()) 29 | -------------------------------------------------------------------------------- /examples/pipeline.py: -------------------------------------------------------------------------------- 1 | import paco 2 | 3 | 4 | async def filterer(x): 5 | return x < 8 6 | 7 | 8 | async def mapper(x): 9 | return x * 2 10 | 11 | 12 | async def drop(x): 13 | return x < 10 14 | 15 | 16 | async def reducer(acc, x): 17 | return acc + x 18 | 19 | 20 | async def task(numbers): 21 | return await (numbers 22 | | paco.filter(filterer) 23 | | paco.map(mapper) 24 | | paco.dropwhile(drop) 25 | | paco.reduce(reducer, initializer=0)) # noqa 26 | 27 | 28 | # Run in event loop 29 | number = paco.run(task((1, 2, 3, 4, 5, 6, 7, 8, 9, 10))) 30 | print('Number:', number) # => 36 31 | -------------------------------------------------------------------------------- /paco/__init__.py: -------------------------------------------------------------------------------- 1 | from .map import map 2 | from .run import run 3 | from .each import each 4 | from .some import some 5 | from .race import race 6 | from .once import once 7 | from .wait import wait 8 | from .curry import curry 9 | from .wraps import wraps 10 | from .apply import apply 11 | from .defer import defer 12 | from .every import every 13 | from .until import until 14 | from .times import times 15 | from .thunk import thunk 16 | from .gather import gather 17 | from .repeat import repeat 18 | from .filter import filter 19 | from .filterfalse import filterfalse 20 | from .reduce import reduce 21 | from .whilst import whilst 22 | from .series import series 23 | from .partial import partial 24 | from .timeout import timeout, TimeoutLimit 25 | from .compose import compose 26 | from .interval import interval 27 | from .flat_map import flat_map 28 | from .constant import constant, identity 29 | from .throttle import throttle 30 | from .dropwhile import dropwhile 31 | from .concurrent import ConcurrentExecutor, concurrent 32 | 33 | __author__ = 'Tomas Aparicio' 34 | __license__ = 'MIT' 35 | 36 | # Current package version 37 | __version__ = '0.2.4' 38 | 39 | # Explicit symbols to export 40 | __all__ = ( 41 | 'ConcurrentExecutor', 42 | 'apply', 43 | 'compose', 44 | 'concurrent', 45 | 'constant', 46 | 'curry', 47 | 'defer', 48 | 'dropwhile', 49 | 'each', 50 | 'every', 51 | 'filter', 52 | 'filterfalse', 53 | 'flat_map', 54 | 'gather', 55 | 'identity', 56 | 'interval', 57 | 'map', 58 | 'once', 59 | 'partial', 60 | 'race', 61 | 'reduce', 62 | 'repeat', 63 | 'run', 64 | 'series', 65 | 'some', 66 | 'throttle', 67 | 'thunk', 68 | 'timeout', 69 | 'TimeoutLimit', 70 | 'times', 71 | 'until', 72 | 'wait', 73 | 'whilst', 74 | 'wraps', 75 | ) 76 | -------------------------------------------------------------------------------- /paco/apply.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | from .decorator import decorate 4 | from .assertions import assert_corofunction 5 | 6 | 7 | @decorate 8 | def apply(coro, *args, **kw): 9 | """ 10 | Creates a continuation coroutine function with some arguments 11 | already applied. 12 | 13 | Useful as a shorthand when combined with other control flow functions. 14 | Any arguments passed to the returned function are added to the arguments 15 | originally passed to apply. 16 | 17 | This is similar to `paco.partial()`. 18 | 19 | This function can be used as decorator. 20 | 21 | arguments: 22 | coro (coroutinefunction): coroutine function to wrap. 23 | *args (mixed): mixed variadic arguments for partial application. 24 | *kwargs (mixed): mixed variadic keyword arguments for partial 25 | application. 26 | 27 | Raises: 28 | TypeError: if coro argument is not a coroutine function. 29 | 30 | Returns: 31 | coroutinefunction: wrapped coroutine function. 32 | 33 | Usage:: 34 | 35 | async def hello(name, mark='!'): 36 | print('Hello, {name}{mark}'.format(name=name, mark=mark)) 37 | 38 | hello_mike = paco.apply(hello, 'Mike') 39 | await hello_mike() 40 | # => Hello, Mike! 41 | 42 | hello_mike = paco.apply(hello, 'Mike', mark='?') 43 | await hello_mike() 44 | # => Hello, Mike? 45 | 46 | """ 47 | assert_corofunction(coro=coro) 48 | 49 | @asyncio.coroutine 50 | def wrapper(*_args, **_kw): 51 | # Explicitely ignore wrapper arguments 52 | return (yield from coro(*args, **kw)) 53 | 54 | return wrapper 55 | -------------------------------------------------------------------------------- /paco/assertions.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import inspect 3 | 4 | # Safe alias to inspect.isasyncgen for Python 3.6+ 5 | isasyncgen = getattr(inspect, 'isasyncgen', lambda x: False) 6 | 7 | 8 | def isiter(x): 9 | """ 10 | Returns `True` if the given value implements an valid iterable 11 | interface. 12 | 13 | Arguments: 14 | x (mixed): value to check if it is an iterable. 15 | 16 | Returns: 17 | bool 18 | """ 19 | return hasattr(x, '__iter__') and not isinstance(x, (str, bytes)) 20 | 21 | 22 | def isgenerator(x): 23 | """ 24 | Returns `True` if the given value is sync or async generator coroutine. 25 | 26 | Arguments: 27 | x (mixed): value to check if it is an iterable. 28 | 29 | Returns: 30 | bool 31 | """ 32 | return any([ 33 | hasattr(x, '__next__'), 34 | hasattr(x, '__anext__') 35 | ]) 36 | 37 | 38 | def iscallable(x): 39 | """ 40 | Returns `True` if the given value is a callable primitive object. 41 | 42 | Arguments: 43 | x (mixed): value to check. 44 | 45 | Returns: 46 | bool 47 | """ 48 | return any([ 49 | isfunc(x), 50 | asyncio.iscoroutinefunction(x) 51 | ]) 52 | 53 | 54 | def isfunc(x): 55 | """ 56 | Returns `True` if the given value is a function or method object. 57 | 58 | Arguments: 59 | x (mixed): value to check. 60 | 61 | Returns: 62 | bool 63 | """ 64 | return any([ 65 | inspect.isfunction(x) and not asyncio.iscoroutinefunction(x), 66 | inspect.ismethod(x) and not asyncio.iscoroutinefunction(x) 67 | ]) 68 | 69 | 70 | def iscoro_or_corofunc(x): 71 | """ 72 | Returns ``True`` if the given value is a coroutine or a coroutine function. 73 | 74 | Arguments: 75 | x (mixed): object value to assert. 76 | 77 | Returns: 78 | bool: returns ``True`` if ``x` is a coroutine or coroutine function. 79 | """ 80 | return asyncio.iscoroutinefunction(x) or asyncio.iscoroutine(x) 81 | 82 | 83 | def assert_corofunction(**kw): 84 | """ 85 | Asserts if a given values are a coroutine function. 86 | 87 | Arguments: 88 | **kw (mixed): value to check if it is an iterable. 89 | 90 | Raises: 91 | TypeError: if assertion fails. 92 | """ 93 | for name, value in kw.items(): 94 | if not asyncio.iscoroutinefunction(value): 95 | raise TypeError( 96 | 'paco: {} must be a coroutine function'.format(name)) 97 | 98 | 99 | def assert_iter(**kw): 100 | """ 101 | Asserts if a given values implements a valid iterable interface. 102 | 103 | Arguments: 104 | **kw (mixed): value to check if it is an iterable. 105 | 106 | Raises: 107 | TypeError: if assertion fails. 108 | """ 109 | for name, value in kw.items(): 110 | if not isiter(value): 111 | raise TypeError( 112 | 'paco: {} must be an iterable object'.format(name)) 113 | -------------------------------------------------------------------------------- /paco/compose.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | from .reduce import reduce 4 | 5 | 6 | def compose(*coros): 7 | """ 8 | Creates a coroutine function based on the composition of the passed 9 | coroutine functions. 10 | 11 | Each function consumes the yielded result of the coroutine that follows. 12 | 13 | Composing coroutine functions f(), g(), and h() would produce 14 | the result of f(g(h())). 15 | 16 | Arguments: 17 | *coros (coroutinefunction): variadic coroutine functions to compose. 18 | 19 | Raises: 20 | RuntimeError: if cannot execute a coroutine function. 21 | 22 | Returns: 23 | coroutinefunction 24 | 25 | Usage:: 26 | 27 | async def sum_1(num): 28 | return num + 1 29 | 30 | async def mul_2(num): 31 | return num * 2 32 | 33 | coro = paco.compose(sum_1, mul_2, sum_1) 34 | await coro(2) 35 | # => 7 36 | 37 | """ 38 | # Make list to inherit built-in type methods 39 | coros = list(coros) 40 | 41 | @asyncio.coroutine 42 | def reducer(acc, coro): 43 | return (yield from coro(acc)) 44 | 45 | @asyncio.coroutine 46 | def wrapper(acc): 47 | return (yield from reduce(reducer, coros, 48 | initializer=acc, right=True)) 49 | 50 | return wrapper 51 | -------------------------------------------------------------------------------- /paco/concurrent.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Coroutines concurrent pool executor with built-in 3 | concurrency limit based on a semaphore free slots algorithm. 4 | 5 | Usage:: 6 | 7 | async def fetch(url): 8 | r = await aiohttp.get(url) 9 | return await r.read() 10 | # limit the concurrent coroutines to 3 11 | pool = concurrent(3) 12 | for _ in range(10): 13 | p.submit(fetch, 'http://www.baidu.com') 14 | await p.join() 15 | """ 16 | import asyncio 17 | from collections import deque, namedtuple 18 | from .observer import Observer 19 | from .assertions import isiter 20 | 21 | # Task represents an immutable tuple storing the index order 22 | # and coroutine object. 23 | Task = namedtuple('Task', ['index', 'coro']) 24 | 25 | 26 | @asyncio.coroutine 27 | def safe_run(coro, return_exceptions=False): 28 | """ 29 | Executes a given coroutine and optionally catches exceptions, returning 30 | them as value. This function is intended to be used internally. 31 | """ 32 | try: 33 | result = yield from coro 34 | except Exception as err: 35 | if return_exceptions: 36 | result = err 37 | else: 38 | raise err 39 | return result 40 | 41 | 42 | @asyncio.coroutine 43 | def collect(coro, index, results, 44 | preserve_order=False, 45 | return_exceptions=False): 46 | """ 47 | Collect is used internally to execute coroutines and collect the returned 48 | value. This function is intended to be used internally. 49 | """ 50 | result = yield from safe_run(coro, return_exceptions=return_exceptions) 51 | 52 | if preserve_order: 53 | results[index] = result 54 | else: 55 | results.append(result) 56 | 57 | 58 | class ConcurrentExecutor(object): 59 | """ 60 | Concurrent executes a set of asynchronous coroutines 61 | with a simple throttle concurrency configurable concurrency limit. 62 | 63 | Provides an observer pub/sub interface, allowing API consumers to 64 | subscribe normal functions or coroutines to certain events that happen 65 | internally. 66 | 67 | ConcurrentExecutor is_running a low-level implementation that powers most of the 68 | utility functions provided in `paco`. 69 | 70 | For most cases you won't need to rely on it, instead you can 71 | use the high-level API functions that provides a simpler abstraction for 72 | the majority of the use cases. 73 | 74 | This class is not thread safe. 75 | 76 | Events: 77 | - start (executor): triggered before executor cycle starts. 78 | - finish (executor): triggered when all the coroutine finished. 79 | - task.start (task): triggered before coroutine starts. 80 | - task.finish (task, result): triggered when the coroutine finished. 81 | - task.error (task, error): triggered when a coroutined task 82 | raised an exception. 83 | 84 | Arguments: 85 | limit (int): concurrency limit. Defaults to 10. 86 | coros (list[coroutine], optional): list of coroutines to schedule. 87 | loop (asyncio.BaseEventLoop, optional): loop to run. 88 | Defaults to asyncio.get_event_loop(). 89 | ignore_empty (bool, optional): do not raise an exception if there are 90 | no coroutines to schedule are empty. 91 | 92 | Returns: 93 | ConcurrentExecutor 94 | 95 | Usage:: 96 | 97 | async def sum(x, y): 98 | return x + y 99 | 100 | pool = paco.ConcurrentExecutor(limit=2) 101 | pool.add(sum, 1, 2) 102 | pool.add(sum, None, 'str') 103 | 104 | done, pending = await pool.run(return_exceptions=True) 105 | [task.result() for task in done] 106 | # => [3, TypeError("unsupported operand type(s) for +: 'NoneType' and 'str'")] # noqa 107 | """ 108 | 109 | def __init__(self, limit=10, loop=None, coros=None, ignore_empty=False): 110 | self.errors = [] 111 | self.running = False 112 | self.return_exceptions = False 113 | self.limit = max(int(limit), 0) 114 | self.pool = deque() 115 | self.observer = Observer() 116 | self.ignore_empty = ignore_empty 117 | self.loop = loop or asyncio.get_event_loop() 118 | self.semaphore = asyncio.Semaphore(self.limit, loop=self.loop) 119 | 120 | # Register coroutines in the pool 121 | if isiter(coros): 122 | self.extend(*coros) 123 | 124 | def __len__(self): 125 | """ 126 | Returns the current length of the coroutines pool queue. 127 | 128 | Returns: 129 | int: current coroutines pool length. 130 | """ 131 | return len(self.pool) 132 | 133 | def reset(self): 134 | """ 135 | Resets the executer scheduler internal state. 136 | 137 | Raises: 138 | RuntimeError: is the executor is still running. 139 | """ 140 | if self.running: 141 | raise RuntimeError('paco: executor is still running') 142 | 143 | self.pool.clear() 144 | self.observer.clear() 145 | self.semaphore = asyncio.Semaphore(self.limit, loop=self.loop) 146 | 147 | def cancel(self): 148 | """ 149 | Tries to gracefully cancel the pending coroutine scheduled 150 | coroutine tasks. 151 | """ 152 | self.pool.clear() 153 | self.running = False 154 | 155 | def on(self, event, fn): 156 | """ 157 | Subscribes to a specific event. 158 | 159 | Arguments: 160 | event (str): event name to subcribe. 161 | fn (function): function to trigger. 162 | """ 163 | return self.observer.on(event, fn) 164 | 165 | def off(self, event): 166 | """ 167 | Removes event subscribers. 168 | 169 | Arguments: 170 | event (str): event name to remove observers. 171 | """ 172 | return self.observer.off(event) 173 | 174 | def extend(self, *coros): 175 | """ 176 | Add multiple coroutines to the executor pool. 177 | 178 | Raises: 179 | TypeError: if the coro object is not a valid coroutine 180 | """ 181 | for coro in coros: 182 | self.add(coro) 183 | 184 | def add(self, coro, *args, **kw): 185 | """ 186 | Adds a new coroutine function with optional variadic argumetns. 187 | 188 | Arguments: 189 | coro (coroutine function): coroutine to execute. 190 | *args (mixed): optional variadic arguments 191 | 192 | Raises: 193 | TypeError: if the coro object is not a valid coroutine 194 | 195 | Returns: 196 | future: coroutine wrapped future 197 | """ 198 | # Create coroutine object if a function is provided 199 | if asyncio.iscoroutinefunction(coro): 200 | coro = coro(*args, **kw) 201 | 202 | # Verify coroutine 203 | if not asyncio.iscoroutine(coro): 204 | raise TypeError('paco: coro must be a coroutine object') 205 | 206 | # Store coroutine with arguments for deferred execution 207 | index = max(len(self.pool), 0) 208 | task = Task(index, coro) 209 | 210 | # Append the coroutine data to the pool 211 | self.pool.append(task) 212 | 213 | return coro 214 | 215 | # Alias to add() 216 | submit = add 217 | 218 | @asyncio.coroutine 219 | def _run_sequentially(self): 220 | # Store futures in two queues 221 | done, pending = [], [] 222 | 223 | # Run until the pool is empty 224 | while len(self.pool): 225 | future = asyncio.Future(loop=self.loop) 226 | pending.append(future) 227 | 228 | # Run coroutine 229 | result = yield from self._run_coro((self.pool.popleft())) 230 | 231 | # Assign result to future 232 | if isinstance(result, Exception): 233 | if not self.return_exceptions: 234 | raise result 235 | future.set_exception(result) 236 | else: 237 | future.set_result(result) 238 | 239 | # Swap future between queues 240 | done.append(pending.pop()) 241 | 242 | # Build futures tuple to be compatible with asyncio.wait() interface 243 | return set(done), set(pending) 244 | 245 | @asyncio.coroutine 246 | def _run_concurrently(self, timeout=None, return_when='ALL_COMPLETED'): 247 | coros = [] 248 | limit = self.limit 249 | 250 | while len(self.pool): 251 | task = self.pool.popleft() 252 | 253 | # Run without concurrency limit 254 | if limit <= 0: 255 | coros.append(self._run_coro(task)) 256 | # Otherwise, schedule for concurrent based flow 257 | else: 258 | coros.append(self._schedule_coro(task)) 259 | 260 | # Wait until all the coroutines finishes 261 | return (yield from asyncio.wait(coros, 262 | loop=self.loop, 263 | timeout=timeout, 264 | return_when=return_when)) 265 | 266 | @asyncio.coroutine 267 | def _run_coro(self, task): 268 | # Executor must be running 269 | if not self.running: 270 | return None 271 | 272 | # Trigger task pre-execution event 273 | yield from self.observer.trigger('task.start', task) 274 | 275 | # Trigger coroutine task 276 | index, coro = task 277 | 278 | # Safe coroutine execution 279 | try: 280 | result = yield from safe_run( 281 | coro, return_exceptions=self.return_exceptions) 282 | except Exception as err: 283 | self.errors.append(err) 284 | yield from self.observer.trigger('task.error', task, err) 285 | raise err # important: re-raise exception for asyncio propagation 286 | 287 | # Trigger task post-execution event 288 | yield from self.observer.trigger('task.finish', task, result) 289 | 290 | # Return result to future binding 291 | return result 292 | 293 | @asyncio.coroutine 294 | def _schedule_coro(self, task): 295 | """ 296 | Executes a given coroutine in the next available slot. 297 | 298 | Slots are available based on a simple free slots 299 | scheduling semaphore-based algorithm. 300 | """ 301 | # Run when a slot is available 302 | with (yield from self.semaphore): 303 | return (yield from self._run_coro(task)) 304 | 305 | @asyncio.coroutine 306 | def run(self, 307 | timeout=None, 308 | return_when=None, 309 | return_exceptions=None, 310 | ignore_empty=None): 311 | """ 312 | Executes the registered coroutines in the executor queue. 313 | 314 | Arguments: 315 | timeout (int/float): max execution timeout. No limit by default. 316 | return_exceptions (bool): in case of coroutine exception. 317 | return_when (str): sets when coroutine should be resolved. 318 | See `asyncio.wait`_ for supported values. 319 | ignore_empty (bool, optional): do not raise an exception if there are 320 | no coroutines to schedule are empty. 321 | 322 | Returns: 323 | asyncio.Future (tuple): two sets of Futures: ``(done, pending)`` 324 | 325 | Raises: 326 | ValueError: if there is no coroutines to schedule. 327 | RuntimeError: if executor is still running. 328 | TimeoutError: if execution takes more than expected. 329 | 330 | .. _asyncio.wait: https://docs.python.org/3/library/asyncio-task.html#asyncio.wait # noqa 331 | """ 332 | # Only allow 1 concurrent execution 333 | if self.running: 334 | raise RuntimeError('paco: executor is already running') 335 | 336 | # Overwrite ignore empty behaviour, if explicitly defined 337 | ignore_empty = (self.ignore_empty if ignore_empty is None 338 | else ignore_empty) 339 | 340 | # Check we have coroutines to schedule 341 | if len(self.pool) == 0: 342 | # If ignore empty mode enabled, just return an empty tuple 343 | if ignore_empty: 344 | return (tuple(), tuple()) 345 | # Othwerise raise an exception 346 | raise ValueError('paco: pool of coroutines is empty') 347 | 348 | # Set executor state to running 349 | self.running = True 350 | 351 | # Configure return exceptions 352 | if return_exceptions is not None: 353 | self.return_exceptions = return_exceptions 354 | 355 | if return_exceptions is False and return_when is None: 356 | return_when = 'FIRST_EXCEPTION' 357 | 358 | if return_when is None: 359 | return_when = 'ALL_COMPLETED' 360 | 361 | # Trigger pre-execution event 362 | yield from self.observer.trigger('start', self) 363 | 364 | # Sequential coroutines execution 365 | if self.limit == 1: 366 | done, pending = yield from self._run_sequentially() 367 | 368 | # Concurrent execution based on configured limit 369 | if self.limit != 1: 370 | done, pending = yield from self._run_concurrently( 371 | timeout=timeout, 372 | return_when=return_when) 373 | 374 | # Reset internal state and queue 375 | self.running = False 376 | 377 | # Raise exception, if needed 378 | if self.return_exceptions is False and self.errors: 379 | err = self.errors[0] 380 | err.errors = self.errors[1:] 381 | raise err 382 | 383 | # Trigger pre-execution event 384 | yield from self.observer.trigger('finish', self) 385 | 386 | # Reset executor state to defaults after each execution 387 | self.reset() 388 | 389 | # Return resultant futures in two tuples 390 | return done, pending 391 | 392 | # Idiomatic method alias to run() 393 | wait = run 394 | 395 | def is_running(self): 396 | """ 397 | Checks the executor running state. 398 | 399 | Returns: 400 | bool: ``True`` if the executur is running, otherwise ``False``. 401 | """ 402 | return self.running 403 | 404 | 405 | # Semantic shortcut to ConcurrentExecutor() 406 | concurrent = ConcurrentExecutor 407 | -------------------------------------------------------------------------------- /paco/constant.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | 4 | 5 | def constant(value, delay=None): 6 | """ 7 | Returns a coroutine function that when called, always returns 8 | the provided value. 9 | 10 | This function has an alias: `paco.identity`. 11 | 12 | Arguments: 13 | value (mixed): value to constantly return when coroutine is called. 14 | delay (int/float): optional return value delay in seconds. 15 | 16 | Returns: 17 | coroutinefunction 18 | 19 | Usage:: 20 | 21 | coro = paco.constant('foo') 22 | 23 | await coro() 24 | # => 'foo' 25 | await coro() 26 | # => 'foo' 27 | 28 | """ 29 | @asyncio.coroutine 30 | def coro(): 31 | if delay: 32 | yield from asyncio.sleep(delay) 33 | return value 34 | 35 | return coro 36 | 37 | 38 | def identity(value, delay=None): 39 | """ 40 | Returns a coroutine function that when called, always returns 41 | the provided value. 42 | 43 | This function is an alias to `paco.constant`. 44 | 45 | Arguments: 46 | value (mixed): value to constantly return when coroutine is called. 47 | delay (int/float): optional return value delay in seconds. 48 | 49 | Returns: 50 | coroutinefunction 51 | 52 | Usage:: 53 | 54 | coro = paco.identity('foo') 55 | 56 | await coro() 57 | # => 'foo' 58 | await coro() 59 | # => 'foo' 60 | 61 | """ 62 | return constant(value, delay=delay) 63 | -------------------------------------------------------------------------------- /paco/curry.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import inspect 3 | import functools 4 | from .wraps import wraps 5 | from .assertions import isfunc, iscallable 6 | 7 | 8 | def curry(arity_or_fn=None, ignore_kwargs=False, evaluator=None, *args, **kw): 9 | """ 10 | Creates a function that accepts one or more arguments of a function and 11 | either invokes func returning its result if at least arity number of 12 | arguments have been provided, or returns a function that accepts the 13 | remaining function arguments until the function arity is satisfied. 14 | 15 | This function is overloaded: you can pass a function or coroutine function 16 | as first argument or an `int` indicating the explicit function arity. 17 | 18 | Function arity can be inferred via function signature or explicitly 19 | passed via `arity_or_fn` param. 20 | 21 | You can optionally ignore keyword based arguments as well passsing the 22 | `ignore_kwargs` param with `True` value. 23 | 24 | This function can be used as decorator. 25 | 26 | Arguments: 27 | arity_or_fn (int|function|coroutinefunction): function arity to curry 28 | or function to curry. 29 | ignore_kwargs (bool): ignore keyword arguments as arity to satisfy 30 | during curry. 31 | evaluator (function): use a custom arity evaluator function. 32 | *args (mixed): mixed variadic arguments for partial function 33 | application. 34 | *kwargs (mixed): keyword variadic arguments for partial function 35 | application. 36 | 37 | Raises: 38 | TypeError: if function is not a function or a coroutine function. 39 | 40 | Returns: 41 | function or coroutinefunction: function will be returned until all the 42 | function arity is satisfied, where a coroutine function will be 43 | returned instead. 44 | 45 | Usage:: 46 | 47 | # Function signature inferred function arity 48 | @paco.curry 49 | async def task(x, y, z=0): 50 | return x * y + z 51 | 52 | await task(4)(4)(z=8) 53 | # => 24 54 | 55 | # User defined function arity 56 | @paco.curry(4) 57 | async def task(x, y, *args, **kw): 58 | return x * y + args[0] * args[1] 59 | 60 | await task(4)(4)(8)(8) 61 | # => 80 62 | 63 | # Ignore keyword arguments from arity 64 | @paco.curry(ignore_kwargs=True) 65 | async def task(x, y, z=0): 66 | return x * y 67 | 68 | await task(4)(4) 69 | # => 16 70 | 71 | """ 72 | def isvalidarg(x): 73 | return all([ 74 | x.kind != x.VAR_KEYWORD, 75 | x.kind != x.VAR_POSITIONAL, 76 | any([ 77 | not ignore_kwargs, 78 | ignore_kwargs and x.default == x.empty 79 | ]) 80 | ]) 81 | 82 | def params(fn): 83 | return inspect.signature(fn).parameters.values() 84 | 85 | def infer_arity(fn): 86 | return len([x for x in params(fn) if isvalidarg(x)]) 87 | 88 | def merge_args(acc, args, kw): 89 | _args, _kw = acc 90 | _args = _args + args 91 | _kw = _kw or {} 92 | _kw.update(kw) 93 | return _args, _kw 94 | 95 | def currier(arity, acc, fn, *args, **kw): 96 | """ 97 | Function either continues curring of the arguments 98 | or executes function if desired arguments have being collected. 99 | If function curried is variadic then execution without arguments 100 | will finish curring and trigger the function 101 | """ 102 | # Merge call arguments with accumulated ones 103 | _args, _kw = merge_args(acc, args, kw) 104 | 105 | # Get current function call accumulated arity 106 | current_arity = len(args) 107 | 108 | # Count keyword params as arity to satisfy, if required 109 | if not ignore_kwargs: 110 | current_arity += len(kw) 111 | 112 | # Decrease function arity to satisfy 113 | arity -= current_arity 114 | 115 | # Use user-defined custom arity evaluator strategy, if present 116 | currify = evaluator and evaluator(acc, fn) 117 | 118 | # If arity is not satisfied, return recursive partial function 119 | if currify is not False and arity > 0: 120 | return functools.partial(currier, arity, (_args, _kw), fn) 121 | 122 | # If arity is satisfied, instanciate coroutine and return it 123 | return fn(*_args, **_kw) 124 | 125 | def wrapper(fn, *args, **kw): 126 | if not iscallable(fn): 127 | raise TypeError('paco: first argument must a coroutine function, ' 128 | 'a function or a method.') 129 | 130 | # Infer function arity, if required 131 | arity = (arity_or_fn if isinstance(arity_or_fn, int) 132 | else infer_arity(fn)) 133 | 134 | # Wraps function as coroutine function, if needed. 135 | fn = wraps(fn) if isfunc(fn) else fn 136 | 137 | # Otherwise return recursive currier function 138 | return currier(arity, (args, kw), fn, *args, **kw) if arity > 0 else fn 139 | 140 | # Return currier function or decorator wrapper 141 | return (wrapper(arity_or_fn, *args, **kw) 142 | if iscallable(arity_or_fn) 143 | else wrapper) 144 | -------------------------------------------------------------------------------- /paco/decorator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | import functools 4 | from inspect import isfunction 5 | from .generator import consume 6 | from .assertions import iscoro_or_corofunc, isgenerator 7 | from .pipe import overload # noqa 8 | 9 | 10 | def generator_consumer(coro): # pragma: no cover 11 | """ 12 | Decorator wrapper that consumes sync/async generators provided as 13 | interable input argument. 14 | 15 | This function is only intended to be used internally. 16 | 17 | Arguments: 18 | coro (coroutinefunction): function to decorate 19 | 20 | Raises: 21 | TypeError: if function or coroutine function is not provided. 22 | 23 | Returns: 24 | function: decorated function. 25 | """ 26 | if not asyncio.iscoroutinefunction(coro): 27 | raise TypeError('paco: coro must be a coroutine function') 28 | 29 | @functools.wraps(coro) 30 | @asyncio.coroutine 31 | def wrapper(*args, **kw): 32 | if len(args) > 1 and isgenerator(args[1]): 33 | args = list(args) 34 | args[1] = (yield from consume(args[1]) 35 | if hasattr(args[1], '__anext__') 36 | else list(args[1])) 37 | args = tuple(args) 38 | return (yield from coro(*args, **kw)) 39 | return wrapper 40 | 41 | 42 | def decorate(fn): 43 | """ 44 | Generic decorator for coroutines helper functions allowing 45 | multiple variadic initialization arguments. 46 | 47 | This function is intended to be used internally. 48 | 49 | Arguments: 50 | fn (function): target function to decorate. 51 | 52 | Raises: 53 | TypeError: if function or coroutine function is not provided. 54 | 55 | Returns: 56 | function: decorated function. 57 | """ 58 | if not isfunction(fn): 59 | raise TypeError('paco: fn must be a callable object') 60 | 61 | @functools.wraps(fn) 62 | def decorator(*args, **kw): 63 | # If coroutine object is passed 64 | for arg in args: 65 | if iscoro_or_corofunc(arg): 66 | return fn(*args, **kw) 67 | 68 | # Explicit argument must be at least a coroutine 69 | if len(args) and args[0] is None: 70 | raise TypeError('paco: first argument cannot be empty') 71 | 72 | def wrapper(coro, *_args, **_kw): 73 | # coro must be a valid type 74 | if not iscoro_or_corofunc(coro): 75 | raise TypeError('paco: first argument must be a ' 76 | 'coroutine or coroutine function') 77 | 78 | # Merge call arguments 79 | _args = ((coro,) + (args + _args)) 80 | kw.update(_kw) 81 | 82 | # Trigger original decorated function 83 | return fn(*_args, **kw) 84 | return wrapper 85 | return decorator 86 | -------------------------------------------------------------------------------- /paco/defer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | from .decorator import decorate 4 | from .assertions import assert_corofunction 5 | 6 | 7 | @decorate 8 | def defer(coro, delay=1): 9 | """ 10 | Returns a coroutine function wrapper that will defer the given coroutine 11 | execution for a certain amount of seconds in a non-blocking way. 12 | 13 | This function can be used as decorator. 14 | 15 | Arguments: 16 | coro (coroutinefunction): coroutine function to defer. 17 | delay (int/float): number of seconds to defer execution. 18 | 19 | Raises: 20 | TypeError: if coro argument is not a coroutine function. 21 | 22 | Returns: 23 | filtered values (list): ordered list of resultant values. 24 | 25 | Usage:: 26 | 27 | # Usage as function 28 | await paco.defer(coro, delay=1) 29 | await paco.defer(coro, delay=0.5) 30 | 31 | # Usage as decorator 32 | @paco.defer(delay=1) 33 | async def mul_2(num): 34 | return num * 2 35 | 36 | await mul_2(2) 37 | # => 4 38 | 39 | """ 40 | assert_corofunction(coro=coro) 41 | 42 | @asyncio.coroutine 43 | def wrapper(*args, **kw): 44 | # Wait until we're done 45 | yield from asyncio.sleep(delay) 46 | return (yield from coro(*args, **kw)) 47 | 48 | return wrapper 49 | -------------------------------------------------------------------------------- /paco/dropwhile.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | from .filter import filter 4 | from .decorator import overload 5 | 6 | 7 | @overload 8 | @asyncio.coroutine 9 | def dropwhile(coro, iterable, loop=None): 10 | """ 11 | Make an iterator that drops elements from the iterable as long as the 12 | predicate is true; afterwards, returns every element. 13 | 14 | Note, the iterator does not produce any output until the predicate first 15 | becomes false, so it may have a lengthy start-up time. 16 | 17 | This function is pretty much equivalent to Python standard 18 | `itertools.dropwhile()`, but designed to be used with async coroutines. 19 | 20 | This function is a coroutine. 21 | 22 | This function can be composed in a pipeline chain with ``|`` operator. 23 | 24 | Arguments: 25 | coro (coroutine function): coroutine function to call with values 26 | to reduce. 27 | iterable (iterable|asynchronousiterable): an iterable collection 28 | yielding coroutines functions. 29 | loop (asyncio.BaseEventLoop): optional event loop to use. 30 | 31 | Raises: 32 | TypeError: if coro argument is not a coroutine function. 33 | 34 | Returns: 35 | filtered values (list): ordered list of resultant values. 36 | 37 | Usage:: 38 | 39 | async def filter(num): 40 | return num < 4 41 | 42 | await paco.dropwhile(filter, [1, 2, 3, 4, 5, 1]) 43 | # => [4, 5, 1] 44 | 45 | """ 46 | drop = False 47 | 48 | @asyncio.coroutine 49 | def assert_fn(element): 50 | nonlocal drop 51 | 52 | if element and not drop: 53 | return False 54 | 55 | if not element and not drop: 56 | drop = True 57 | 58 | return True if drop else element 59 | 60 | @asyncio.coroutine 61 | def filter_fn(element): 62 | return (yield from coro(element)) 63 | 64 | return (yield from filter(filter_fn, iterable, 65 | assert_fn=assert_fn, limit=1, loop=loop)) 66 | -------------------------------------------------------------------------------- /paco/each.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | from .decorator import overload 4 | from .concurrent import ConcurrentExecutor, safe_run 5 | from .assertions import assert_corofunction, assert_iter 6 | 7 | 8 | @overload 9 | @asyncio.coroutine 10 | def each(coro, iterable, limit=0, loop=None, 11 | collect=False, timeout=None, return_exceptions=False, *args, **kw): 12 | """ 13 | Concurrently iterates values yielded from an iterable, passing them to 14 | an asynchronous coroutine. 15 | 16 | You can optionally collect yielded values passing collect=True param, 17 | which would be equivalent to `paco.map()``. 18 | 19 | Mapped values will be returned as an ordered list. 20 | Items order is preserved based on origin iterable order. 21 | 22 | Concurrency level can be configurable via `limit` param. 23 | 24 | All coroutines will be executed in the same loop. 25 | 26 | This function is a coroutine. 27 | 28 | This function can be composed in a pipeline chain with ``|`` operator. 29 | 30 | Arguments: 31 | coro (coroutinefunction): coroutine iterator function that accepts 32 | iterable values. 33 | iterable (iterable|asynchronousiterable): an iterable collection 34 | yielding coroutines functions. 35 | limit (int): max iteration concurrency limit. Use ``0`` for no limit. 36 | collect (bool): return yielded values from coroutines. Default False. 37 | loop (asyncio.BaseEventLoop): optional event loop to use. 38 | return_exceptions (bool): enable/disable returning exceptions in case 39 | of error. `collect` param must be True. 40 | timeout (int|float): timeout can be used to control the maximum number 41 | of seconds to wait before returning. timeout can be an int or 42 | float. If timeout is not specified or None, there is no limit to 43 | the wait time. 44 | *args (mixed): optional variadic arguments to pass to the 45 | coroutine iterable function. 46 | 47 | Returns: 48 | results (list): ordered list of values yielded by coroutines 49 | 50 | Raises: 51 | TypeError: in case of invalid input arguments. 52 | 53 | Usage:: 54 | 55 | async def mul_2(num): 56 | return num * 2 57 | 58 | await paco.each(mul_2, [1, 2, 3, 4, 5]) 59 | # => None 60 | 61 | await paco.each(mul_2, [1, 2, 3, 4, 5], collect=True) 62 | # => [2, 4, 6, 8, 10] 63 | 64 | """ 65 | assert_corofunction(coro=coro) 66 | assert_iter(iterable=iterable) 67 | 68 | # By default do not collect yielded values from coroutines 69 | results = None 70 | 71 | if collect: 72 | # Store ordered results 73 | results = [None] * len(iterable) 74 | 75 | # Create concurrent executor 76 | pool = ConcurrentExecutor(limit=limit, loop=loop) 77 | 78 | @asyncio.coroutine 79 | def collector(index, item): 80 | result = yield from safe_run(coro(item, *args, **kw), 81 | return_exceptions=return_exceptions) 82 | if collect: 83 | results[index] = result 84 | 85 | return result 86 | 87 | # Iterate and pass elements to coroutine 88 | for index, value in enumerate(iterable): 89 | pool.add(collector(index, value)) 90 | 91 | # Wait until all the coroutines finishes 92 | yield from pool.run(return_exceptions=return_exceptions, 93 | ignore_empty=True, 94 | timeout=timeout) 95 | 96 | # Returns list of mapped results in order 97 | return results 98 | -------------------------------------------------------------------------------- /paco/every.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | from .partial import partial 4 | from .decorator import overload 5 | from .concurrent import ConcurrentExecutor 6 | from .assertions import assert_corofunction, assert_iter 7 | 8 | 9 | @overload 10 | @asyncio.coroutine 11 | def every(coro, iterable, limit=1, loop=None): 12 | """ 13 | Returns `True` if every element in a given iterable satisfies the coroutine 14 | asynchronous test. 15 | 16 | If any iteratee coroutine call returns `False`, the process is inmediately 17 | stopped, and `False` will be returned. 18 | 19 | You can increase the concurrency limit for a fast race condition scenario. 20 | 21 | This function is a coroutine. 22 | 23 | This function can be composed in a pipeline chain with ``|`` operator. 24 | 25 | Arguments: 26 | coro (coroutine function): coroutine function to call with values 27 | to reduce. 28 | iterable (iterable): an iterable collection yielding 29 | coroutines functions. 30 | limit (int): max concurrency execution limit. Use ``0`` for no limit. 31 | loop (asyncio.BaseEventLoop): optional event loop to use. 32 | 33 | Raises: 34 | TypeError: if input arguments are not valid. 35 | 36 | Returns: 37 | bool: `True` if all the values passes the test, otherwise `False`. 38 | 39 | Usage:: 40 | 41 | async def gt_10(num): 42 | return num > 10 43 | 44 | await paco.every(gt_10, [1, 2, 3, 11]) 45 | # => False 46 | 47 | await paco.every(gt_10, [11, 12, 13]) 48 | # => True 49 | 50 | """ 51 | assert_corofunction(coro=coro) 52 | assert_iter(iterable=iterable) 53 | 54 | # Reduced accumulator value 55 | passes = True 56 | 57 | # Handle empty iterables 58 | if len(iterable) == 0: 59 | return passes 60 | 61 | # Create concurrent executor 62 | pool = ConcurrentExecutor(limit=limit, loop=loop) 63 | 64 | # Tester function to guarantee the file is canceled. 65 | @asyncio.coroutine 66 | def tester(element): 67 | nonlocal passes 68 | if not passes: 69 | return None 70 | 71 | if not (yield from coro(element)): 72 | # Flag as not test passed 73 | passes = False 74 | # Force ignoring pending coroutines 75 | pool.cancel() 76 | 77 | # Iterate and attach coroutine for defer scheduling 78 | for element in iterable: 79 | pool.add(partial(tester, element)) 80 | 81 | # Wait until all coroutines finish 82 | yield from pool.run() 83 | 84 | return passes 85 | -------------------------------------------------------------------------------- /paco/filter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | from .decorator import overload 4 | from .concurrent import ConcurrentExecutor 5 | from .assertions import assert_corofunction, assert_iter 6 | 7 | 8 | @asyncio.coroutine 9 | def assert_true(element): 10 | """ 11 | Asserts that a given coroutine yields a true-like value. 12 | 13 | Arguments: 14 | element (mixed): element to evaluate. 15 | 16 | Returns: 17 | bool 18 | """ 19 | return element 20 | 21 | 22 | @overload 23 | @asyncio.coroutine 24 | def filter(coro, iterable, assert_fn=None, limit=0, loop=None): 25 | """ 26 | Returns a list of all the values in coll which pass an asynchronous truth 27 | test coroutine. 28 | 29 | Operations are executed concurrently by default, but results 30 | will be in order. 31 | 32 | You can configure the concurrency via `limit` param. 33 | 34 | This function is the asynchronous equivalent port Python built-in 35 | `filter()` function. 36 | 37 | This function is a coroutine. 38 | 39 | This function can be composed in a pipeline chain with ``|`` operator. 40 | 41 | Arguments: 42 | coro (coroutine function): coroutine filter function to call accepting 43 | iterable values. 44 | iterable (iterable|asynchronousiterable): an iterable collection 45 | yielding coroutines functions. 46 | assert_fn (coroutinefunction): optional assertion function. 47 | limit (int): max filtering concurrency limit. Use ``0`` for no limit. 48 | loop (asyncio.BaseEventLoop): optional event loop to use. 49 | 50 | Raises: 51 | TypeError: if coro argument is not a coroutine function. 52 | 53 | Returns: 54 | list: ordered list containing values that passed 55 | the filter. 56 | 57 | Usage:: 58 | 59 | async def iseven(num): 60 | return num % 2 == 0 61 | 62 | async def assert_false(el): 63 | return not el 64 | 65 | await paco.filter(iseven, [1, 2, 3, 4, 5]) 66 | # => [2, 4] 67 | 68 | await paco.filter(iseven, [1, 2, 3, 4, 5], assert_fn=assert_false) 69 | # => [1, 3, 5] 70 | 71 | """ 72 | assert_corofunction(coro=coro) 73 | assert_iter(iterable=iterable) 74 | 75 | # Check valid or empty iterable 76 | if len(iterable) == 0: 77 | return iterable 78 | 79 | # Reduced accumulator value 80 | results = [None] * len(iterable) 81 | 82 | # Use a custom or default filter assertion function 83 | assert_fn = assert_fn or assert_true 84 | 85 | # Create concurrent executor 86 | pool = ConcurrentExecutor(limit=limit, loop=loop) 87 | 88 | # Reducer partial function for deferred coroutine execution 89 | def filterer(index, element): 90 | @asyncio.coroutine 91 | def wrapper(): 92 | result = yield from coro(element) 93 | if (yield from assert_fn(result)): 94 | results[index] = element 95 | return wrapper 96 | 97 | # Iterate and attach coroutine for defer scheduling 98 | for index, element in enumerate(iterable): 99 | pool.add(filterer(index, element)) 100 | 101 | # Wait until all coroutines finish 102 | yield from pool.run(ignore_empty=True) 103 | 104 | # Returns filtered elements 105 | return [x for x in results if x is not None] 106 | -------------------------------------------------------------------------------- /paco/filterfalse.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | from .filter import filter 4 | from .decorator import overload 5 | 6 | 7 | @asyncio.coroutine 8 | def assert_false(element): 9 | """ 10 | Asserts that a given coroutine yields a non-true value. 11 | """ 12 | return not element 13 | 14 | 15 | @overload 16 | @asyncio.coroutine 17 | def filterfalse(coro, iterable, limit=0, loop=None): 18 | """ 19 | Returns a list of all the values in coll which pass an asynchronous truth 20 | test coroutine. 21 | 22 | Operations are executed concurrently by default, but results 23 | will be in order. 24 | 25 | You can configure the concurrency via `limit` param. 26 | 27 | This function is the asynchronous equivalent port Python built-in 28 | `filterfalse()` function. 29 | 30 | This function is a coroutine. 31 | 32 | This function can be composed in a pipeline chain with ``|`` operator. 33 | 34 | Arguments: 35 | coro (coroutine function): coroutine filter function to call accepting 36 | iterable values. 37 | iterable (iterable): an iterable collection yielding 38 | coroutines functions. 39 | assert_fn (coroutinefunction): optional assertion function. 40 | limit (int): max filtering concurrency limit. Use ``0`` for no limit. 41 | loop (asyncio.BaseEventLoop): optional event loop to use. 42 | 43 | Raises: 44 | TypeError: if coro argument is not a coroutine function. 45 | 46 | Returns: 47 | filtered values (list): ordered list containing values that do not 48 | passed the filter. 49 | 50 | Usage:: 51 | 52 | async def iseven(num): 53 | return num % 2 == 0 54 | 55 | await paco.filterfalse(coro, [1, 2, 3, 4, 5]) 56 | # => [1, 3, 5] 57 | 58 | """ 59 | return (yield from filter(coro, iterable, 60 | assert_fn=assert_false, 61 | limit=limit, loop=loop)) 62 | -------------------------------------------------------------------------------- /paco/flat_map.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | from .assertions import isiter 4 | from .reduce import reduce 5 | from .decorator import overload 6 | from .assertions import assert_corofunction, assert_iter 7 | 8 | 9 | @overload 10 | @asyncio.coroutine 11 | def flat_map(coro, iterable, limit=0, loop=None, timeout=None, 12 | return_exceptions=False, initializer=None, *args, **kw): 13 | """ 14 | Concurrently iterates values yielded from an iterable, passing them to 15 | an asynchronous coroutine. 16 | 17 | This function is the flatten version of to ``paco.map()``. 18 | 19 | Mapped values will be returned as an ordered list. 20 | Items order is preserved based on origin iterable order. 21 | 22 | Concurrency level can be configurable via ``limit`` param. 23 | 24 | All coroutines will be executed in the same loop. 25 | 26 | This function is a coroutine. 27 | 28 | This function can be composed in a pipeline chain with ``|`` operator. 29 | 30 | Arguments: 31 | coro (coroutinefunction): coroutine iterator function that accepts 32 | iterable values. 33 | iterable (iterable|asynchronousiterable): an iterable collection 34 | yielding coroutines functions. 35 | limit (int): max iteration concurrency limit. Use ``0`` for no limit. 36 | collect (bool): return yielded values from coroutines. Default False. 37 | loop (asyncio.BaseEventLoop): optional event loop to use. 38 | return_exceptions (bool): enable/disable returning exceptions in case 39 | of error. `collect` param must be True. 40 | timeout (int|float): timeout can be used to control the maximum number 41 | of seconds to wait before returning. timeout can be an int or 42 | float. If timeout is not specified or None, there is no limit to 43 | the wait time. 44 | *args (mixed): optional variadic arguments to pass to the 45 | coroutine iterable function. 46 | 47 | Returns: 48 | results (list): ordered list of values yielded by coroutines 49 | 50 | Raises: 51 | TypeError: in case of invalid input arguments. 52 | 53 | Usage:: 54 | 55 | async def mul_2(num): 56 | return num * 2 57 | 58 | await paco.flat_map(mul_2, [1, [2], [3, [4]], [(5,)]]) 59 | # => [2, 4, 6, 8, 10] 60 | 61 | # Pipeline style 62 | await [1, [2], [3, [4]], [(5,)]] | paco.flat_map(mul_2) 63 | # => [2, 4, 6, 8, 10] 64 | 65 | """ 66 | assert_corofunction(coro=coro) 67 | assert_iter(iterable=iterable) 68 | 69 | # By default do not collect yielded values from coroutines 70 | results = initializer if isiter(initializer) else [] 71 | 72 | @asyncio.coroutine 73 | def reducer(buf, value): 74 | if isiter(value): 75 | yield from _reduce(value) 76 | else: 77 | buf.append((yield from coro(value))) 78 | return buf 79 | 80 | def _reduce(iterable): 81 | return reduce(reducer, iterable, 82 | initializer=results, limit=limit, loop=loop) 83 | 84 | # Returns list of mapped reduced results 85 | return (yield from _reduce(iterable)) 86 | -------------------------------------------------------------------------------- /paco/gather.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | from .assertions import isiter 4 | from .concurrent import ConcurrentExecutor, collect 5 | 6 | 7 | @asyncio.coroutine 8 | def gather(*coros_or_futures, limit=0, loop=None, timeout=None, 9 | preserve_order=False, return_exceptions=False): 10 | """ 11 | Return a future aggregating results from the given coroutine objects 12 | with a concurrency execution limit. 13 | 14 | If all the tasks are done successfully, the returned future’s result is 15 | the list of results (in the order of the original sequence, 16 | not necessarily the order of results arrival). 17 | 18 | If return_exceptions is `True`, exceptions in the tasks are treated the 19 | same as successful results, and gathered in the result list; otherwise, 20 | the first raised exception will be immediately propagated to the 21 | returned future. 22 | 23 | All futures must share the same event loop. 24 | 25 | This functions is mostly compatible with Python standard 26 | ``asyncio.gather``, but providing ordered results and concurrency control 27 | flow. 28 | 29 | This function is a coroutine. 30 | 31 | Arguments: 32 | *coros_or_futures (coroutines|list): an iterable collection yielding 33 | coroutines functions or futures. 34 | limit (int): max concurrency limit. Use ``0`` for no limit. 35 | timeout can be used to control the maximum number 36 | of seconds to wait before returning. timeout can be an int or 37 | float. If timeout is not specified or None, there is no limit to 38 | the wait time. 39 | preserve_order (bool): preserves results order. 40 | return_exceptions (bool): returns exceptions as valid results. 41 | loop (asyncio.BaseEventLoop): optional event loop to use. 42 | 43 | Returns: 44 | list: coroutines returned results. 45 | 46 | Usage:: 47 | 48 | async def sum(x, y): 49 | return x + y 50 | 51 | await paco.gather( 52 | sum(1, 2), 53 | sum(None, 'str'), 54 | return_exceptions=True) 55 | # => [3, TypeError("unsupported operand type(s) for +: 'NoneType' and 'str'")] # noqa 56 | 57 | """ 58 | # If no coroutines to schedule, return empty list (as Python stdlib) 59 | if len(coros_or_futures) == 0: 60 | return [] 61 | 62 | # Support iterable as first argument for better interoperability 63 | if len(coros_or_futures) == 1 and isiter(coros_or_futures[0]): 64 | coros_or_futures = coros_or_futures[0] 65 | 66 | # Pre-initialize results 67 | results = [None] * len(coros_or_futures) if preserve_order else [] 68 | 69 | # Create concurrent executor 70 | pool = ConcurrentExecutor(limit=limit, loop=loop) 71 | 72 | # Iterate and attach coroutine for defer scheduling 73 | for index, coro in enumerate(coros_or_futures): 74 | # Validate coroutine object 75 | if asyncio.iscoroutinefunction(coro): 76 | coro = coro() 77 | if not asyncio.iscoroutine(coro): 78 | raise TypeError( 79 | 'paco: only coroutines or coroutine functions allowed') 80 | 81 | # Add coroutine to the executor pool 82 | pool.add(collect(coro, index, results, 83 | preserve_order=preserve_order, 84 | return_exceptions=return_exceptions)) 85 | 86 | # Wait until all the tasks finishes 87 | yield from pool.run(timeout=timeout, return_exceptions=return_exceptions) 88 | 89 | # Returns aggregated results 90 | return results 91 | -------------------------------------------------------------------------------- /paco/generator.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import asyncio 3 | 4 | PY_35 = sys.version_info >= (3, 5) 5 | 6 | 7 | @asyncio.coroutine 8 | def consume(generator): # pragma: no cover 9 | """ 10 | Helper function to consume a synchronous or asynchronous generator. 11 | 12 | Arguments: 13 | generator (generator|asyncgenerator): generator to consume. 14 | 15 | Returns: 16 | list 17 | """ 18 | # If synchronous generator, just consume and return as list 19 | if hasattr(generator, '__next__'): 20 | return list(generator) 21 | 22 | if not PY_35: 23 | raise RuntimeError( 24 | 'paco: asynchronous iterator protocol not supported') 25 | 26 | # If asynchronous generator, consume it generator protocol manually 27 | buf = [] 28 | while True: 29 | try: 30 | buf.append((yield from generator.__anext__())) 31 | except StopAsyncIteration: # noqa 32 | break 33 | 34 | return buf 35 | -------------------------------------------------------------------------------- /paco/interval.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | from .decorator import decorate 4 | from .assertions import assert_corofunction 5 | 6 | try: 7 | ensure_future = asyncio.ensure_future 8 | except: 9 | ensure_future = getattr(asyncio, 'async') 10 | 11 | 12 | @decorate 13 | def interval(coro, interval=1, times=None, loop=None): 14 | """ 15 | Schedules the execution of a coroutine function every `x` amount of 16 | seconds. 17 | 18 | The function returns an `asyncio.Task`, which implements also an 19 | `asyncio.Future` interface, allowing the user to cancel the execution 20 | cycle. 21 | 22 | This function can be used as decorator. 23 | 24 | Arguments: 25 | coro (coroutinefunction): coroutine function to defer. 26 | interval (int/float): number of seconds to repeat the coroutine 27 | execution. 28 | times (int): optional maximum time of executions. Infinite by default. 29 | loop (asyncio.BaseEventLoop, optional): loop to run. 30 | Defaults to asyncio.get_event_loop(). 31 | 32 | Raises: 33 | TypeError: if coro argument is not a coroutine function. 34 | 35 | Returns: 36 | future (asyncio.Task): coroutine wrapped as task future. 37 | Useful for cancellation and state checking. 38 | 39 | Usage:: 40 | 41 | # Usage as function 42 | future = paco.interval(coro, 1) 43 | 44 | # Cancel it after a while... 45 | await asyncio.sleep(5) 46 | future.cancel() 47 | 48 | # Usage as decorator 49 | @paco.interval(10) 50 | async def metrics(): 51 | await send_metrics() 52 | 53 | future = await metrics() 54 | 55 | """ 56 | assert_corofunction(coro=coro) 57 | 58 | # Store maximum allowed number of calls 59 | times = int(times or 0) or float('inf') 60 | 61 | @asyncio.coroutine 62 | def schedule(times, *args, **kw): 63 | while times > 0: 64 | # Decrement times counter 65 | times -= 1 66 | 67 | # Schedule coroutine 68 | yield from coro(*args, **kw) 69 | yield from asyncio.sleep(interval) 70 | 71 | def wrapper(*args, **kw): 72 | return ensure_future(schedule(times, *args, **kw), loop=loop) 73 | 74 | return wrapper 75 | -------------------------------------------------------------------------------- /paco/map.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | from .each import each 4 | from .decorator import overload 5 | 6 | 7 | @overload 8 | @asyncio.coroutine 9 | def map(coro, iterable, limit=0, loop=None, timeout=None, 10 | return_exceptions=False, *args, **kw): 11 | """ 12 | Concurrently maps values yielded from an iterable, passing then 13 | into an asynchronous coroutine function. 14 | 15 | Mapped values will be returned as list. 16 | Items order will be preserved based on origin iterable order. 17 | 18 | Concurrency level can be configurable via ``limit`` param. 19 | 20 | This function is the asynchronous equivalent port Python built-in 21 | `map()` function. 22 | 23 | This function is a coroutine. 24 | 25 | This function can be composed in a pipeline chain with ``|`` operator. 26 | 27 | Arguments: 28 | coro (coroutinefunction): map coroutine function to use. 29 | iterable (iterable|asynchronousiterable): an iterable collection 30 | yielding coroutines functions. 31 | limit (int): max concurrency limit. Use ``0`` for no limit. 32 | loop (asyncio.BaseEventLoop): optional event loop to use. 33 | timeout (int|float): timeout can be used to control the maximum number 34 | of seconds to wait before returning. timeout can be an int or 35 | float. If timeout is not specified or None, there is no limit to 36 | the wait time. 37 | return_exceptions (bool): returns exceptions as valid results. 38 | *args (mixed): optional variadic arguments to be passed to the 39 | coroutine map function. 40 | 41 | Returns: 42 | list: ordered list of values yielded by coroutines 43 | 44 | Usage:: 45 | 46 | async def mul_2(num): 47 | return num * 2 48 | 49 | await paco.map(mul_2, [1, 2, 3, 4, 5]) 50 | # => [2, 4, 6, 8, 10] 51 | 52 | """ 53 | # Call each iterable but collecting yielded values 54 | return (yield from each(coro, iterable, 55 | limit=limit, loop=loop, 56 | timeout=timeout, collect=True, 57 | return_exceptions=return_exceptions, 58 | *args, **kw)) 59 | -------------------------------------------------------------------------------- /paco/observer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | from inspect import isfunction 4 | 5 | 6 | def coroutine_wrapper(fn): 7 | @asyncio.coroutine 8 | def wrapper(*args, **kw): 9 | return fn(*args, **kw) 10 | return wrapper 11 | 12 | 13 | class Observer(object): 14 | """ 15 | Observer implements a simple observer pub/sub pattern with a minimal 16 | interface and built-in coroutines support for asynchronous-first approach, 17 | desiged as abstract class to be inherited or embed by observable classes. 18 | """ 19 | 20 | def __init__(self): 21 | self._pool = {} 22 | 23 | def observe(self, event, fn): 24 | """ 25 | Arguments: 26 | event (str): event to subscribe. 27 | fn (function|coroutinefunction): function to trigger. 28 | 29 | Raises: 30 | TypeError: if fn argument is not valid 31 | """ 32 | iscoroutine = asyncio.iscoroutinefunction(fn) 33 | if not iscoroutine and not isfunction(fn): 34 | raise TypeError('paco: fn param must be a callable ' 35 | 'object or coroutine function') 36 | 37 | observers = self._pool.get(event) 38 | if not observers: 39 | observers = self._pool[event] = [] 40 | 41 | # Register the observer 42 | observers.append(fn if iscoroutine else coroutine_wrapper(fn)) 43 | 44 | def remove(self, event=None): 45 | """ 46 | Remove all the registered observers for the given event name. 47 | 48 | Arguments: 49 | event (str): event name to remove. 50 | """ 51 | observers = self._pool.get(event) 52 | if observers: 53 | self._pool[event] = [] 54 | 55 | def clear(self): 56 | """ 57 | Clear all the registered observers. 58 | """ 59 | self._pool = {} 60 | 61 | # Shortcut methods 62 | on = observe 63 | off = remove 64 | 65 | @asyncio.coroutine 66 | def trigger(self, event, *args, **kw): 67 | """ 68 | Triggers event observers for the given event name, 69 | passing custom variadic arguments. 70 | """ 71 | observers = self._pool.get(event) 72 | 73 | # If no observers registered for the event, do no-op 74 | if not observers or len(observers) == 0: 75 | return None 76 | 77 | # Trigger observers coroutines in FIFO sequentially 78 | for fn in observers: 79 | # Review: perhaps this should not wait 80 | yield from fn(*args, **kw) 81 | -------------------------------------------------------------------------------- /paco/once.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from .times import times 3 | from .decorator import decorate 4 | 5 | 6 | @decorate 7 | def once(coro, raise_exception=False, return_value=None): 8 | """ 9 | Wrap a given coroutine function that is restricted to one execution. 10 | 11 | Repeated calls to the coroutine function will return the value of the first 12 | invocation. 13 | 14 | This function can be used as decorator. 15 | 16 | arguments: 17 | coro (coroutinefunction): coroutine function to wrap. 18 | raise_exception (bool): raise exception if execution times exceeded. 19 | return_value (mixed): value to return when execution times exceeded, 20 | instead of the memoized one from last invocation. 21 | 22 | Raises: 23 | TypeError: if coro argument is not a coroutine function. 24 | 25 | Returns: 26 | coroutinefunction 27 | 28 | Usage:: 29 | 30 | async def mul_2(num): 31 | return num * 2 32 | 33 | once = paco.once(mul_2) 34 | await once(2) 35 | # => 4 36 | await once(3) 37 | # => 4 38 | 39 | once = paco.once(mul_2, return_value='exceeded') 40 | await once(2) 41 | # => 4 42 | await once(3) 43 | # => 'exceeded' 44 | 45 | """ 46 | return times(coro, 47 | limit=1, 48 | return_value=return_value, 49 | raise_exception=raise_exception) 50 | -------------------------------------------------------------------------------- /paco/partial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | from .decorator import decorate 4 | from .assertions import assert_corofunction 5 | 6 | 7 | @decorate 8 | def partial(coro, *args, **kw): 9 | """ 10 | Partial function implementation designed 11 | for coroutines, allowing variadic input arguments. 12 | 13 | This function can be used as decorator. 14 | 15 | arguments: 16 | coro (coroutinefunction): coroutine function to wrap. 17 | *args (mixed): mixed variadic arguments for partial application. 18 | 19 | Raises: 20 | TypeError: if ``coro`` is not a coroutine function. 21 | 22 | Returns: 23 | coroutinefunction 24 | 25 | Usage:: 26 | 27 | async def pow(x, y): 28 | return x ** y 29 | 30 | pow_2 = paco.partial(pow, 2) 31 | await pow_2(4) 32 | # => 16 33 | 34 | """ 35 | assert_corofunction(coro=coro) 36 | 37 | @asyncio.coroutine 38 | def wrapper(*_args, **_kw): 39 | call_args = args + _args 40 | kw.update(_kw) 41 | return (yield from coro(*call_args, **kw)) 42 | 43 | return wrapper 44 | -------------------------------------------------------------------------------- /paco/pipe.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | import functools 4 | from inspect import isfunction, getargspec 5 | from .generator import consume 6 | from .assertions import isiter 7 | 8 | # Yielded iterable error 9 | IterableError = TypeError('pipeline yielded a non iterable object') 10 | 11 | 12 | class PipeOverloader(object): 13 | """ 14 | Pipe operator overloader object wrapping a given fn. 15 | """ 16 | def __init__(self, fn, args, kw): 17 | self.__fn = fn 18 | self.__args = args 19 | self.__kw = kw 20 | 21 | @asyncio.coroutine 22 | def __await_coro(self, coro): 23 | return (yield from self.__trigger((yield from coro))) 24 | 25 | @asyncio.coroutine 26 | def __consume_generator(self, iterable): 27 | return (yield from self.__trigger((yield from consume(iterable)))) 28 | 29 | def __trigger(self, iterable): 30 | if not isiter(iterable): 31 | raise IterableError 32 | 33 | # Compose arguments, placing iterable as second one 34 | args = self.__args[:1] + (iterable,) + self.__args[1:] 35 | # Call wrapped function 36 | result = self.__fn(*args, **self.__kw) 37 | 38 | # Clean memoized arguments to prevent memory leaks 39 | self.__args = None 40 | self.__kw = None 41 | 42 | # Return actual result 43 | return result 44 | 45 | def __ror__(self, iterable): 46 | """ 47 | Overloads ``|`` operator expressions. 48 | """ 49 | if not iterable: 50 | raise IterableError 51 | 52 | if hasattr(iterable, '__anext__'): 53 | return self.__consume_generator(iterable) 54 | 55 | if asyncio.iscoroutine(iterable): 56 | return self.__await_coro(iterable) 57 | 58 | return self.__trigger(iterable) 59 | 60 | def __call__(self, *args, **kw): 61 | """ 62 | Maintain callable object behaviour. 63 | """ 64 | _args = self.__args + args 65 | _kw = self.__kw 66 | _kw.update(kw) 67 | 68 | # Clean memoized falues 69 | self.__args = None 70 | self.__kw = None 71 | 72 | return self.__fn(*_args, **_kw) 73 | 74 | 75 | def overload(fn): 76 | """ 77 | Overload a given callable object to be used with ``|`` operator 78 | overloading. 79 | 80 | This is especially used for composing a pipeline of 81 | transformation over a single data set. 82 | 83 | Arguments: 84 | fn (function): target function to decorate. 85 | 86 | Raises: 87 | TypeError: if function or coroutine function is not provided. 88 | 89 | Returns: 90 | function: decorated function 91 | """ 92 | if not isfunction(fn): 93 | raise TypeError('paco: fn must be a callable object') 94 | 95 | spec = getargspec(fn) 96 | args = spec.args 97 | if not spec.varargs and (len(args) < 2 or args[1] != 'iterable'): 98 | raise ValueError('paco: invalid function signature or arity') 99 | 100 | @functools.wraps(fn) 101 | def decorator(*args, **kw): 102 | # Check function arity 103 | if len(args) < 2: 104 | return PipeOverloader(fn, args, kw) 105 | # Otherwise, behave like a normal wrapper 106 | return fn(*args, **kw) 107 | 108 | return decorator 109 | -------------------------------------------------------------------------------- /paco/race.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | from .assertions import assert_iter 4 | 5 | try: 6 | from asyncio import ensure_future 7 | except ImportError: 8 | ensure_future = getattr(asyncio, 'async') 9 | 10 | 11 | @asyncio.coroutine 12 | def race(iterable, loop=None, timeout=None, *args, **kw): 13 | """ 14 | Runs coroutines from a given iterable concurrently without waiting until 15 | the previous one has completed. 16 | 17 | Once any of the tasks completes, the main coroutine 18 | is immediately resolved, yielding the first resolved value. 19 | 20 | All coroutines will be executed in the same loop. 21 | 22 | This function is a coroutine. 23 | 24 | Arguments: 25 | iterable (iterable): an iterable collection yielding 26 | coroutines functions or coroutine objects. 27 | *args (mixed): mixed variadic arguments to pass to coroutines. 28 | loop (asyncio.BaseEventLoop): optional event loop to use. 29 | timeout (int|float): timeout can be used to control the maximum number 30 | of seconds to wait before returning. timeout can be an int or 31 | float. If timeout is not specified or None, there is no limit to 32 | the wait time. 33 | *args (mixed): optional variadic argument to pass to coroutine 34 | function, if provided. 35 | 36 | Raises: 37 | TypeError: if ``iterable`` argument is not iterable. 38 | asyncio.TimoutError: if wait timeout is exceeded. 39 | 40 | Returns: 41 | filtered values (list): ordered list of resultant values. 42 | 43 | Usage:: 44 | 45 | async def coro1(): 46 | await asyncio.sleep(2) 47 | return 1 48 | 49 | async def coro2(): 50 | return 2 51 | 52 | async def coro3(): 53 | await asyncio.sleep(1) 54 | return 3 55 | 56 | await paco.race([coro1, coro2, coro3]) 57 | # => 2 58 | 59 | """ 60 | assert_iter(iterable=iterable) 61 | 62 | # Store coros and internal state 63 | coros = [] 64 | resolved = False 65 | result = None 66 | 67 | # Resolve first yielded data from coroutine and stop pending ones 68 | @asyncio.coroutine 69 | def resolver(index, coro): 70 | nonlocal result 71 | nonlocal resolved 72 | 73 | value = yield from coro 74 | if not resolved: 75 | resolved = True 76 | 77 | # Flag as not test passed 78 | result = value 79 | 80 | # Force canceling pending coroutines 81 | for _index, future in enumerate(coros): 82 | if _index != index: 83 | future.cancel() 84 | 85 | # Iterate and attach coroutine for defer scheduling 86 | for index, coro in enumerate(iterable): 87 | # Validate yielded object 88 | isfunction = asyncio.iscoroutinefunction(coro) 89 | if not isfunction and not asyncio.iscoroutine(coro): 90 | raise TypeError( 91 | 'paco: coro must be a coroutine or coroutine function') 92 | 93 | # Init coroutine function, if required 94 | if isfunction: 95 | coro = coro(*args, **kw) 96 | 97 | # Store future tasks 98 | coros.append(ensure_future(resolver(index, coro))) 99 | 100 | # Run coroutines concurrently 101 | yield from asyncio.wait(coros, timeout=timeout, loop=loop) 102 | 103 | return result 104 | -------------------------------------------------------------------------------- /paco/reduce.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | from .decorator import overload 4 | from .concurrent import ConcurrentExecutor 5 | from .assertions import assert_corofunction, assert_iter 6 | 7 | 8 | @overload 9 | @asyncio.coroutine 10 | def reduce(coro, iterable, initializer=None, limit=1, right=False, loop=None): 11 | """ 12 | Apply function of two arguments cumulatively to the items of sequence, 13 | from left to right, so as to reduce the sequence to a single value. 14 | 15 | Reduction will be executed sequentially without concurrency, 16 | so passed values would be in order. 17 | 18 | This function is the asynchronous coroutine equivalent to Python standard 19 | `functools.reduce()` function. 20 | 21 | This function is a coroutine. 22 | 23 | This function can be composed in a pipeline chain with ``|`` operator. 24 | 25 | Arguments: 26 | coro (coroutine function): reducer coroutine binary function. 27 | iterable (iterable|asynchronousiterable): an iterable collection 28 | yielding coroutines functions. 29 | initializer (mixed): initial accumulator value used in 30 | the first reduction call. 31 | limit (int): max iteration concurrency limit. Use ``0`` for no limit. 32 | right (bool): reduce iterable from right to left. 33 | loop (asyncio.BaseEventLoop): optional event loop to use. 34 | 35 | Raises: 36 | TypeError: if input arguments are not valid. 37 | 38 | Returns: 39 | mixed: accumulated final reduced value. 40 | 41 | Usage:: 42 | 43 | async def reducer(acc, num): 44 | return acc + num 45 | 46 | await paco.reduce(reducer, [1, 2, 3, 4, 5], initializer=0) 47 | # => 15 48 | 49 | """ 50 | assert_corofunction(coro=coro) 51 | assert_iter(iterable=iterable) 52 | 53 | # Reduced accumulator value 54 | acc = initializer 55 | 56 | # If interable is empty, just return the initializer value 57 | if len(iterable) == 0: 58 | return initializer 59 | 60 | # Create concurrent executor 61 | pool = ConcurrentExecutor(limit=limit, loop=loop) 62 | 63 | # Reducer partial function for deferred coroutine execution 64 | def reducer(element): 65 | @asyncio.coroutine 66 | def wrapper(): 67 | nonlocal acc 68 | acc = yield from coro(acc, element) 69 | return wrapper 70 | 71 | # Support right reduction 72 | if right: 73 | iterable.reverse() 74 | 75 | # Iterate and attach coroutine for defer scheduling 76 | for element in iterable: 77 | pool.add(reducer(element)) 78 | 79 | # Wait until all coroutines finish 80 | yield from pool.run(ignore_empty=True) 81 | 82 | # Returns final reduced value 83 | return acc 84 | -------------------------------------------------------------------------------- /paco/repeat.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | from .assertions import assert_corofunction 4 | from .map import map 5 | 6 | 7 | @asyncio.coroutine 8 | def repeat(coro, times=1, step=1, limit=1, loop=None): 9 | """ 10 | Executes the coroutine function ``x`` number of times, 11 | and accumulates results in order as you would use with ``map``. 12 | 13 | Execution concurrency is configurable using ``limit`` param. 14 | 15 | This function is a coroutine. 16 | 17 | Arguments: 18 | coro (coroutinefunction): coroutine function to schedule. 19 | times (int): number of times to execute the coroutine. 20 | step (int): increment iteration step, as with ``range()``. 21 | limit (int): concurrency execution limit. Defaults to 10. 22 | loop (asyncio.BaseEventLoop): optional event loop to use. 23 | 24 | Raises: 25 | TypeError: if coro is not a coroutine function. 26 | 27 | Returns: 28 | list: accumulated yielded values returned by coroutine. 29 | 30 | Usage:: 31 | 32 | async def mul_2(num): 33 | return num * 2 34 | 35 | await paco.repeat(mul_2, times=5) 36 | # => [2, 4, 6, 8, 10] 37 | 38 | """ 39 | assert_corofunction(coro=coro) 40 | 41 | # Iterate and attach coroutine for defer scheduling 42 | times = max(int(times), 1) 43 | iterable = range(1, times + 1, step) 44 | 45 | # Run iterable times 46 | return (yield from map(coro, iterable, limit=limit, loop=loop)) 47 | -------------------------------------------------------------------------------- /paco/run.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | 4 | def run(coro, loop=None): 5 | """ 6 | Convenient shortcut alias to ``loop.run_until_complete``. 7 | 8 | Arguments: 9 | coro (coroutine): coroutine object to schedule. 10 | loop (asyncio.BaseEventLoop): optional event loop to use. 11 | Defaults to: ``asyncio.get_event_loop()``. 12 | 13 | Returns: 14 | mixed: returned value by coroutine. 15 | 16 | Usage:: 17 | 18 | async def mul_2(num): 19 | return num * 2 20 | 21 | paco.run(mul_2(4)) 22 | # => 8 23 | 24 | """ 25 | loop = loop or asyncio.get_event_loop() 26 | return loop.run_until_complete(coro) 27 | -------------------------------------------------------------------------------- /paco/series.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | from .gather import gather 4 | 5 | 6 | @asyncio.coroutine 7 | def series(*coros_or_futures, timeout=None, 8 | loop=None, return_exceptions=False): 9 | """ 10 | Run the given coroutine functions in series, each one 11 | running once the previous execution has completed. 12 | 13 | If any coroutines raises an exception, no more 14 | coroutines are executed. Otherwise, the coroutines returned values 15 | will be returned as `list`. 16 | 17 | ``timeout`` can be used to control the maximum number of seconds to 18 | wait before returning. timeout can be an int or float. 19 | If timeout is not specified or None, there is no limit to the wait time. 20 | 21 | If ``return_exceptions`` is True, exceptions in the tasks are treated the 22 | same as successful results, and gathered in the result list; otherwise, 23 | the first raised exception will be immediately propagated to the 24 | returned future. 25 | 26 | All futures must share the same event loop. 27 | 28 | This functions is basically the sequential execution version of 29 | ``asyncio.gather()``. Interface compatible with ``asyncio.gather()``. 30 | 31 | This function is a coroutine. 32 | 33 | Arguments: 34 | *coros_or_futures (iter|list): 35 | an iterable collection yielding coroutines functions. 36 | timeout (int/float): 37 | maximum number of seconds to wait before returning. 38 | return_exceptions (bool): 39 | exceptions in the tasks are treated the same as successful results, 40 | instead of raising them. 41 | loop (asyncio.BaseEventLoop): 42 | optional event loop to use. 43 | *args (mixed): 44 | optional variadic argument to pass to the coroutines function. 45 | 46 | Returns: 47 | list: coroutines returned results. 48 | 49 | Raises: 50 | TypeError: in case of invalid coroutine object. 51 | ValueError: in case of empty set of coroutines or futures. 52 | TimeoutError: if execution takes more than expected. 53 | 54 | Usage:: 55 | 56 | async def sum(x, y): 57 | return x + y 58 | 59 | await paco.series( 60 | sum(1, 2), 61 | sum(2, 3), 62 | sum(3, 4)) 63 | # => [3, 5, 7] 64 | 65 | """ 66 | return (yield from gather(*coros_or_futures, 67 | loop=loop, limit=1, timeout=timeout, 68 | return_exceptions=return_exceptions)) 69 | -------------------------------------------------------------------------------- /paco/some.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | from .partial import partial 4 | from .decorator import overload 5 | from .concurrent import ConcurrentExecutor 6 | from .assertions import assert_corofunction, assert_iter 7 | 8 | 9 | @overload 10 | @asyncio.coroutine 11 | def some(coro, iterable, limit=0, timeout=None, loop=None): 12 | """ 13 | Returns `True` if at least one element in the iterable satisfies the 14 | asynchronous coroutine test. If any iteratee call returns `True`, 15 | iteration stops and `True` will be returned. 16 | 17 | This function is a coroutine. 18 | 19 | This function can be composed in a pipeline chain with ``|`` operator. 20 | 21 | Arguments: 22 | coro (coroutine function): coroutine function for test values. 23 | iterable (iterable|asynchronousiterable): an iterable collection 24 | yielding coroutines functions. 25 | limit (int): max concurrency limit. Use ``0`` for no limit. 26 | timeout can be used to control the maximum number 27 | of seconds to wait before returning. timeout can be an int or 28 | float. If timeout is not specified or None, there is no limit to 29 | the wait time. 30 | loop (asyncio.BaseEventLoop): optional event loop to use. 31 | 32 | Raises: 33 | TypeError: if input arguments are not valid. 34 | 35 | Returns: 36 | bool: `True` if at least on value passes the test, otherwise `False`. 37 | 38 | Usage:: 39 | 40 | async def gt_3(num): 41 | return num > 3 42 | 43 | await paco.some(test, [1, 2, 3, 4, 5]) 44 | # => True 45 | 46 | """ 47 | assert_corofunction(coro=coro) 48 | assert_iter(iterable=iterable) 49 | 50 | # Reduced accumulator value 51 | passes = False 52 | 53 | # If no items in iterable, return False 54 | if len(iterable) == 0: 55 | return passes 56 | 57 | # Create concurrent executor 58 | pool = ConcurrentExecutor(limit=limit, loop=loop) 59 | 60 | # Reducer partial function for deferred coroutine execution 61 | @asyncio.coroutine 62 | def tester(element): 63 | nonlocal passes 64 | if passes: 65 | return None 66 | 67 | if (yield from coro(element)): 68 | # Flag as not test passed 69 | passes = True 70 | # Force stop pending coroutines 71 | pool.cancel() 72 | 73 | # Iterate and attach coroutine for defer scheduling 74 | for element in iterable: 75 | pool.add(partial(tester, element)) 76 | 77 | # Wait until all coroutines finish 78 | yield from pool.run(timeout=timeout) 79 | 80 | return passes 81 | -------------------------------------------------------------------------------- /paco/throttle.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import time 3 | import asyncio 4 | from .decorator import decorate 5 | from .assertions import assert_corofunction 6 | 7 | 8 | def now(): 9 | """ 10 | Returns the current machine time in milliseconds. 11 | """ 12 | return int(round(time.time() * 1000)) 13 | 14 | 15 | @decorate 16 | def throttle(coro, limit=1, timeframe=1, 17 | return_value=None, raise_exception=False): 18 | """ 19 | Creates a throttled coroutine function that only invokes 20 | ``coro`` at most once per every time frame of seconds or milliseconds. 21 | 22 | Provide options to indicate whether func should be invoked on the 23 | leading and/or trailing edge of the wait timeout. 24 | 25 | Subsequent calls to the throttled coroutine 26 | return the result of the last coroutine invocation. 27 | 28 | This function can be used as decorator. 29 | 30 | Arguments: 31 | coro (coroutinefunction): 32 | coroutine function to wrap with throttle strategy. 33 | limit (int): 34 | number of coroutine allowed execution in the given time frame. 35 | timeframe (int|float): 36 | throttle limit time frame in seconds. 37 | return_value (mixed): 38 | optional return if the throttle limit is reached. 39 | Returns the latest returned value by default. 40 | raise_exception (bool): 41 | raise exception if throttle limit is reached. 42 | 43 | Raises: 44 | RuntimeError: if cannot throttle limit reached (optional). 45 | 46 | Returns: 47 | coroutinefunction 48 | 49 | Usage:: 50 | 51 | async def mul_2(num): 52 | return num * 2 53 | 54 | # Use as simple wrapper 55 | throttled = paco.throttle(mul_2, limit=1, timeframe=2) 56 | await throttled(2) 57 | # => 4 58 | await throttled(3) # ignored! 59 | # => 4 60 | await asyncio.sleep(2) 61 | await throttled(3) # executed! 62 | # => 6 63 | 64 | # Use as decorator 65 | @paco.throttle(limit=1, timeframe=2) 66 | async def mul_2(num): 67 | return num * 2 68 | 69 | await mul_2(2) 70 | # => 4 71 | await mul_2(3) # ignored! 72 | # => 4 73 | await asyncio.sleep(2) 74 | await mul_2(3) # executed! 75 | # => 6 76 | 77 | """ 78 | assert_corofunction(coro=coro) 79 | 80 | # Store execution limits 81 | limit = max(int(limit), 1) 82 | remaning = limit 83 | 84 | # Turn seconds in milliseconds 85 | timeframe = timeframe * 1000 86 | 87 | # Keep call state 88 | last_call = now() 89 | # Cache latest retuned result 90 | result = None 91 | 92 | def stop(): 93 | if raise_exception: 94 | raise RuntimeError('paco: coroutine throttle limit exceeded') 95 | if return_value: 96 | return return_value 97 | return result 98 | 99 | def elapsed(): 100 | return now() - last_call 101 | 102 | @asyncio.coroutine 103 | def wrapper(*args, **kw): 104 | nonlocal result 105 | nonlocal remaning 106 | nonlocal last_call 107 | 108 | if elapsed() > timeframe: 109 | # Reset reamining calls counter 110 | remaning = limit 111 | # Update last call time 112 | last_call = now() 113 | elif elapsed() < timeframe and remaning <= 0: 114 | return stop() 115 | 116 | # Decrease remaining limit 117 | remaning -= 1 118 | 119 | # Schedule coroutine passing arguments and cache result 120 | result = yield from coro(*args, **kw) 121 | return result 122 | 123 | return wrapper 124 | -------------------------------------------------------------------------------- /paco/thunk.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | from .assertions import assert_corofunction 4 | 5 | 6 | def thunk(coro): 7 | """ 8 | A thunk is a subroutine that is created, often automatically, to assist 9 | a call to another subroutine. 10 | 11 | Creates a thunk coroutine which returns coroutine function that accepts no 12 | arguments and when invoked it schedules the wrapper coroutine and 13 | returns the final result. 14 | 15 | See Wikipedia page for more information about Thunk subroutines: 16 | https://en.wikipedia.org/wiki/Thunk 17 | 18 | Arguments: 19 | value (coroutinefunction): wrapped coroutine function to invoke. 20 | 21 | Returns: 22 | coroutinefunction 23 | 24 | Usage:: 25 | 26 | async def task(): 27 | return 'foo' 28 | 29 | coro = paco.thunk(task) 30 | 31 | await coro() 32 | # => 'foo' 33 | await coro() 34 | # => 'foo' 35 | 36 | """ 37 | assert_corofunction(coro=coro) 38 | 39 | @asyncio.coroutine 40 | def wrapper(): 41 | return (yield from coro()) 42 | 43 | return wrapper 44 | -------------------------------------------------------------------------------- /paco/timeout.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | from .decorator import decorate 4 | 5 | 6 | @decorate 7 | def timeout(coro, timeout=None, loop=None): 8 | """ 9 | Wraps a given coroutine function, that when executed, if it takes more 10 | than the given timeout in seconds to execute, it will be canceled and 11 | raise an `asyncio.TimeoutError`. 12 | 13 | This function is equivalent to Python standard 14 | `asyncio.wait_for()` function. 15 | 16 | This function can be used as decorator. 17 | 18 | Arguments: 19 | coro (coroutinefunction|coroutine): coroutine to wrap. 20 | timeout (int|float): max wait timeout in seconds. 21 | loop (asyncio.BaseEventLoop): optional event loop to use. 22 | 23 | Raises: 24 | TypeError: if coro argument is not a coroutine function. 25 | 26 | Returns: 27 | coroutinefunction: wrapper coroutine function. 28 | 29 | Usage:: 30 | 31 | await paco.timeout(coro, timeout=10) 32 | 33 | """ 34 | @asyncio.coroutine 35 | def _timeout(coro): 36 | return (yield from asyncio.wait_for(coro, timeout, loop=loop)) 37 | 38 | @asyncio.coroutine 39 | def wrapper(*args, **kw): 40 | return (yield from _timeout(coro(*args, **kw))) 41 | 42 | return _timeout(coro) if asyncio.iscoroutine(coro) else wrapper 43 | 44 | 45 | class TimeoutLimit(object): 46 | """ 47 | Timeout limit context manager. 48 | 49 | Useful in cases when you want to apply timeout logic around block 50 | of code or in cases when asyncio.wait_for is not suitable. 51 | 52 | Originally based on: https://github.com/aio-libs/async-timeout 53 | 54 | Arguments: 55 | timeout (int): value in seconds or None to disable timeout logic. 56 | loop (asyncio.BaseEventLoop): asyncio compatible event loop. 57 | 58 | Usage:: 59 | 60 | with paco.TimeoutLimit(0.1): 61 | await paco.wait(task1, task2) 62 | """ 63 | 64 | def __init__(self, timeout, loop=None): 65 | self._timeout = timeout 66 | self._loop = loop or asyncio.get_event_loop() 67 | self._task = None 68 | self._cancelled = False 69 | self._cancel_handler = None 70 | 71 | def __enter__(self): 72 | self._task = asyncio.Task.current_task(loop=self._loop) 73 | if self._task is None: 74 | raise RuntimeError('paco: timeout context manager should ' 75 | 'be used inside a task') 76 | if self._timeout is not None: 77 | self._cancel_handler = self._loop.call_later( 78 | self._timeout, self.cancel) 79 | return self 80 | 81 | def __exit__(self, exc_type, exc_val, exc_tb): 82 | if exc_type is asyncio.CancelledError and self._cancelled: 83 | self._cancel_handler = None 84 | self._task = None 85 | raise asyncio.TimeoutError from None 86 | if self._timeout is not None: 87 | self._cancel_handler.cancel() 88 | self._cancel_handler = None 89 | self._task = None 90 | 91 | def cancel(self): 92 | """ 93 | Cancels current task running task in the context manager. 94 | """ 95 | self._cancelled = self._task.cancel() 96 | -------------------------------------------------------------------------------- /paco/times.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | from .decorator import decorate 4 | from .assertions import assert_corofunction 5 | 6 | ExceptionMessage = 'paco: coroutine cannot be executed more than {} times' 7 | 8 | 9 | @decorate 10 | def times(coro, limit=1, raise_exception=False, return_value=None): 11 | """ 12 | Wraps a given coroutine function to be executed only a certain amount 13 | of times. 14 | 15 | If the execution limit is exceeded, the last execution return value will 16 | be returned as result. 17 | 18 | You can optionally define a custom return value on exceeded via 19 | `return_value` param. 20 | 21 | This function can be used as decorator. 22 | 23 | arguments: 24 | coro (coroutinefunction): coroutine function to wrap. 25 | limit (int): max limit of coroutine executions. 26 | raise_exception (bool): raise exception if execution times exceeded. 27 | return_value (mixed): value to return when execution times exceeded. 28 | 29 | Raises: 30 | TypeError: if coro argument is not a coroutine function. 31 | RuntimeError: if max execution excedeed (optional). 32 | 33 | Returns: 34 | coroutinefunction 35 | 36 | Usage:: 37 | 38 | async def mul_2(num): 39 | return num * 2 40 | 41 | timed = paco.times(mul_2, 3) 42 | await timed(2) 43 | # => 4 44 | await timed(3) 45 | # => 6 46 | await timed(4) 47 | # => 8 48 | await timed(5) # ignored! 49 | # => 8 50 | """ 51 | assert_corofunction(coro=coro) 52 | 53 | # Store call times 54 | limit = max(limit, 1) 55 | times = limit 56 | 57 | # Store result from last execution 58 | result = None 59 | 60 | @asyncio.coroutine 61 | def wrapper(*args, **kw): 62 | nonlocal limit 63 | nonlocal result 64 | 65 | # Check execution limit 66 | if limit == 0: 67 | if raise_exception: 68 | raise RuntimeError(ExceptionMessage.format(times)) 69 | if return_value: 70 | return return_value 71 | return result 72 | 73 | # Decreases counter 74 | limit -= 1 75 | 76 | # If return_value is present, do not memoize result 77 | if return_value: 78 | return (yield from coro(*args, **kw)) 79 | 80 | # Schedule coroutine and memoize result 81 | result = yield from coro(*args, **kw) 82 | return result 83 | 84 | return wrapper 85 | -------------------------------------------------------------------------------- /paco/until.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | from .whilst import whilst 4 | 5 | 6 | @asyncio.coroutine 7 | def until(coro, coro_test, assert_coro=None, *args, **kw): 8 | """ 9 | Repeatedly call `coro` coroutine function until `coro_test` returns `True`. 10 | 11 | This function is the inverse of `paco.whilst()`. 12 | 13 | This function is a coroutine. 14 | 15 | Arguments: 16 | coro (coroutinefunction): coroutine function to execute. 17 | coro_test (coroutinefunction): coroutine function to test. 18 | assert_coro (coroutinefunction): optional assertion coroutine used 19 | to determine if the test passed or not. 20 | *args (mixed): optional variadic arguments to pass to `coro` function. 21 | 22 | Raises: 23 | TypeError: if input arguments are invalid. 24 | 25 | Returns: 26 | list: result values returned by `coro`. 27 | 28 | Usage:: 29 | 30 | calls = 0 31 | 32 | async def task(): 33 | nonlocal calls 34 | calls += 1 35 | return calls 36 | 37 | async def calls_gt_4(): 38 | return calls > 4 39 | 40 | await paco.until(task, calls_gt_4) 41 | # => [1, 2, 3, 4, 5] 42 | 43 | """ 44 | @asyncio.coroutine 45 | def assert_coro(value): 46 | return not value 47 | 48 | return (yield from whilst(coro, coro_test, 49 | assert_coro=assert_coro, *args, **kw)) 50 | -------------------------------------------------------------------------------- /paco/wait.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | from .assertions import isiter 4 | from .concurrent import ConcurrentExecutor 5 | 6 | 7 | @asyncio.coroutine 8 | def wait(*coros_or_futures, limit=0, timeout=None, loop=None, 9 | return_exceptions=False, return_when='ALL_COMPLETED'): 10 | """ 11 | Wait for the Futures and coroutine objects given by the sequence 12 | futures to complete, with optional concurrency limit. 13 | Coroutines will be wrapped in Tasks. 14 | 15 | ``timeout`` can be used to control the maximum number of seconds to 16 | wait before returning. timeout can be an int or float. 17 | If timeout is not specified or None, there is no limit to the wait time. 18 | 19 | If ``return_exceptions`` is True, exceptions in the tasks are treated the 20 | same as successful results, and gathered in the result list; otherwise, 21 | the first raised exception will be immediately propagated to the 22 | returned future. 23 | 24 | ``return_when`` indicates when this function should return. 25 | It must be one of the following constants of the concurrent.futures module. 26 | 27 | All futures must share the same event loop. 28 | 29 | This functions is mostly compatible with Python standard 30 | ``asyncio.wait()``. 31 | 32 | Arguments: 33 | *coros_or_futures (iter|list): 34 | an iterable collection yielding coroutines functions. 35 | limit (int): 36 | optional concurrency execution limit. Use ``0`` for no limit. 37 | timeout (int/float): 38 | maximum number of seconds to wait before returning. 39 | return_exceptions (bool): 40 | exceptions in the tasks are treated the same as successful results, 41 | instead of raising them. 42 | return_when (str): 43 | indicates when this function should return. 44 | loop (asyncio.BaseEventLoop): 45 | optional event loop to use. 46 | *args (mixed): 47 | optional variadic argument to pass to the coroutines function. 48 | 49 | Returns: 50 | tuple: Returns two sets of Future: (done, pending). 51 | 52 | Raises: 53 | TypeError: in case of invalid coroutine object. 54 | ValueError: in case of empty set of coroutines or futures. 55 | TimeoutError: if execution takes more than expected. 56 | 57 | Usage:: 58 | 59 | async def sum(x, y): 60 | return x + y 61 | 62 | done, pending = await paco.wait( 63 | sum(1, 2), 64 | sum(3, 4)) 65 | [task.result() for task in done] 66 | # => [3, 7] 67 | """ 68 | # Support iterable as first argument for better interoperability 69 | if len(coros_or_futures) == 1 and isiter(coros_or_futures[0]): 70 | coros_or_futures = coros_or_futures[0] 71 | 72 | # If no coroutines to schedule, return empty list 73 | # Mimics asyncio behaviour. 74 | if len(coros_or_futures) == 0: 75 | raise ValueError('paco: set of coroutines/futures is empty') 76 | 77 | # Create concurrent executor 78 | pool = ConcurrentExecutor(limit=limit, loop=loop, 79 | coros=coros_or_futures) 80 | 81 | # Wait until all the tasks finishes 82 | return (yield from pool.run(timeout=timeout, 83 | return_when=return_when, 84 | return_exceptions=return_exceptions)) 85 | -------------------------------------------------------------------------------- /paco/whilst.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | from .filter import assert_true 4 | from .assertions import assert_corofunction 5 | 6 | 7 | @asyncio.coroutine 8 | def whilst(coro, coro_test, assert_coro=None, *args, **kw): 9 | """ 10 | Repeatedly call `coro` coroutine function while `coro_test` returns `True`. 11 | 12 | This function is the inverse of `paco.until()`. 13 | 14 | This function is a coroutine. 15 | 16 | Arguments: 17 | coro (coroutinefunction): coroutine function to execute. 18 | coro_test (coroutinefunction): coroutine function to test. 19 | assert_coro (coroutinefunction): optional assertion coroutine used 20 | to determine if the test passed or not. 21 | *args (mixed): optional variadic arguments to pass to `coro` function. 22 | 23 | Raises: 24 | TypeError: if input arguments are invalid. 25 | 26 | Returns: 27 | list: result values returned by `coro`. 28 | 29 | Usage:: 30 | 31 | calls = 0 32 | 33 | async def task(): 34 | nonlocal calls 35 | calls += 1 36 | return calls 37 | 38 | async def calls_lt_4(): 39 | return calls > 4 40 | 41 | await paco.until(task, calls_lt_4) 42 | # => [1, 2, 3, 4, 5] 43 | 44 | """ 45 | assert_corofunction(coro=coro, coro_test=coro_test) 46 | 47 | # Store yielded values by coroutine 48 | results = [] 49 | 50 | # Set assertion coroutine 51 | assert_coro = assert_coro or assert_true 52 | 53 | # Execute coroutine until a certain 54 | while (yield from assert_coro((yield from coro_test()))): 55 | results.append((yield from coro(*args, **kw))) 56 | 57 | return results 58 | -------------------------------------------------------------------------------- /paco/wraps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | 4 | 5 | def wraps(fn): 6 | """ 7 | Wraps a given function as coroutine function. 8 | 9 | This function can be used as decorator. 10 | 11 | Arguments: 12 | fn (function): function object to wrap. 13 | 14 | Returns: 15 | coroutinefunction: wrapped function as coroutine. 16 | 17 | Usage:: 18 | 19 | def mul_2(num): 20 | return num * 2 21 | 22 | # Use as function wrapper 23 | coro = paco.wraps(mul_2) 24 | await coro(2) 25 | # => 4 26 | 27 | # Use as decorator 28 | @paco.wraps 29 | def mul_2(num): 30 | return num * 2 31 | 32 | await mul_2(2) 33 | # => 4 34 | 35 | """ 36 | return asyncio.coroutine(fn) 37 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | wheel>=0.29 2 | setuptools>=32 3 | coveralls~=1.1 4 | flake8~=3.2.1 5 | pytest~=3.0.3 6 | pytest-cov~=2.4.0 7 | pytest-flakes~=1.0.1 8 | Sphinx~=1.4.8 9 | sphinx-rtd-theme~=0.1.9 10 | python-coveralls~=2.9.0 11 | bumpversion~=0.5.3 12 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | paco 4 | ==== 5 | Utility library for asynchronous coroutine-driven programming for Python +3.4. 6 | 7 | :copyright: (c) 2016-2018 Tomas Aparicio 8 | :license: MIT 9 | """ 10 | 11 | import os 12 | import sys 13 | from setuptools import setup, find_packages 14 | from setuptools.command.test import test as TestCommand 15 | 16 | # Publish command 17 | if sys.argv[-1] == 'publish': 18 | os.system('python setup.py sdist upload') 19 | sys.exit() 20 | 21 | 22 | setup_requires = [] 23 | if 'test' in sys.argv: 24 | setup_requires.append('pytest') 25 | 26 | 27 | def read_version(package): 28 | with open(os.path.join(package, '__init__.py'), 'r') as fd: 29 | for line in fd: 30 | if line.startswith('__version__ = '): 31 | return line.split()[-1].strip().strip("'") 32 | 33 | 34 | # Get package current version 35 | version = read_version('paco') 36 | 37 | 38 | class PyTest(TestCommand): 39 | def finalize_options(self): 40 | TestCommand.finalize_options(self) 41 | self.test_args = ['tests/'] 42 | self.test_suite = True 43 | 44 | def run_tests(self): 45 | # import here, cause outside the eggs aren't loaded 46 | import pytest 47 | errno = pytest.main(self.test_args) 48 | sys.exit(errno) 49 | 50 | 51 | with open('requirements-dev.txt', encoding='utf-8') as f: 52 | tests_require = f.read().splitlines() 53 | with open('README.rst', encoding='utf-8') as f: 54 | readme = f.read() 55 | with open('History.rst', encoding='utf-8') as f: 56 | history = f.read() 57 | 58 | 59 | setup( 60 | name='paco', 61 | version=version, 62 | author='Tomas Aparicio', 63 | author_email='tomas+python@aparicio.me', 64 | description=( 65 | 'Small utility library for coroutine-based ' 66 | 'asynchronous generic programming' 67 | ), 68 | url='https://github.com/h2non/paco', 69 | license='MIT', 70 | long_description=readme + '\n\n' + history, 71 | py_modules=['paco'], 72 | zip_safe=False, 73 | tests_require=tests_require, 74 | packages=find_packages(exclude=['tests', 'examples']), 75 | package_data={'': ['LICENSE', 'History.rst', 'requirements-dev.txt']}, 76 | package_dir={'paco': 'paco'}, 77 | include_package_data=True, 78 | cmdclass={'test': PyTest}, 79 | classifiers=[ 80 | 'Intended Audience :: Developers', 81 | 'Intended Audience :: System Administrators', 82 | 'Operating System :: OS Independent', 83 | 'Development Status :: 5 - Production/Stable', 84 | 'Natural Language :: English', 85 | 'License :: OSI Approved :: MIT License', 86 | 'Programming Language :: Python :: 3.5', 87 | 'Programming Language :: Python :: 3.6', 88 | 'Programming Language :: Python :: 3.7', 89 | 'Programming Language :: Python :: 3.8', 90 | 'Programming Language :: Python :: 3.9', 91 | 'Programming Language :: Python :: 3.10', 92 | 'Programming Language :: Python :: 3.11', 93 | 'Programming Language :: Python :: 3.12', 94 | 'Programming Language :: Python :: 3.13', 95 | 'Topic :: Software Development', 96 | 'Topic :: Software Development :: Libraries :: Python Modules', 97 | 'Programming Language :: Python :: Implementation :: CPython' 98 | ], 99 | ) 100 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2non/paco/a82b50fe3d4e012a9d23aeeb8f9e00b66a513fa0/tests/__init__.py -------------------------------------------------------------------------------- /tests/apply_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | from paco import apply 4 | from .helpers import run_in_loop 5 | 6 | 7 | @asyncio.coroutine 8 | def coro(*args, **kw): 9 | return args, kw 10 | 11 | 12 | def test_apply(): 13 | task = apply(coro, 1, 2, foo='bar') 14 | args, kw = run_in_loop(task) 15 | 16 | assert len(args) == 2 17 | assert len(kw) == 1 18 | assert args == (1, 2) 19 | assert kw == {'foo': 'bar'} 20 | 21 | 22 | def test_apply_variadic_arguments(): 23 | task = apply(coro, *(1, 2, 3, 4)) 24 | args, kw = run_in_loop(task) 25 | 26 | assert len(args) == 4 27 | assert len(kw) == 0 28 | assert args == (1, 2, 3, 4) 29 | 30 | 31 | def test_apply_ignore_call_arguments(): 32 | task = apply(coro, 1, 2, foo='bar') 33 | args, kw = run_in_loop(task, 1, 2, bar='foo') 34 | 35 | assert len(args) == 2 36 | assert len(kw) == 1 37 | assert args == (1, 2) 38 | assert kw == {'foo': 'bar'} 39 | -------------------------------------------------------------------------------- /tests/assertions_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | import asyncio 4 | from paco.assertions import (assert_corofunction, 5 | assert_iter, isiter, 6 | iscoro_or_corofunc, 7 | iscallable, isfunc) 8 | 9 | 10 | def test_isiter(): 11 | assert isiter(()) 12 | assert isiter([]) 13 | assert not isiter('foo') 14 | assert not isiter(bytes()) 15 | assert not isiter(True) 16 | 17 | 18 | def test_iscallable(): 19 | @asyncio.coroutine 20 | def coro(): 21 | pass 22 | 23 | assert iscallable(test_iscallable) 24 | assert iscallable(lambda: True) 25 | assert iscallable(coro) 26 | assert not iscallable(tuple()) 27 | assert not iscallable([]) 28 | assert not iscallable('foo') 29 | assert not iscallable(bytes()) 30 | assert not iscallable(True) 31 | 32 | 33 | def test_isfunc(): 34 | @asyncio.coroutine 35 | def coro(): 36 | pass 37 | 38 | assert isfunc(test_isfunc) 39 | assert isfunc(lambda: True) 40 | assert not isfunc(coro) 41 | assert not isfunc(tuple()) 42 | assert not isfunc([]) 43 | assert not isfunc('foo') 44 | assert not isfunc(bytes()) 45 | assert not isfunc(True) 46 | 47 | 48 | @asyncio.coroutine 49 | def coro(*args, **kw): 50 | return args, kw 51 | 52 | 53 | def test_iscoro_or_(): 54 | assert iscoro_or_corofunc(coro) 55 | assert iscoro_or_corofunc(coro()) 56 | assert not iscoro_or_corofunc(lambda: True) 57 | assert not iscoro_or_corofunc(None) 58 | assert not iscoro_or_corofunc(1) 59 | assert not iscoro_or_corofunc(True) 60 | 61 | 62 | def test_assert_corofunction(): 63 | assert_corofunction(coro=coro) 64 | 65 | with pytest.raises(TypeError, message='coro must be a coroutine function'): 66 | assert_corofunction(coro=None) 67 | 68 | 69 | def test_assert_iter(): 70 | assert_iter(iterable=()) 71 | 72 | with pytest.raises(TypeError, message='iterable must be an iterable'): 73 | assert_iter(iterable=None) 74 | -------------------------------------------------------------------------------- /tests/compose_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import time 3 | import pytest 4 | import asyncio 5 | from paco import compose, partial 6 | from .helpers import run_in_loop 7 | 8 | 9 | @asyncio.coroutine 10 | def coro(num, acc): 11 | yield from asyncio.sleep(0.1) 12 | return acc + (num,) 13 | 14 | 15 | def test_compose(): 16 | task = compose(partial(coro, 1), partial(coro, 2), partial(coro, 3)) 17 | now = time.time() 18 | assert run_in_loop(task, (0,)) == (0, 3, 2, 1) 19 | assert time.time() - now >= 0.3 20 | 21 | 22 | def test_compose_exception(): 23 | count = 0 24 | 25 | @asyncio.coroutine 26 | def coro_exception(x): 27 | nonlocal count 28 | count += 1 29 | 30 | if count == 2: 31 | raise ValueError('foo') 32 | 33 | return x + 1 34 | 35 | task = compose(*(coro_exception,) * 5) 36 | 37 | with pytest.raises(ValueError): 38 | run_in_loop(task, 0) 39 | 40 | assert count == 2 41 | -------------------------------------------------------------------------------- /tests/concurrent_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import time 3 | import pytest 4 | import asyncio 5 | from paco import concurrent 6 | from .helpers import sleep_coro, run_in_loop 7 | 8 | 9 | @asyncio.coroutine 10 | def run_test(limit=3, times=10, timespan=0.1): 11 | p = concurrent(limit) 12 | for i in range(times): 13 | p.add(sleep_coro, timespan) 14 | return (yield from p.run()) 15 | 16 | 17 | def test_concurrent_single(): 18 | @asyncio.coroutine 19 | def coro(num): 20 | return num * 2 21 | 22 | p = concurrent(10) 23 | p.add(coro, 2) 24 | assert len(p) == 1 25 | assert p.__len__() == 1 26 | assert len(p.pool) == 1 27 | 28 | done, pending = run_in_loop(p.run()) 29 | 30 | assert len(done) == 1 31 | assert len(pending) == 0 32 | 33 | for future in done: 34 | assert future.result() == 4 35 | 36 | 37 | def test_concurrent(): 38 | timespan, times, limit = 0.1, 10, 3 39 | start = time.time() 40 | done, pending = run_in_loop( 41 | run_test(limit=limit, times=times, timespan=timespan) 42 | ) 43 | assert time.time() - start >= (times * timespan / limit) 44 | assert len(done) == times 45 | assert len(pending) == 0 46 | 47 | for future in done: 48 | assert future.result() >= 0.1 49 | 50 | 51 | def test_concurrent_high_limit(): 52 | timespan, times, limit = 0.1, 1000, 100 53 | start = time.time() 54 | done, pending = run_in_loop( 55 | run_test(limit=limit, times=times, timespan=timespan) 56 | ) 57 | assert time.time() - start >= (times * timespan / limit) 58 | assert len(done) == times 59 | assert len(pending) == 0 60 | 61 | for future in done: 62 | assert future.result() >= 0.1 63 | 64 | 65 | def test_concurrent_sequential(): 66 | timespan, times, limit = 0.05, 10, 1 67 | start = time.time() 68 | done, pending = run_in_loop( 69 | run_test(limit=limit, times=times, timespan=timespan) 70 | ) 71 | assert time.time() - start >= (times * timespan / limit) 72 | assert len(done) == times 73 | assert len(pending) == 0 74 | 75 | for future in done: 76 | assert future.result() >= 0.05 77 | 78 | 79 | def test_concurrent_ignore_empty(): 80 | runner = concurrent(ignore_empty=True) 81 | done, pending = run_in_loop(runner.run()) 82 | assert len(done) == 0 83 | assert len(pending) == 0 84 | 85 | runner = concurrent() 86 | done, pending = run_in_loop(runner.run(ignore_empty=True)) 87 | assert len(done) == 0 88 | assert len(pending) == 0 89 | 90 | 91 | def test_concurrent_empty_error(): 92 | with pytest.raises(ValueError): 93 | run_in_loop(concurrent().run()) 94 | 95 | 96 | def test_concurrent_observe(): 97 | start = [] 98 | finish = [] 99 | 100 | @asyncio.coroutine 101 | def coro(num): 102 | return num * 2 103 | 104 | @asyncio.coroutine 105 | def on_start(task): 106 | start.append(task) 107 | 108 | @asyncio.coroutine 109 | def on_finish(task, result): 110 | finish.append(result) 111 | 112 | p = concurrent(10) 113 | p.on('task.start', on_start) 114 | p.on('task.finish', on_finish) 115 | 116 | p.add(coro, 2) 117 | p.add(coro, 4) 118 | p.add(coro, 6) 119 | assert len(p.pool) == 3 120 | 121 | done, pending = run_in_loop(p.run()) 122 | assert len(done) == 3 123 | assert len(pending) == 0 124 | 125 | # Assert event calls 126 | assert len(start) == 3 127 | assert len(finish) == 3 128 | 129 | results = [future.result() for future in done] 130 | results.sort() 131 | assert results == [4, 8, 12] 132 | 133 | finish.sort() 134 | assert finish == [4, 8, 12] 135 | 136 | 137 | def test_concurrent_observe_exception(): 138 | start = [] 139 | error = [] 140 | finish = [] 141 | 142 | @asyncio.coroutine 143 | def coro(num): 144 | if num > 4: 145 | raise ValueError('invalid number') 146 | return num * 2 147 | 148 | @asyncio.coroutine 149 | def on_start(task): 150 | start.append(task) 151 | 152 | @asyncio.coroutine 153 | def on_error(task, err): 154 | error.append(err) 155 | 156 | @asyncio.coroutine 157 | def on_finish(task, result): 158 | finish.append(result) 159 | 160 | p = concurrent(1) 161 | p.on('task.start', on_start) 162 | p.on('task.error', on_error) 163 | p.on('task.finish', on_finish) 164 | 165 | p.add(coro, 2) 166 | p.add(coro, 4) 167 | p.add(coro, 6) 168 | assert len(p.pool) == 3 169 | 170 | with pytest.raises(ValueError): 171 | run_in_loop(p.run(return_exceptions=False)) 172 | 173 | # Assert event calls 174 | assert len(start) == 3 175 | assert len(error) == 1 176 | assert len(finish) == 2 177 | 178 | finish.sort() 179 | assert finish == [4, 8] 180 | -------------------------------------------------------------------------------- /tests/constant_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import time 3 | from paco import constant, identity 4 | from .helpers import run_in_loop 5 | 6 | 7 | def test_constant(): 8 | task = constant(1) 9 | assert run_in_loop(task) == 1 10 | 11 | task = constant('foo') 12 | assert run_in_loop(task) == 'foo' 13 | 14 | task = constant({'foo': 'bar'}) 15 | assert run_in_loop(task) == {'foo': 'bar'} 16 | 17 | task = constant((1, 2, 3)) 18 | assert run_in_loop(task) == (1, 2, 3) 19 | 20 | task = constant(None) 21 | assert run_in_loop(task) is None 22 | 23 | 24 | def test_identify(): 25 | task = identity(1) 26 | assert run_in_loop(task) == 1 27 | 28 | task = identity('foo') 29 | assert run_in_loop(task) == 'foo' 30 | 31 | task = identity({'foo': 'bar'}) 32 | assert run_in_loop(task) == {'foo': 'bar'} 33 | 34 | task = identity((1, 2, 3)) 35 | assert run_in_loop(task) == (1, 2, 3) 36 | 37 | task = identity(None) 38 | assert run_in_loop(task) is None 39 | 40 | 41 | def test_constant_delay(): 42 | task = constant(1, delay=0.5) 43 | now = time.time() 44 | assert run_in_loop(task) == 1 45 | assert time.time() - now >= 0.5 46 | -------------------------------------------------------------------------------- /tests/curry_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | from paco import curry 4 | from .helpers import run_in_loop 5 | 6 | 7 | def task(x, y, baz=None, *args, **kw): 8 | return x + y, baz, kw 9 | 10 | 11 | @asyncio.coroutine 12 | def coro(x, y, baz=None, *args, **kw): 13 | return task(x, y, baz=baz, *args, **kw) 14 | 15 | 16 | def test_curry_function_arity(): 17 | num, val, kw = run_in_loop(curry(task)(2)(4)(baz='foo')) 18 | assert num == 6 19 | assert val == 'foo' 20 | assert kw == {} 21 | 22 | num, val, kw = run_in_loop(curry(task)(2, 4)(baz='foo')) 23 | assert num == 6 24 | assert val == 'foo' 25 | assert kw == {} 26 | 27 | num, val, kw = run_in_loop(curry(task)(2, 4, baz='foo')) 28 | assert num == 6 29 | assert val == 'foo' 30 | assert kw == {} 31 | 32 | num, val, kw = run_in_loop(curry(task)(2, 4, baz='foo', fee=True)) 33 | assert num == 6 34 | assert val == 'foo' 35 | assert kw == {'fee': True} 36 | 37 | 38 | def test_curry_single_arity(): 39 | assert run_in_loop(curry(lambda x: x)(True)) 40 | 41 | 42 | def test_curry_zero_arity(): 43 | assert run_in_loop(curry(lambda: True)) 44 | 45 | 46 | def test_curry_custom_arity(): 47 | currier = curry(4) 48 | num, val, kw = run_in_loop(currier(task)(2)(4)(baz='foo')(tee=True)) 49 | assert num == 6 50 | assert val == 'foo' 51 | assert kw == {'tee': True} 52 | 53 | 54 | def test_curry_ignore_kwargs(): 55 | currier = curry(ignore_kwargs=True) 56 | num, val, kw = run_in_loop(currier(task)(2)(4)) 57 | assert num == 6 58 | assert val is None 59 | assert kw == {} 60 | 61 | currier = curry(ignore_kwargs=True) 62 | num, val, kw = run_in_loop(currier(task)(2)(4, baz='foo', tee=True)) 63 | assert num == 6 64 | assert val is 'foo' 65 | assert kw == {'tee': True} 66 | 67 | 68 | def test_curry_extra_arguments(): 69 | currier = curry(4) 70 | num, val, kw = run_in_loop(currier(task)(2)(4)(baz='foo')(tee=True)) 71 | assert num == 6 72 | assert val == 'foo' 73 | assert kw == {'tee': True} 74 | 75 | currier = curry(4) 76 | num, val, kw = run_in_loop(currier(task)(2)(4)(baz='foo')(tee=True)) 77 | assert num == 6 78 | assert val == 'foo' 79 | assert kw == {'tee': True} 80 | 81 | 82 | def test_curry_evaluator_function(): 83 | def evaluator(acc, fn): 84 | return len(acc[0]) < 3 85 | 86 | def task(x, y): 87 | return x * y 88 | 89 | currier = curry(evaluator=evaluator) 90 | assert run_in_loop(currier(task)(4, 4)) == 16 91 | 92 | 93 | def test_curry_decorator(): 94 | @curry 95 | def task(x, y, z): 96 | return x + y + z 97 | 98 | assert run_in_loop(task(2)(4)(8)) == 14 99 | 100 | @curry(4) 101 | def task(x, y, *args): 102 | return x + y + args[0] + args[1] 103 | 104 | assert run_in_loop(task(2)(4)(8)(10)) == 24 105 | 106 | @curry(4) 107 | @asyncio.coroutine 108 | def task(x, y, *args): 109 | return x + y + args[0] + args[1] 110 | 111 | assert run_in_loop(task(2)(4)(8)(10)) == 24 112 | 113 | 114 | def test_curry_coroutine(): 115 | num, val, kw = run_in_loop(curry(coro)(2)(4)(baz='foo', tee=True)) 116 | assert num == 6 117 | assert val == 'foo' 118 | assert kw == {'tee': True} 119 | -------------------------------------------------------------------------------- /tests/decorator_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | import asyncio 4 | from paco.decorator import decorate 5 | from .helpers import run_in_loop 6 | 7 | 8 | @asyncio.coroutine 9 | def coro(*args, **kw): 10 | return args, kw 11 | 12 | 13 | def sample(coro, *args, **kw): 14 | return coro(*args, **kw) 15 | 16 | 17 | def test_decorate_with_arguments(): 18 | wrapper = decorate(sample) 19 | task = wrapper(1, foo='bar') 20 | args, kw = run_in_loop(task, coro, 2, bar='baz') 21 | 22 | assert args == (1, 2) 23 | assert kw == {'foo': 'bar', 'bar': 'baz'} 24 | 25 | 26 | def test_decorate_coro_argument(): 27 | wrapper = decorate(sample) 28 | task = wrapper(coro, 1, foo='bar') 29 | args, kw = run_in_loop(task) 30 | 31 | assert args == (1,) 32 | assert kw == {'foo': 'bar'} 33 | 34 | 35 | def test_decorate_coro_object_argument(): 36 | wrapper = decorate(lambda coro: coro) 37 | task = wrapper(coro(1, foo='bar')) 38 | args, kw = run_in_loop(task) 39 | 40 | assert args == (1,) 41 | assert kw == {'foo': 'bar'} 42 | 43 | 44 | def test_decorate_invalid_input(): 45 | with pytest.raises(TypeError): 46 | decorate(None) 47 | 48 | 49 | def test_decorate_invalid_coroutine(): 50 | with pytest.raises(TypeError): 51 | decorate(sample)(1)() 52 | 53 | 54 | def test_decorate_invalid_coroutine_param(): 55 | with pytest.raises(TypeError): 56 | decorate(sample)(None)(None) 57 | -------------------------------------------------------------------------------- /tests/defer_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import time 3 | import asyncio 4 | from paco import defer 5 | from .helpers import run_in_loop 6 | 7 | 8 | @asyncio.coroutine 9 | def coro(x): 10 | return x 11 | 12 | 13 | def test_defer(): 14 | task = defer(coro, delay=0.2) 15 | now = time.time() 16 | assert run_in_loop(task, 1) == 1 17 | assert time.time() - now >= 0.2 18 | 19 | 20 | def test_defer_decorator(): 21 | task = defer(delay=0.2)(coro) 22 | now = time.time() 23 | assert run_in_loop(task, 1) == 1 24 | assert time.time() - now >= 0.2 25 | -------------------------------------------------------------------------------- /tests/dropwhile_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | import asyncio 4 | from paco import dropwhile 5 | from .helpers import run_in_loop 6 | 7 | 8 | @asyncio.coroutine 9 | def coro(num): 10 | return num < 4 11 | 12 | 13 | def test_dropwhile(): 14 | task = dropwhile(coro, [1, 2, 3, 4, 3, 1]) 15 | assert run_in_loop(task) == [4, 3, 1] 16 | 17 | 18 | def test_dropwhile_empty(): 19 | task = dropwhile(coro, []) 20 | assert run_in_loop(task) == [] 21 | 22 | 23 | def test_dropwhile_invalid_input(): 24 | with pytest.raises(TypeError): 25 | run_in_loop(dropwhile(coro, None)) 26 | -------------------------------------------------------------------------------- /tests/each_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import time 3 | import pytest 4 | import asyncio 5 | from paco import each 6 | from .helpers import run_in_loop 7 | 8 | 9 | @asyncio.coroutine 10 | def coro(num): 11 | return num 12 | 13 | 14 | def test_each(): 15 | calls = 0 16 | 17 | @asyncio.coroutine 18 | def coro(num): 19 | nonlocal calls 20 | calls += 1 21 | 22 | task = each(coro, [1, 2, 3, 4, 5]) 23 | assert run_in_loop(task) is None 24 | assert calls == 5 25 | 26 | 27 | def test_each_collect(): 28 | task = each(coro, [1, 2, 3, 4, 5], collect=True) 29 | assert run_in_loop(task) == [1, 2, 3, 4, 5] 30 | 31 | 32 | def test_each_collect_sequential(): 33 | @asyncio.coroutine 34 | def coro(num): 35 | yield from asyncio.sleep(0.1) 36 | return num 37 | 38 | init = time.time() 39 | task = each(coro, [1, 2, 3, 4, 5], limit=1) 40 | assert run_in_loop(task) is None 41 | assert time.time() - init >= 0.5 42 | 43 | 44 | def test_each_exception(): 45 | @asyncio.coroutine 46 | def coro(num): 47 | yield from asyncio.sleep(0.1) 48 | if num == 4: 49 | raise ValueError('foo') 50 | return num 51 | 52 | init = time.time() 53 | task = each(coro, [1, 2, 3, 4, 5], limit=1) 54 | 55 | with pytest.raises(ValueError): 56 | run_in_loop(task) 57 | 58 | assert time.time() - init >= 0.3 59 | 60 | 61 | def test_each_invalid_input(): 62 | with pytest.raises(TypeError): 63 | run_in_loop(each(coro, None)) 64 | 65 | 66 | def test_each_invalid_coro(): 67 | with pytest.raises(TypeError): 68 | run_in_loop(each(None)) 69 | 70 | 71 | def test_each_return_exceptions(): 72 | @asyncio.coroutine 73 | def coro(num): 74 | raise ValueError('foo') 75 | 76 | task = each(coro, [1, 2, 3, 4, 5], collect=True, return_exceptions=True) 77 | results = run_in_loop(task) 78 | assert len(results) == 5 79 | 80 | for err in results: 81 | assert str(err) == 'foo' 82 | -------------------------------------------------------------------------------- /tests/every_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | import asyncio 4 | from paco import every 5 | from .helpers import run_in_loop 6 | 7 | 8 | @asyncio.coroutine 9 | def coro(num): 10 | return num < 4 11 | 12 | 13 | @asyncio.coroutine 14 | def coro_truly(num): 15 | return num < 10 16 | 17 | 18 | def test_every_truly(): 19 | task = every(coro_truly, [1, 2, 3, 4, 3, 1]) 20 | assert run_in_loop(task) is True 21 | 22 | 23 | def test_every_false(): 24 | task = every(coro, [1, 2, 3, 4, 3, 1]) 25 | assert run_in_loop(task) is False 26 | 27 | 28 | def test_every_empty(): 29 | task = every(coro, []) 30 | assert run_in_loop(task) is True 31 | 32 | 33 | def test_every_invalid_input(): 34 | with pytest.raises(TypeError): 35 | run_in_loop(every(coro, None)) 36 | -------------------------------------------------------------------------------- /tests/filter_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import time 3 | import pytest 4 | import asyncio 5 | from paco import filter 6 | from .helpers import run_in_loop 7 | 8 | 9 | @asyncio.coroutine 10 | def even(num): 11 | return num % 2 == 0 12 | 13 | 14 | def test_filter(): 15 | task = filter(even, [1, 2, 3, 4, 5, 6]) 16 | assert run_in_loop(task) == [2, 4, 6] 17 | 18 | 19 | def test_filter_collect_sequential(): 20 | @asyncio.coroutine 21 | def coro(num): 22 | yield from asyncio.sleep(0.1) 23 | return (yield from even(num)) 24 | 25 | init = time.time() 26 | task = filter(coro, [1, 2, 3, 4, 5, 6], limit=1) 27 | assert run_in_loop(task) == [2, 4, 6] 28 | assert time.time() - init >= 0.5 29 | 30 | 31 | def test_filter_empty(): 32 | assert run_in_loop(filter(even, [])) == [] 33 | 34 | 35 | def test_filter_invalid_input(): 36 | with pytest.raises(TypeError): 37 | run_in_loop(filter(even, None)) 38 | 39 | 40 | def test_filter_invalid_coro(): 41 | with pytest.raises(TypeError): 42 | run_in_loop(filter(None)) 43 | -------------------------------------------------------------------------------- /tests/filterfalse_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import time 3 | import pytest 4 | import asyncio 5 | from paco import filterfalse 6 | from .helpers import run_in_loop 7 | 8 | 9 | @asyncio.coroutine 10 | def even(num): 11 | return num % 2 == 0 12 | 13 | 14 | def test_filterfalse(): 15 | task = filterfalse(even, [1, 2, 3, 4, 5, 6]) 16 | assert run_in_loop(task) == [1, 3, 5] 17 | 18 | 19 | def test_filterfalse_collect_sequential(): 20 | @asyncio.coroutine 21 | def coro(num): 22 | yield from asyncio.sleep(0.1) 23 | return (yield from even(num)) 24 | 25 | init = time.time() 26 | task = filterfalse(coro, [1, 2, 3, 4, 5, 6], limit=1) 27 | assert run_in_loop(task) == [1, 3, 5] 28 | assert time.time() - init >= 0.5 29 | 30 | 31 | def test_filterfalse_invalid_input(): 32 | with pytest.raises(TypeError): 33 | run_in_loop(filterfalse(even, None)) 34 | 35 | 36 | def test_filterfalse_invalid_coro(): 37 | with pytest.raises(TypeError): 38 | run_in_loop(filterfalse(None)) 39 | -------------------------------------------------------------------------------- /tests/flat_map_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | import asyncio 4 | from paco import flat_map 5 | from .helpers import run_in_loop 6 | 7 | 8 | @asyncio.coroutine 9 | def coro(num): 10 | return num * 2 11 | 12 | 13 | def test_flat_map(): 14 | task = flat_map(coro, [1, [2, 3, 4], 5, 6, (7, [8, [(9,)]])]) 15 | results = run_in_loop(task) 16 | results.sort() 17 | assert results == [2, 4, 6, 8, 10, 12, 14, 16, 18] 18 | 19 | 20 | def test_flat_map_sequential(): 21 | task = flat_map(coro, [1, [2], (3, [4]), [5]], limit=1) 22 | assert run_in_loop(task) == [2, 4, 6, 8, 10] 23 | 24 | 25 | def test_flat_map_invalid_input(): 26 | with pytest.raises(TypeError): 27 | run_in_loop(flat_map(coro, None)) 28 | 29 | 30 | def test_flat_map_invalid_coro(): 31 | with pytest.raises(TypeError): 32 | run_in_loop(flat_map(None)) 33 | 34 | 35 | def test_flat_map_pipeline(): 36 | results = run_in_loop([1, [2], [(3, [4]), 5]] | flat_map(coro, limit=1)) 37 | assert results == [2, 4, 6, 8, 10] 38 | -------------------------------------------------------------------------------- /tests/gather_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import time 3 | import pytest 4 | import asyncio 5 | from paco import gather, partial 6 | from .helpers import run_in_loop 7 | 8 | 9 | @asyncio.coroutine 10 | def coro(num): 11 | yield from asyncio.sleep(0.1) 12 | return num * 2 13 | 14 | 15 | def test_gather(): 16 | results = run_in_loop(gather(coro(1), coro(2), coro(3), 17 | preserve_order=True)) 18 | assert results == [2, 4, 6] 19 | 20 | # Test array first argument 21 | results = run_in_loop(gather([coro(1), coro(2), coro(3)], 22 | preserve_order=True)) 23 | assert results == [2, 4, 6] 24 | 25 | 26 | def test_gather_sequential(): 27 | start = time.time() 28 | results = run_in_loop(gather(coro(1), coro(2), coro(3), limit=1)) 29 | assert results == [2, 4, 6] 30 | assert time.time() - start >= 0.3 31 | 32 | 33 | def test_gather_empty(): 34 | results = run_in_loop(gather(limit=1)) 35 | assert results == [] 36 | 37 | 38 | def test_gather_coroutinefunction(): 39 | results = run_in_loop(gather(partial(coro, 1), partial(coro, 2), limit=1)) 40 | assert results == [2, 4] 41 | 42 | 43 | def test_gather_invalid_coro(): 44 | with pytest.raises(TypeError): 45 | run_in_loop(gather(None)) 46 | 47 | 48 | def test_gather_return_exceptions(): 49 | @asyncio.coroutine 50 | def coro(num): 51 | if num == 2: 52 | raise ValueError('foo') 53 | return num * 2 54 | 55 | results = run_in_loop(gather(coro(1), coro(2), coro(3), 56 | return_exceptions=True, preserve_order=True)) 57 | assert results == [2, results[1], 6] 58 | 59 | 60 | def test_gather_no_order(): 61 | start = time.time() 62 | results = run_in_loop(gather(coro(1), coro(2), coro(3))) 63 | assert 2 in results 64 | assert 4 in results 65 | assert 6 in results 66 | assert len(results) == 3 67 | assert time.time() - start < 0.3 68 | -------------------------------------------------------------------------------- /tests/helpers.py: -------------------------------------------------------------------------------- 1 | import time 2 | import asyncio 3 | from inspect import isfunction 4 | 5 | 6 | @asyncio.coroutine 7 | def sleep_coro(timespan=0.1): 8 | start = time.time() 9 | yield from asyncio.sleep(timespan) 10 | return time.time() - start 11 | 12 | 13 | def run_in_loop(coro, *args, **kw): 14 | loop = asyncio.get_event_loop() 15 | if asyncio.iscoroutinefunction(coro) or isfunction(coro): 16 | coro = coro(*args, **kw) 17 | return loop.run_until_complete(coro) 18 | -------------------------------------------------------------------------------- /tests/interval_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import time 3 | import pytest 4 | import asyncio 5 | from paco import interval 6 | from .helpers import run_in_loop 7 | 8 | 9 | @asyncio.coroutine 10 | def coro(track): 11 | track['calls'] += 1 12 | 13 | 14 | def test_interval(): 15 | track = {'calls': 0} 16 | start = time.time() 17 | 18 | task = interval(coro, .1, 5) 19 | future = task(track) 20 | 21 | run_in_loop(future) 22 | 23 | assert future.cancelled 24 | assert track['calls'] == 5 25 | assert time.time() - start > .4 26 | 27 | 28 | def test_interval_cancellation(): 29 | track = {'calls': 0} 30 | start = time.time() 31 | 32 | task = interval(coro, .1) 33 | future = task(track) 34 | 35 | def cancel(): 36 | future.cancel() 37 | 38 | @asyncio.coroutine 39 | def runner(loop): 40 | loop.call_later(1, cancel) 41 | try: 42 | yield from future 43 | except asyncio.CancelledError: 44 | pass 45 | 46 | loop = asyncio.get_event_loop() 47 | loop.run_until_complete(runner(loop)) 48 | 49 | assert future.cancelled 50 | assert track['calls'] > 9 51 | assert time.time() - start > .9 52 | 53 | 54 | def test_interval_decorator(): 55 | track = {'calls': 0} 56 | start = time.time() 57 | 58 | task = interval(.1, 3)(coro) 59 | future = task(track) 60 | 61 | run_in_loop(future) 62 | 63 | assert future.cancelled 64 | assert track['calls'] == 3 65 | assert time.time() - start > .2 66 | 67 | 68 | def test_interval_invalid_input(): 69 | with pytest.raises(TypeError): 70 | run_in_loop(interval(None)) 71 | 72 | with pytest.raises(TypeError): 73 | run_in_loop(interval(lambda x: x)) 74 | -------------------------------------------------------------------------------- /tests/map_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import time 3 | import pytest 4 | import asyncio 5 | from paco import map 6 | from .helpers import run_in_loop 7 | 8 | 9 | @asyncio.coroutine 10 | def coro(num): 11 | return num * 2 12 | 13 | 14 | def test_map(): 15 | task = map(coro, [1, 2, 3, 4, 5]) 16 | assert run_in_loop(task) == [2, 4, 6, 8, 10] 17 | 18 | 19 | def test_map_sequential(): 20 | @asyncio.coroutine 21 | def _coro(num): 22 | yield from asyncio.sleep(0.1) 23 | return (yield from coro(num)) 24 | 25 | init = time.time() 26 | task = map(_coro, [1, 2, 3, 4, 5], limit=1) 27 | assert run_in_loop(task) == [2, 4, 6, 8, 10] 28 | assert time.time() - init >= 0.5 29 | 30 | 31 | def test_map_empty(): 32 | assert run_in_loop(map(coro, [])) == [] 33 | 34 | 35 | def test_map_invalid_input(): 36 | with pytest.raises(TypeError): 37 | run_in_loop(map(coro, None)) 38 | 39 | 40 | def test_map_invalid_coro(): 41 | with pytest.raises(TypeError): 42 | run_in_loop(map(None)) 43 | 44 | 45 | def test_map_return_exceptions(): 46 | @asyncio.coroutine 47 | def coro(num): 48 | raise ValueError('foo') 49 | 50 | task = map(coro, [1, 2, 3, 4, 5], return_exceptions=True) 51 | for exp in run_in_loop(task): 52 | assert isinstance(exp, ValueError) 53 | 54 | 55 | def test_map_raise_exceptions(): 56 | @asyncio.coroutine 57 | def coro(num): 58 | if num > 3: 59 | raise ValueError('foo') 60 | return num * 2 61 | 62 | with pytest.raises(ValueError): 63 | run_in_loop(map(coro, [1, 2, 3, 4, 5], return_exceptions=False)) 64 | -------------------------------------------------------------------------------- /tests/observer_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | from paco.observer import Observer 4 | from .helpers import run_in_loop 5 | 6 | 7 | def test_observer(): 8 | def foo_listener(data, key=None): 9 | assert data == 'foo' 10 | assert key == 'foo' 11 | 12 | @asyncio.coroutine 13 | def bar_listener(data, key=None): 14 | assert data == 'bar' 15 | assert key == 'bar' 16 | 17 | observer = Observer() 18 | 19 | observer.observe('foo', foo_listener) 20 | observer.observe('bar', bar_listener) 21 | assert len(observer._pool) == 2 22 | 23 | run_in_loop(observer.trigger, 'foo', 'foo', key='foo') 24 | run_in_loop(observer.trigger, 'bar', 'bar', key='bar') 25 | 26 | # Event with no listeners 27 | observer.trigger('baz') 28 | 29 | # Remove listenrs 30 | observer.remove('bar') 31 | assert len(observer._pool) == 2 32 | assert len(observer._pool['bar']) == 0 33 | assert len(observer._pool['foo']) == 1 34 | 35 | # Remove all listeners 36 | observer.clear() 37 | assert len(observer._pool) == 0 38 | -------------------------------------------------------------------------------- /tests/once_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | import asyncio 4 | from paco import once 5 | from .helpers import run_in_loop 6 | 7 | 8 | @asyncio.coroutine 9 | def coro(*args, **kw): 10 | return args, kw 11 | 12 | 13 | def test_once(): 14 | task = once(coro) 15 | 16 | args, kw = run_in_loop(task, 1, 2, foo='bar') 17 | assert args == (1, 2) 18 | assert kw == {'foo': 'bar'} 19 | 20 | # Memoized result 21 | args, kw = run_in_loop(task, 3, 4, foo='baz') 22 | assert args == (1, 2) 23 | assert kw == {'foo': 'bar'} 24 | 25 | args, kw = run_in_loop(task, 5, 6, foo='foo') 26 | assert args == (1, 2) 27 | assert kw == {'foo': 'bar'} 28 | 29 | 30 | def test_once_return_value(): 31 | task = once(coro, return_value='ignored') 32 | 33 | args, kw = run_in_loop(task, 1, 2, foo='bar') 34 | assert args == (1, 2) 35 | assert kw == {'foo': 'bar'} 36 | 37 | # Ignored calls 38 | assert run_in_loop(task, 3, foo='foo') == 'ignored' 39 | assert run_in_loop(task, 4, foo='baz') == 'ignored' 40 | 41 | 42 | def test_once_raise_exception(): 43 | task = once(coro, raise_exception=True) 44 | 45 | args, kw = run_in_loop(task, 1, 2, foo='bar') 46 | assert args == (1, 2) 47 | assert kw == {'foo': 'bar'} 48 | 49 | with pytest.raises(RuntimeError): 50 | run_in_loop(task, 3, foo='foo') 51 | 52 | 53 | def test_once_invalid_coro(): 54 | with pytest.raises(TypeError): 55 | once(None) 56 | -------------------------------------------------------------------------------- /tests/partial_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | from paco import partial 4 | from .helpers import run_in_loop 5 | 6 | 7 | @asyncio.coroutine 8 | def coro(*args, **kw): 9 | return args, kw 10 | 11 | 12 | def test_partial(): 13 | task = partial(coro, 1, 2, foo='bar') 14 | args, kw = run_in_loop(task, 3, 4, bar='baz') 15 | 16 | assert len(args) == 4 17 | assert len(kw) == 2 18 | assert args == (1, 2, 3, 4) 19 | assert kw == {'foo': 'bar', 'bar': 'baz'} 20 | 21 | 22 | def test_partial_variadic_arguments(): 23 | task = partial(coro, 1) 24 | args, kw = run_in_loop(task, *(2, 3, 4)) 25 | 26 | assert len(args) == 4 27 | assert len(kw) == 0 28 | assert args == (1, 2, 3, 4) 29 | 30 | 31 | def test_partial_keyword_params_overwrite(): 32 | task = partial(coro, foo='bar', bar='baz') 33 | args, kw = run_in_loop(task, bar='foo') 34 | 35 | assert len(args) == 0 36 | assert len(kw) == 2 37 | assert kw == {'foo': 'bar', 'bar': 'foo'} 38 | 39 | 40 | def test_partial_no_extra_arguments(): 41 | task = partial(coro, *(1, 2, 3, 4), foo='bar') 42 | args, kw = run_in_loop(task) 43 | 44 | assert len(args) == 4 45 | assert len(kw) == 1 46 | assert args == (1, 2, 3, 4) 47 | assert kw == {'foo': 'bar'} 48 | -------------------------------------------------------------------------------- /tests/pipe_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | import pytest 4 | import asyncio 5 | import paco 6 | from paco.pipe import overload 7 | 8 | 9 | @asyncio.coroutine 10 | def filterer(x): 11 | return x < 8 12 | 13 | 14 | @asyncio.coroutine 15 | def mapper(x): 16 | return x * 2 17 | 18 | 19 | @asyncio.coroutine 20 | def drop(x): 21 | return x < 10 22 | 23 | 24 | @asyncio.coroutine 25 | def reducer(acc, x): 26 | return acc + x 27 | 28 | 29 | def test_pipe_operator_overload(): 30 | @asyncio.coroutine 31 | def task(numbers): 32 | return (yield from (numbers 33 | | paco.filter(filterer) 34 | | paco.map(mapper) 35 | | paco.dropwhile(drop) 36 | | paco.reduce(reducer, initializer=0))) 37 | 38 | result = paco.run(task((1, 2, 3, 4, 5, 6, 7, 8, 9, 10))) 39 | assert result == 36 40 | 41 | 42 | @pytest.mark.skipif(sys.version_info < (3, 5), reason='requires Python 3.5+') 43 | def test_pipe_async_generator(): 44 | class AsyncGenerator(object): 45 | def __init__(self, values=None): 46 | self.pos = 0 47 | self.values = values or [1, 2, 3] 48 | 49 | @asyncio.coroutine 50 | def __aiter__(self): 51 | self.pos = 0 52 | return self 53 | 54 | @asyncio.coroutine 55 | def __anext__(self): 56 | if self.pos == len(self.values): 57 | raise StopAsyncIteration # noqa 58 | 59 | value = self.values[self.pos] 60 | self.pos += 1 61 | return value 62 | 63 | @asyncio.coroutine 64 | def task(numbers): 65 | return (yield from (AsyncGenerator(numbers) 66 | | paco.map(mapper) 67 | | paco.reduce(reducer, initializer=0))) 68 | 69 | result = paco.run(task([1, 2, 3, 4, 5])) 70 | assert result == 30 71 | 72 | 73 | def test_overload_error(): 74 | with pytest.raises(TypeError, message='fn must be a callable object'): 75 | overload(None) 76 | 77 | with pytest.raises(ValueError, 78 | messsage='invalid function signature or arity'): 79 | overload(lambda x: True) 80 | 81 | with pytest.raises(ValueError, 82 | messsage='invalid function signature or arity'): 83 | overload(lambda x, y: True) 84 | -------------------------------------------------------------------------------- /tests/race_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | import asyncio 4 | from paco import race 5 | from .helpers import run_in_loop 6 | 7 | 8 | def coro(delay=1): 9 | @asyncio.coroutine 10 | def wrapper(): 11 | yield from asyncio.sleep(delay) 12 | return delay 13 | return wrapper 14 | 15 | 16 | def test_race(): 17 | faster = run_in_loop( 18 | race((coro(0.8), coro(0.5), coro(1), coro(3), coro(0.2))) 19 | ) 20 | assert faster == 0.2 21 | 22 | 23 | def test_race_timeout(): 24 | faster = run_in_loop( 25 | race((coro(1), coro(1)), timeout=0.1) 26 | ) 27 | assert faster is None 28 | 29 | 30 | def test_race_invalid_input(): 31 | with pytest.raises(TypeError): 32 | run_in_loop(race(None)) 33 | 34 | 35 | def test_race_invalid_iterable_value(): 36 | with pytest.raises(TypeError): 37 | run_in_loop(race([None])) 38 | -------------------------------------------------------------------------------- /tests/reduce_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | import asyncio 4 | from paco import reduce 5 | from .helpers import run_in_loop 6 | 7 | 8 | @asyncio.coroutine 9 | def coro(acc, num): 10 | return acc + (num * 2,) 11 | 12 | 13 | @asyncio.coroutine 14 | def sumfn(acc, num): 15 | return acc + num 16 | 17 | 18 | def test_reduce(): 19 | task = reduce(coro, [1, 2, 3, 4, 5], initializer=()) 20 | assert run_in_loop(task) == (2, 4, 6, 8, 10) 21 | 22 | 23 | def test_reduce_right(): 24 | task = reduce(coro, [1, 2, 3, 4, 5], initializer=(), right=True) 25 | assert run_in_loop(task) == (10, 8, 6, 4, 2) 26 | 27 | 28 | def test_reduce_acc(): 29 | task = reduce(sumfn, [1, 2, 3, 4, 5], initializer=0) 30 | assert run_in_loop(task) == 15 31 | 32 | 33 | def test_reduce_empty(): 34 | assert run_in_loop(reduce(coro, (), initializer=1)) == 1 35 | 36 | 37 | def test_reduce_invalid_input(): 38 | with pytest.raises(TypeError): 39 | run_in_loop(reduce(coro, None)) 40 | 41 | 42 | def test_reduce_invalid_coro(): 43 | with pytest.raises(TypeError): 44 | run_in_loop(reduce(None)) 45 | -------------------------------------------------------------------------------- /tests/repeat_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | import asyncio 4 | from paco import repeat 5 | from .helpers import run_in_loop 6 | 7 | 8 | def test_repeat(): 9 | calls = 0 10 | 11 | @asyncio.coroutine 12 | def coro(num): 13 | nonlocal calls 14 | calls += 1 15 | return num * 2 16 | 17 | assert run_in_loop(repeat(coro, 5)) == [2, 4, 6, 8, 10] 18 | assert calls == 5 19 | 20 | 21 | def test_repeat_defaults(): 22 | @asyncio.coroutine 23 | def coro(num): 24 | return num * 2 25 | 26 | assert run_in_loop(repeat(coro)) == [2] 27 | 28 | 29 | def test_repeat_concurrency(): 30 | @asyncio.coroutine 31 | def coro(num): 32 | return num * 2 33 | 34 | assert run_in_loop(repeat(coro, 5), limit=5) == [2, 4, 6, 8, 10] 35 | 36 | 37 | def test_repeat_invalid_input(): 38 | with pytest.raises(TypeError): 39 | run_in_loop(repeat(None)) 40 | -------------------------------------------------------------------------------- /tests/run_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | from paco import run 4 | 5 | 6 | @asyncio.coroutine 7 | def coro(num): 8 | return num * 2 9 | 10 | 11 | def test_run(): 12 | assert run(coro(2)) == 4 13 | 14 | 15 | def test_run_loop(): 16 | loop = asyncio.get_event_loop() 17 | assert run(coro(2), loop=loop) == 4 18 | -------------------------------------------------------------------------------- /tests/series_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import time 3 | import asyncio 4 | from paco import series 5 | from .helpers import run_in_loop 6 | 7 | 8 | @asyncio.coroutine 9 | def coro(num): 10 | yield from asyncio.sleep(0.1) 11 | return num * 2 12 | 13 | 14 | def test_series(): 15 | init = time.time() 16 | task = series(coro(1), coro(2), coro(3)) 17 | assert run_in_loop(task) == [2, 4, 6] 18 | assert time.time() - init >= 0.3 19 | 20 | 21 | def test_series_return_exceptions(): 22 | @asyncio.coroutine 23 | def coro(num): 24 | raise ValueError('foo') 25 | 26 | task = series(coro(1), coro(2), coro(3), return_exceptions=True) 27 | results = run_in_loop(task) 28 | assert len(results) == 3 29 | 30 | for err in results: 31 | assert str(err) == 'foo' 32 | -------------------------------------------------------------------------------- /tests/some_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | import asyncio 4 | from paco import some 5 | from .helpers import run_in_loop 6 | 7 | 8 | @asyncio.coroutine 9 | def coro(num): 10 | return num < 2 11 | 12 | 13 | @asyncio.coroutine 14 | def coro_false(num): 15 | return num > 10 16 | 17 | 18 | def test_some_truly(): 19 | task = some(coro, [1, 2, 3, 4, 3, 1]) 20 | assert run_in_loop(task) is True 21 | 22 | 23 | def test_some_false(): 24 | task = some(coro_false, [1, 2, 3, 4, 3, 1]) 25 | assert run_in_loop(task) is False 26 | 27 | 28 | def test_some_empty(): 29 | task = some(coro, []) 30 | assert run_in_loop(task) is False 31 | 32 | 33 | def test_some_invalid_input(): 34 | with pytest.raises(TypeError): 35 | run_in_loop(some(coro, None)) 36 | -------------------------------------------------------------------------------- /tests/throttle_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import time 3 | import pytest 4 | import asyncio 5 | from paco import throttle 6 | from .helpers import run_in_loop 7 | 8 | 9 | @asyncio.coroutine 10 | def coro(num): 11 | return num 12 | 13 | 14 | def test_throttle(): 15 | task = throttle(coro, limit=2, timeframe=0.2) 16 | assert run_in_loop(task, 1) == 1 17 | assert run_in_loop(task, 2) == 2 18 | assert run_in_loop(task, 3) == 2 19 | assert run_in_loop(task, 4) == 2 20 | time.sleep(0.3) 21 | assert run_in_loop(task, 5) == 5 22 | assert run_in_loop(task, 6) == 6 23 | assert run_in_loop(task, 7) == 6 24 | assert run_in_loop(task, 8) == 6 25 | 26 | 27 | def test_throttle_return_value(): 28 | task = throttle(coro, limit=2, timeframe=0.2, return_value='ignored') 29 | assert run_in_loop(task, 1) == 1 30 | assert run_in_loop(task, 2) == 2 31 | assert run_in_loop(task, 3) == 'ignored' 32 | assert run_in_loop(task, 4) == 'ignored' 33 | time.sleep(0.3) 34 | assert run_in_loop(task, 5) == 5 35 | assert run_in_loop(task, 6) == 6 36 | assert run_in_loop(task, 7) == 'ignored' 37 | assert run_in_loop(task, 8) == 'ignored' 38 | 39 | 40 | def test_throttle_raise_exception(): 41 | task = throttle(coro, limit=1, timeframe=1, raise_exception=True) 42 | assert run_in_loop(task, 1) == 1 43 | 44 | with pytest.raises(RuntimeError): 45 | run_in_loop(task, 2) 46 | 47 | 48 | def test_throttle_invalid_coro(): 49 | with pytest.raises(TypeError): 50 | throttle(None) 51 | 52 | 53 | def test_decorator(): 54 | task = throttle(limit=2, timeframe=0.2)(coro) 55 | assert run_in_loop(task, 1) == 1 56 | assert run_in_loop(task, 2) == 2 57 | assert run_in_loop(task, 3) == 2 58 | assert run_in_loop(task, 4) == 2 59 | -------------------------------------------------------------------------------- /tests/thunk_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | import asyncio 4 | from paco import thunk 5 | from .helpers import run_in_loop 6 | 7 | 8 | @asyncio.coroutine 9 | def task(): 10 | return 'foo' 11 | 12 | 13 | def test_thunk(): 14 | coro = thunk(task) 15 | assert run_in_loop(coro()) == 'foo' 16 | assert run_in_loop(coro()) == 'foo' 17 | assert run_in_loop(coro()) == 'foo' 18 | 19 | 20 | def test_thunk_error(): 21 | with pytest.raises(TypeError): 22 | run_in_loop(None) 23 | 24 | with pytest.raises(TypeError): 25 | run_in_loop(1) 26 | 27 | with pytest.raises(TypeError): 28 | run_in_loop('foo') 29 | -------------------------------------------------------------------------------- /tests/timeout_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import time 3 | import pytest 4 | import asyncio 5 | from paco import timeout, TimeoutLimit, run 6 | from .helpers import run_in_loop 7 | 8 | try: 9 | from asyncio import ensure_future 10 | except ImportError: 11 | ensure_future = asyncio.async 12 | 13 | 14 | @asyncio.coroutine 15 | def coro(delay=1): 16 | yield from asyncio.sleep(delay) 17 | 18 | 19 | def test_timeout(): 20 | task = timeout(coro, timeout=1) 21 | now = time.time() 22 | run_in_loop(task, delay=0.2) 23 | assert time.time() - now >= 0.2 24 | 25 | 26 | def test_timeout_exceeded(): 27 | task = timeout(coro, timeout=0.2) 28 | now = time.time() 29 | 30 | with pytest.raises(asyncio.TimeoutError): 31 | run_in_loop(task, delay=1) 32 | 33 | assert time.time() - now >= 0.2 34 | 35 | 36 | def test_timeout_decorator(): 37 | task = timeout(timeout=1)(coro) 38 | now = time.time() 39 | run_in_loop(task, delay=0.2) 40 | assert time.time() - now >= 0.2 41 | 42 | 43 | def test_timeout_coroutine_object(): 44 | now = time.time() 45 | 46 | with pytest.raises(asyncio.TimeoutError): 47 | @asyncio.coroutine 48 | def _run(): 49 | task = timeout(coro(delay=1), timeout=0.2) 50 | return (yield from task) 51 | 52 | run(_run()) 53 | 54 | assert time.time() - now >= 0.2 55 | 56 | 57 | def test_timeout_limit_context(): 58 | now = time.time() 59 | 60 | @asyncio.coroutine 61 | def test(): 62 | with TimeoutLimit(timeout=0.2): 63 | yield from coro(delay=1) 64 | 65 | with pytest.raises(asyncio.TimeoutError): 66 | run(test()) 67 | 68 | assert time.time() - now >= 0.2 69 | 70 | 71 | def test_timeout_limit_out_of_context(): 72 | with pytest.raises(RuntimeError, message='Timeout context manager ' 73 | 'should be used inside a task'): 74 | with TimeoutLimit(timeout=1): 75 | pass 76 | 77 | 78 | def create_future(loop): 79 | """ 80 | Compatibility wrapper for the loop.create_future() call 81 | introduced in 3.5.2. 82 | """ 83 | if hasattr(loop, 'create_future'): 84 | return loop.create_future() 85 | else: 86 | return asyncio.Future(loop=loop) 87 | -------------------------------------------------------------------------------- /tests/times_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | import asyncio 4 | from paco import times 5 | from .helpers import run_in_loop 6 | 7 | 8 | @asyncio.coroutine 9 | def coro(*args, **kw): 10 | return args, kw 11 | 12 | 13 | def test_times(): 14 | task = times(coro, 2) 15 | 16 | args, kw = run_in_loop(task, 1, 2, foo='bar') 17 | assert args == (1, 2) 18 | assert kw == {'foo': 'bar'} 19 | 20 | args, kw = run_in_loop(task, 3, 4, foo='baz') 21 | assert args == (3, 4) 22 | assert kw == {'foo': 'baz'} 23 | 24 | # Memoized result 25 | args, kw = run_in_loop(task, 5, 6, foo='foo') 26 | assert args == (3, 4) 27 | assert kw == {'foo': 'baz'} 28 | 29 | args, kw = run_in_loop(task, 7, 8, foo='foo') 30 | assert args == (3, 4) 31 | assert kw == {'foo': 'baz'} 32 | 33 | 34 | def test_times_return_value(): 35 | task = times(coro, 1, return_value='ignored') 36 | 37 | args, kw = run_in_loop(task, 1, 2, foo='bar') 38 | assert args == (1, 2) 39 | assert kw == {'foo': 'bar'} 40 | 41 | # Memoized result 42 | assert run_in_loop(task, 3, foo='foo') == 'ignored' 43 | assert run_in_loop(task, 4, foo='baz') == 'ignored' 44 | 45 | 46 | def test_times_raise_exception(): 47 | task = times(coro, 1, raise_exception=True) 48 | 49 | args, kw = run_in_loop(task, 1, 2, foo='bar') 50 | assert args == (1, 2) 51 | assert kw == {'foo': 'bar'} 52 | 53 | with pytest.raises(RuntimeError): 54 | run_in_loop(task, 3, foo='foo') 55 | 56 | 57 | def test_times_invalid_coro(): 58 | with pytest.raises(TypeError): 59 | times(None) 60 | -------------------------------------------------------------------------------- /tests/until_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | import asyncio 4 | from paco import until 5 | from .helpers import run_in_loop 6 | 7 | 8 | def test_until(): 9 | calls = 0 10 | 11 | @asyncio.coroutine 12 | def coro_test(): 13 | return calls > 4 14 | 15 | @asyncio.coroutine 16 | def coro(): 17 | nonlocal calls 18 | calls += 1 19 | return calls 20 | 21 | task = until(coro, coro_test) 22 | assert run_in_loop(task) == [1, 2, 3, 4, 5] 23 | 24 | 25 | def test_until_invalid_input(): 26 | with pytest.raises(TypeError): 27 | run_in_loop(until(None, None)) 28 | -------------------------------------------------------------------------------- /tests/wait_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import time 3 | import pytest 4 | import asyncio 5 | from paco import wait 6 | from .helpers import run_in_loop 7 | 8 | 9 | @asyncio.coroutine 10 | def coro(num): 11 | yield from asyncio.sleep(0.1) 12 | return num * 2 13 | 14 | 15 | def test_wait(limit=0): 16 | done, pending = run_in_loop(wait([coro(1), coro(2), coro(3)], limit=limit)) 17 | assert len(done) == 3 18 | assert len(pending) == 0 19 | 20 | for future in done: 21 | assert future.result() < 7 22 | 23 | 24 | def test_wait_sequential(): 25 | start = time.time() 26 | test_wait(limit=1) 27 | assert time.time() - start >= 0.3 28 | 29 | 30 | def test_wait_return_exceptions(): 31 | @asyncio.coroutine 32 | def coro(num): 33 | raise ValueError('foo') 34 | 35 | done, pending = run_in_loop(wait([coro(1), coro(2), coro(3)], 36 | return_exceptions=True)) 37 | assert len(done) == 3 38 | assert len(pending) == 0 39 | 40 | for future in done: 41 | assert str(future.result()) == 'foo' 42 | 43 | 44 | def test_wait_empty(): 45 | with pytest.raises(ValueError): 46 | run_in_loop(wait([])) 47 | -------------------------------------------------------------------------------- /tests/whilst_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | import asyncio 4 | from paco import whilst 5 | from .helpers import run_in_loop 6 | 7 | 8 | def test_whilst(): 9 | calls = 0 10 | 11 | @asyncio.coroutine 12 | def coro_test(): 13 | return calls < 5 14 | 15 | @asyncio.coroutine 16 | def coro(): 17 | nonlocal calls 18 | calls += 1 19 | return calls 20 | 21 | task = whilst(coro, coro_test) 22 | assert run_in_loop(task) == [1, 2, 3, 4, 5] 23 | 24 | 25 | def test_whilst_invalid_input(): 26 | with pytest.raises(TypeError): 27 | run_in_loop(whilst(None, None)) 28 | -------------------------------------------------------------------------------- /tests/wraps_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | from paco import wraps 4 | from .helpers import run_in_loop 5 | 6 | 7 | def task(*args, **kw): 8 | return args, kw 9 | 10 | 11 | def test_wraps(): 12 | coro = wraps(task) 13 | 14 | args, kw = run_in_loop(coro, 1, 2, foo='bar') 15 | assert args == (1, 2) 16 | assert kw == {'foo': 'bar'} 17 | 18 | args, kw = run_in_loop(coro, 3, 4, foo='baz') 19 | assert args == (3, 4) 20 | assert kw == {'foo': 'baz'} 21 | 22 | args, kw = run_in_loop(coro, 5, 6, foo='foo') 23 | assert args == (5, 6) 24 | assert kw == {'foo': 'foo'} 25 | 26 | 27 | def test_wraps_coroutine(): 28 | @asyncio.coroutine 29 | def coro(x, foo=None): 30 | return x * 2, foo 31 | 32 | coro = wraps(coro) 33 | num, foo = run_in_loop(coro, 2, foo='bar') 34 | assert num == 4 35 | assert foo == 'bar' 36 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = {py34,py35,py36} 3 | 4 | [testenv] 5 | setenv = 6 | PYTHONPATH = {toxinidir} 7 | deps = 8 | -r{toxinidir}/requirements-dev.txt 9 | commands = 10 | py.test . --cov paco --cov-report term-missing --flakes 11 | --------------------------------------------------------------------------------