├── t ├── __init__.py └── unit │ ├── __init__.py │ ├── test_abstract.py │ ├── test_synchronization.py │ ├── test_funtools.py │ └── test_promises.py ├── docs ├── _static │ └── .keep ├── changelog.rst ├── images │ ├── favicon.ico │ └── celery_128.png ├── reference │ ├── vine.utils.rst │ ├── index.rst │ ├── vine.abstract.rst │ ├── vine.funtools.rst │ ├── vine.promises.rst │ └── vine.synchronization.rst ├── index.rst ├── includes │ └── introduction.txt ├── conf.py ├── templates │ └── readme.txt ├── _templates │ └── sidebardonations.html ├── make.bat └── Makefile ├── requirements ├── test.txt ├── test-ci.txt ├── docs.txt └── pkgutils.txt ├── AUTHORS ├── .coveragerc ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── codeql-analysis.yml ├── .editorconfig ├── MANIFEST.in ├── SECURITY.md ├── .bumpversion.cfg ├── .cookiecutterrc ├── .gitignore ├── setup.cfg ├── appveyor.yml ├── .pre-commit-config.yaml ├── vine ├── utils.py ├── __init__.py ├── abstract.py ├── funtools.py ├── synchronization.py └── promises.py ├── .travis.yml ├── tox.ini ├── README.rst ├── LICENSE ├── setup.py ├── Makefile └── Changelog /t/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_static/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /t/unit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements/test.txt: -------------------------------------------------------------------------------- 1 | pytest>=7.2 2 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../Changelog 2 | -------------------------------------------------------------------------------- /requirements/test-ci.txt: -------------------------------------------------------------------------------- 1 | pytest-cov 2 | codecov 3 | -------------------------------------------------------------------------------- /requirements/docs.txt: -------------------------------------------------------------------------------- 1 | sphinx_celery>=1.1 2 | six # missing dependency in sphinx_celery 3 | -------------------------------------------------------------------------------- /docs/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celery/vine/master/docs/images/favicon.ico -------------------------------------------------------------------------------- /docs/images/celery_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celery/vine/master/docs/images/celery_128.png -------------------------------------------------------------------------------- /requirements/pkgutils.txt: -------------------------------------------------------------------------------- 1 | setuptools>=59.2.0 2 | wheel>=0.37.0 3 | flake8>=4.0.1 4 | tox>=3.24.4 5 | sphinx2rst>=1.0 6 | bumpversion 7 | pydocstyle 8 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Ask Solem 2 | Asif Saif Uddin 3 | Ionel Cristian Mărieș 4 | Fahad Siddiqui 5 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = 1 3 | cover_pylib = 0 4 | include=*vine/* 5 | omit = vine.tests.* 6 | 7 | [report] 8 | omit = 9 | */python?.?/* 10 | */site-packages/* 11 | */pypy/* 12 | *vine/five.py 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "pip" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [Makefile] 14 | indent_style = tab 15 | -------------------------------------------------------------------------------- /docs/reference/vine.utils.rst: -------------------------------------------------------------------------------- 1 | ===================================================== 2 | vine.utils 3 | ===================================================== 4 | 5 | .. contents:: 6 | :local: 7 | .. currentmodule:: vine.utils 8 | 9 | .. automodule:: vine.utils 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /docs/reference/index.rst: -------------------------------------------------------------------------------- 1 | .. _apiref: 2 | 3 | =============== 4 | API Reference 5 | =============== 6 | 7 | :Release: |version| 8 | :Date: |today| 9 | 10 | .. toctree:: 11 | :maxdepth: 1 12 | 13 | vine.promises 14 | vine.synchronization 15 | vine.funtools 16 | vine.abstract 17 | vine.utils 18 | -------------------------------------------------------------------------------- /docs/reference/vine.abstract.rst: -------------------------------------------------------------------------------- 1 | ===================================================== 2 | vine.abstract 3 | ===================================================== 4 | 5 | .. contents:: 6 | :local: 7 | .. currentmodule:: vine.abstract 8 | 9 | .. automodule:: vine.abstract 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /docs/reference/vine.funtools.rst: -------------------------------------------------------------------------------- 1 | ===================================================== 2 | vine.funtools 3 | ===================================================== 4 | 5 | .. contents:: 6 | :local: 7 | .. currentmodule:: vine.funtools 8 | 9 | .. automodule:: vine.funtools 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /docs/reference/vine.promises.rst: -------------------------------------------------------------------------------- 1 | ===================================================== 2 | vine.promises 3 | ===================================================== 4 | 5 | .. contents:: 6 | :local: 7 | .. currentmodule:: vine.promises 8 | 9 | .. automodule:: vine.promises 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst Changelog LICENSE 2 | recursive-include docs * 3 | recursive-include extra README *.py 4 | recursive-include requirements *.txt 5 | recursive-include t *.py 6 | 7 | recursive-exclude docs/_build * 8 | recursive-exclude * __pycache__ 9 | recursive-exclude * *.py[co] 10 | recursive-exclude * .*.sw* 11 | -------------------------------------------------------------------------------- /docs/reference/vine.synchronization.rst: -------------------------------------------------------------------------------- 1 | ===================================================== 2 | vine.synchronization 3 | ===================================================== 4 | 5 | .. contents:: 6 | :local: 7 | .. currentmodule:: vine.synchronization 8 | 9 | .. automodule:: vine.synchronization 10 | :members: 11 | :undoc-members: 12 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | 6 | | Version | Supported | 7 | | ------- | ------------------ | 8 | | 5.1.x | :white_check_mark: | 9 | | 2.0.x | :x: | 10 | | 5.0.x | :white_check_mark: | 11 | | < 5.0 | :x: | 12 | 13 | ## Reporting a Vulnerability 14 | 15 | please report Vulnerability to auvipy@gmail.com. 16 | -------------------------------------------------------------------------------- /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 5.1.0 3 | commit = True 4 | tag = True 5 | parse = (?P\d+)\.(?P\d+)\.(?P\d+)(?P[a-z\d]+)? 6 | serialize = 7 | {major}.{minor}.{patch}{releaselevel} 8 | {major}.{minor}.{patch} 9 | 10 | [bumpversion:file:vine/__init__.py] 11 | 12 | [bumpversion:file:docs/includes/introduction.txt] 13 | 14 | [bumpversion:file:README.rst] 15 | -------------------------------------------------------------------------------- /.cookiecutterrc: -------------------------------------------------------------------------------- 1 | default_context: 2 | 3 | email: 'ask@celeryproject.org' 4 | full_name: 'Ask Solem' 5 | github_username: 'celery' 6 | project_name: 'vine' 7 | project_short_description: 'Promises, promises, promises.' 8 | project_slug: 'vine' 9 | version: '1.0.0' 10 | year: '2016' 11 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ============================================= 2 | vine - Python Promises 3 | ============================================= 4 | 5 | .. include:: includes/introduction.txt 6 | 7 | Contents 8 | ======== 9 | 10 | .. toctree:: 11 | :maxdepth: 2 12 | 13 | reference/index 14 | changelog 15 | 16 | Indices and tables 17 | ================== 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *$py.class 4 | *~ 5 | .*.sw[pon] 6 | dist/ 7 | *.egg-info 8 | *.egg 9 | *.egg/ 10 | build/ 11 | .build/ 12 | _build/ 13 | pip-log.txt 14 | .directory 15 | erl_crash.dump 16 | *.db 17 | Documentation/ 18 | .tox/ 19 | .ropeproject/ 20 | .project 21 | .pydevproject 22 | .idea/ 23 | .coverage 24 | celery/tests/cover/ 25 | .ve* 26 | cover/ 27 | .vagrant/ 28 | coverage.xml 29 | htmlcov/ 30 | .cache/ 31 | .python-version 32 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | testpaths = t/unit/ 3 | python_classes = test_* 4 | 5 | [pydocstyle] 6 | ignore = D102,D104,D203,D105,D213,D107,D407,D412,D413 7 | 8 | [flake8] 9 | # classes can be lowercase, arguments and variables can be uppercase 10 | # whenever it makes the code more readable. 11 | extend-ignore = N806, N802, N801, N803 12 | 13 | [bdist_wheel] 14 | universal = 0 15 | 16 | [metadata] 17 | license_files = LICENSE 18 | 19 | [isort] 20 | profile=black 21 | -------------------------------------------------------------------------------- /t/unit/test_abstract.py: -------------------------------------------------------------------------------- 1 | from vine.abstract import Thenable 2 | from vine.promises import promise 3 | 4 | 5 | class CanThen: 6 | 7 | def then(self, x, y): 8 | pass 9 | 10 | 11 | class CannotThen: 12 | pass 13 | 14 | 15 | class test_Thenable: 16 | 17 | def test_isa(self): 18 | assert isinstance(CanThen(), Thenable) 19 | assert not isinstance(CannotThen(), Thenable) 20 | 21 | def test_promise(self): 22 | assert isinstance(promise(lambda x: x), Thenable) 23 | -------------------------------------------------------------------------------- /docs/includes/introduction.txt: -------------------------------------------------------------------------------- 1 | :Version: 5.1.0 2 | :Web: https://vine.readthedocs.io/ 3 | :Download: https://pypi.org/project/vine/ 4 | :Source: https://github.com/celery/vine/ 5 | :Keywords: promise, async, future 6 | 7 | About 8 | ===== 9 | 10 | This is a special implementation of promises in that it can be used both 11 | for promise of a value and lazy evaluation. The biggest upside for this 12 | is that everything in a promise can also be a promise, e.g. filters, 13 | callbacks and errbacks can all be promises. 14 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - TOXENV: "3.9" 4 | TOX_APPVEYOR_X64: 0 5 | 6 | - TOXENV: "3.7" 7 | TOX_APPVEYOR_X64: 0 8 | 9 | - TOXENV: "3.8" 10 | TOX_APPVEYOR_X64: 0 11 | 12 | - TOXENV: "3.9" 13 | TOX_APPVEYOR_X64: 1 14 | 15 | - TOXENV: "3.7" 16 | TOX_APPVEYOR_X64: 1 17 | 18 | - TOXENV: "3.8" 19 | TOX_APPVEYOR_X64: 1 20 | 21 | build: off 22 | 23 | install: 24 | - "py -3.8 -m pip install -U pip setuptools wheel tox tox-appveyor" 25 | 26 | test_script: 27 | - "py -3.8 -m tox" 28 | 29 | cache: 30 | - '%LOCALAPPDATA%\pip\Cache' 31 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/asottile/pyupgrade 3 | rev: v3.21.2 4 | hooks: 5 | - id: pyupgrade 6 | args: ["--py36-plus"] 7 | 8 | - repo: https://github.com/PyCQA/flake8 9 | rev: 7.3.0 10 | hooks: 11 | - id: flake8 12 | 13 | - repo: https://github.com/asottile/yesqa 14 | rev: v1.5.0 15 | hooks: 16 | - id: yesqa 17 | 18 | - repo: https://github.com/pre-commit/pre-commit-hooks 19 | rev: v6.0.0 20 | hooks: 21 | - id: check-merge-conflict 22 | - id: check-toml 23 | - id: check-yaml 24 | - id: mixed-line-ending 25 | 26 | - repo: https://github.com/pycqa/isort 27 | rev: 7.0.0 28 | hooks: 29 | - id: isort 30 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | from sphinx_celery import conf 2 | 3 | globals().update(conf.build_config( 4 | 'vine', __file__, 5 | project='Vine', 6 | description='Python Promises', 7 | # version_dev='2.0', 8 | # version_stable='1.0', 9 | canonical_url='https://vine.readthedocs.io', 10 | webdomain='celeryproject.org', 11 | github_project='celery/vine', 12 | author='Ask Solem & contributors', 13 | author_name='Ask Solem', 14 | copyright='2016', 15 | publisher='Celery Project', 16 | html_logo='images/celery_128.png', 17 | html_favicon='images/favicon.ico', 18 | html_prepend_sidebars=['sidebardonations.html'], 19 | extra_extensions=[], 20 | include_intersphinx={'python', 'sphinx'}, 21 | apicheck_ignore_modules=['vine'], 22 | )) 23 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [pull_request, push] 3 | jobs: 4 | 5 | #################### Unittests #################### 6 | unittest: 7 | runs-on: blacksmith-4vcpu-ubuntu-2204 8 | strategy: 9 | matrix: 10 | python-version: [3.8,3.9,"3.10","3.12"] 11 | steps: 12 | - name: Check out code from GitHub 13 | uses: actions/checkout@v6 14 | - name: Set up Python ${{ matrix.python-version }} 15 | id: python 16 | uses: useblacksmith/setup-python@v6 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install dependencies 20 | run: pip install --upgrade pip setuptools wheel tox 21 | - name: Run unittest 22 | run: tox -v -e ${{ matrix.python-version }}-unit -- -v 23 | -------------------------------------------------------------------------------- /vine/utils.py: -------------------------------------------------------------------------------- 1 | """Python compatibility utilities.""" 2 | from functools import WRAPPER_ASSIGNMENTS, WRAPPER_UPDATES, partial 3 | from functools import update_wrapper as _update_wrapper 4 | 5 | __all__ = ['update_wrapper', 'wraps'] 6 | 7 | 8 | def update_wrapper(wrapper, wrapped, *args, **kwargs): 9 | """Update wrapper, also setting .__wrapped__.""" 10 | wrapper = _update_wrapper(wrapper, wrapped, *args, **kwargs) 11 | wrapper.__wrapped__ = wrapped 12 | return wrapper 13 | 14 | 15 | def wraps(wrapped, 16 | assigned=WRAPPER_ASSIGNMENTS, 17 | updated=WRAPPER_UPDATES): 18 | """Backport of Python 3.5 wraps that adds .__wrapped__.""" 19 | return partial(update_wrapper, wrapped=wrapped, 20 | assigned=assigned, updated=updated) 21 | 22 | 23 | def reraise(tp, value, tb=None): 24 | """Reraise exception.""" 25 | if value.__traceback__ is not tb: 26 | raise value.with_traceback(tb) 27 | raise value 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: bionic 2 | language: python 3 | cache: pip 4 | 5 | stages: 6 | - lint 7 | - test 8 | 9 | env: 10 | global: 11 | PYTHONUNBUFFERED=yes 12 | 13 | matrix: 14 | include: 15 | - python: 3.6 16 | env: TOXENV=3.6 17 | - python: 3.7 18 | env: TOXENV=3.7 19 | - python: 3.8 20 | env: TOXENV=3.8 21 | - python: pypy3.6-7.3.1 22 | env: TOXENV=pypy3 23 | - python: 3.8 24 | env: TOXENV=flake8 25 | stage: lint 26 | - python: 3.8 27 | env: TOXENV=apicheck 28 | stage: lint 29 | - python: 3.8 30 | env: TOXENV=pydocstyle 31 | stage: lint 32 | 33 | install: 34 | - pip install -U pip setuptools wheel | cat 35 | - pip install -U tox | cat 36 | script: tox -v -- -v 37 | after_success: 38 | - .tox/$TRAVIS_PYTHON_VERSION/bin/coverage xml 39 | - .tox/$TRAVIS_PYTHON_VERSION/bin/codecov -e TOXENV 40 | notifications: 41 | irc: 42 | channels: 43 | - "chat.freenode.net#celery" 44 | on_success: change 45 | on_failure: change 46 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | {pypy3,3.12,3.8,3.9,3.10}-unit 4 | flake8 5 | apicheck 6 | pydocstyle 7 | 8 | 9 | [testenv] 10 | deps= 11 | -r{toxinidir}/requirements/test.txt 12 | -r{toxinidir}/requirements/test-ci.txt 13 | 14 | apicheck,linkcheck: -r{toxinidir}/requirements/docs.txt 15 | flake8,pydocstyle: -r{toxinidir}/requirements/pkgutils.txt 16 | sitepackages = False 17 | recreate = False 18 | commands = py.test -xv --cov=vine --cov-report=xml --no-cov-on-fail t/unit {posargs} 19 | 20 | basepython = 21 | flake8,apicheck,linkcheck,pydocstyle: python3.10 22 | pypy3: pypy3.10 23 | 3.8: python3.8 24 | 3.9: python3.9 25 | 3.10: python3.10 26 | 3.12: python3.12 27 | 28 | [testenv:apicheck] 29 | commands = 30 | sphinx-build -b apicheck -d {envtmpdir}/doctrees docs docs/_build/apicheck 31 | 32 | [testenv:linkcheck] 33 | commands = 34 | sphinx-build -W -b linkcheck -d {envtmpdir}/doctrees docs docs/_build/linkcheck 35 | 36 | [testenv:flake8] 37 | commands = 38 | flake8 {toxinidir}/vine {toxinidir}/t 39 | 40 | [testenv:pydocstyle] 41 | commands = 42 | pydocstyle {toxinidir}/vine 43 | -------------------------------------------------------------------------------- /vine/__init__.py: -------------------------------------------------------------------------------- 1 | """Python promises.""" 2 | import re 3 | from collections import namedtuple 4 | 5 | from .abstract import Thenable 6 | from .funtools import ( 7 | ensure_promise, 8 | maybe_promise, 9 | ppartial, 10 | preplace, 11 | starpromise, 12 | transform, 13 | wrap, 14 | ) 15 | from .promises import promise 16 | from .synchronization import barrier 17 | 18 | __version__ = '5.1.0' 19 | __author__ = 'Ask Solem' 20 | __contact__ = 'auvipy@gmail.com' 21 | __homepage__ = 'https://github.com/celery/vine' 22 | __docformat__ = 'restructuredtext' 23 | 24 | # -eof meta- 25 | 26 | version_info_t = namedtuple('version_info_t', ( 27 | 'major', 'minor', 'micro', 'releaselevel', 'serial', 28 | )) 29 | # bump version can only search for {current_version} 30 | # so we have to parse the version here. 31 | _temp = re.match( 32 | r'(\d+)\.(\d+).(\d+)(.+)?', __version__).groups() 33 | VERSION = version_info = version_info_t( 34 | int(_temp[0]), int(_temp[1]), int(_temp[2]), _temp[3] or '', '') 35 | del (_temp) 36 | del (re) 37 | 38 | __all__ = [ 39 | 'Thenable', 'promise', 'barrier', 40 | 'maybe_promise', 'ensure_promise', 41 | 'ppartial', 'preplace', 'starpromise', 'transform', 'wrap', 42 | ] 43 | -------------------------------------------------------------------------------- /docs/templates/readme.txt: -------------------------------------------------------------------------------- 1 | ===================================================================== 2 | vine - Python Promises 3 | ===================================================================== 4 | 5 | |build-status| |coverage| |license| |wheel| |pyversion| |pyimp| 6 | 7 | .. include:: ../includes/introduction.txt 8 | 9 | .. |build-status| image:: https://secure.travis-ci.org/celery/vine.png?branch=master 10 | :alt: Build status 11 | :target: https://travis-ci.org/celery/vine 12 | 13 | .. |coverage| image:: https://codecov.io/github/celery/vine/coverage.svg?branch=master 14 | :target: https://codecov.io/github/celery/vine?branch=master 15 | 16 | .. |license| image:: https://img.shields.io/pypi/l/vine.svg 17 | :alt: BSD License 18 | :target: https://opensource.org/licenses/BSD-3-Clause 19 | 20 | .. |wheel| image:: https://img.shields.io/pypi/wheel/vine.svg 21 | :alt: Vine can be installed via wheel 22 | :target: https://pypi.org/project/vine/ 23 | 24 | .. |pyversion| image:: https://img.shields.io/pypi/pyversions/vine.svg 25 | :alt: Supported Python versions. 26 | :target: https://pypi.org/project/vine/ 27 | 28 | .. |pyimp| image:: https://img.shields.io/pypi/implementation/vine.svg 29 | :alt: Support Python implementations. 30 | :target: https://pypi.org/project/vine/ 31 | -------------------------------------------------------------------------------- /t/unit/test_synchronization.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock 2 | 3 | import pytest 4 | 5 | from vine.promises import promise 6 | from vine.synchronization import barrier 7 | 8 | 9 | class test_barrier: 10 | 11 | def setup_method(self): 12 | self.m1, self.m2, self.m3 = Mock(), Mock(), Mock() 13 | self.ps = [promise(self.m1), promise(self.m2), promise(self.m3)] 14 | 15 | def test_evaluate(self): 16 | x = barrier(self.ps) 17 | x() 18 | assert not x.ready 19 | x() 20 | assert not x.ready 21 | x.add(promise()) 22 | x() 23 | assert not x.ready 24 | x() 25 | assert x.ready 26 | x() 27 | x() 28 | 29 | with pytest.raises(ValueError): 30 | x.add(promise()) 31 | 32 | def test_reverse(self): 33 | callback = Mock() 34 | x = barrier(self.ps, callback=promise(callback)) 35 | for p in self.ps: 36 | p() 37 | assert x.ready 38 | callback.assert_called_with() 39 | 40 | def test_cancel(self): 41 | x = barrier(self.ps) 42 | x.cancel() 43 | for p in self.ps: 44 | p() 45 | x.add(promise()) 46 | x.throw(KeyError()) 47 | assert not x.ready 48 | 49 | def test_throw(self): 50 | x = barrier(self.ps) 51 | with pytest.raises(KeyError): 52 | x.throw(KeyError(10)) 53 | -------------------------------------------------------------------------------- /vine/abstract.py: -------------------------------------------------------------------------------- 1 | """Abstract classes.""" 2 | import abc 3 | from collections.abc import Callable 4 | 5 | __all__ = ['Thenable'] 6 | 7 | 8 | class Thenable(Callable, metaclass=abc.ABCMeta): # pragma: no cover 9 | """Object that supports ``.then()``.""" 10 | 11 | __slots__ = () 12 | 13 | @abc.abstractmethod 14 | def then(self, on_success, on_error=None): 15 | raise NotImplementedError() 16 | 17 | @abc.abstractmethod 18 | def throw(self, exc=None, tb=None, propagate=True): 19 | raise NotImplementedError() 20 | 21 | @abc.abstractmethod 22 | def cancel(self): 23 | raise NotImplementedError() 24 | 25 | @classmethod 26 | def __subclasshook__(cls, C): 27 | if cls is Thenable: 28 | if any('then' in B.__dict__ for B in C.__mro__): 29 | return True 30 | return NotImplemented 31 | 32 | @classmethod 33 | def register(cls, other): 34 | # overide to return other so `register` can be used as a decorator 35 | type(cls).register(cls, other) 36 | return other 37 | 38 | 39 | @Thenable.register 40 | class ThenableProxy: 41 | """Proxy to object that supports ``.then()``.""" 42 | 43 | def _set_promise_target(self, p): 44 | self._p = p 45 | 46 | def then(self, on_success, on_error=None): 47 | return self._p.then(on_success, on_error) 48 | 49 | def cancel(self): 50 | return self._p.cancel() 51 | 52 | def throw1(self, exc=None): 53 | return self._p.throw1(exc) 54 | 55 | def throw(self, exc=None, tb=None, propagate=True): 56 | return self._p.throw(exc, tb=tb, propagate=propagate) 57 | 58 | @property 59 | def cancelled(self): 60 | return self._p.cancelled 61 | 62 | @property 63 | def ready(self): 64 | return self._p.ready 65 | 66 | @property 67 | def failed(self): 68 | return self._p.failed 69 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ===================================================================== 2 | vine - Python Promises 3 | ===================================================================== 4 | 5 | |build-status| |coverage| |license| |wheel| |pyversion| |pyimp| 6 | 7 | :Version: 5.1.0 8 | :Web: https://vine.readthedocs.io/ 9 | :Download: https://pypi.org/project/vine/ 10 | :Source: https://github.com/celery/vine/ 11 | :DeepWiki: |deepwiki| 12 | :Keywords: promise, async, future 13 | 14 | About 15 | ===== 16 | 17 | This is a special implementation of promises in that it can be used both 18 | for promise of a value and lazy evaluation. The biggest upside for this 19 | is that everything in a promise can also be a promise, e.g. filters, 20 | callbacks and errbacks can all be promises. 21 | 22 | .. |build-status| image:: https://secure.travis-ci.org/celery/vine.png?branch=master 23 | :alt: Build status 24 | :target: https://travis-ci.org/celery/vine 25 | 26 | .. |coverage| image:: https://codecov.io/github/celery/vine/coverage.svg?branch=master 27 | :target: https://codecov.io/github/celery/vine?branch=master 28 | 29 | .. |license| image:: https://img.shields.io/pypi/l/vine.svg 30 | :alt: BSD License 31 | :target: https://opensource.org/licenses/BSD-3-Clause 32 | 33 | .. |wheel| image:: https://img.shields.io/pypi/wheel/vine.svg 34 | :alt: Vine can be installed via wheel 35 | :target: https://pypi.org/project/vine/ 36 | 37 | .. |pyversion| image:: https://img.shields.io/pypi/pyversions/vine.svg 38 | :alt: Supported Python versions. 39 | :target: https://pypi.org/project/vine/ 40 | 41 | .. |pyimp| image:: https://img.shields.io/pypi/implementation/vine.svg 42 | :alt: Support Python implementations. 43 | :target: https://pypi.org/project/vine/ 44 | 45 | .. |deepwiki| image:: https://devin.ai/assets/deepwiki-badge.png 46 | :alt: Ask http://DeepWiki.com 47 | :target: https://deepwiki.com/celery/vine 48 | :width: 125px 49 | 50 | -------------------------------------------------------------------------------- /t/unit/test_funtools.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock 2 | 3 | import pytest 4 | 5 | from vine.abstract import Thenable 6 | from vine.funtools import ( 7 | maybe_promise, 8 | ppartial, 9 | preplace, 10 | ready_promise, 11 | starpromise, 12 | transform, 13 | wrap, 14 | ) 15 | from vine.promises import promise 16 | 17 | 18 | def test_wrap(): 19 | cb1 = Mock() 20 | cb2 = Mock() 21 | x = wrap(promise(cb1)) 22 | x(1, y=2) 23 | cb1.assert_called_with(1, y=2) 24 | p2 = promise(cb2) 25 | x(p2) 26 | p2() 27 | cb1.assert_called_with(cb2()) 28 | 29 | 30 | def test_transform(): 31 | callback = Mock() 32 | 33 | def filter_key_value(key, filter_, mapping): 34 | return filter_(mapping[key]) 35 | 36 | x = transform(filter_key_value, promise(callback), 'Value', int) 37 | x({'Value': 303}) 38 | callback.assert_called_with(303) 39 | 40 | with pytest.raises(KeyError): 41 | x({}) 42 | 43 | 44 | class test_maybe_promise: 45 | 46 | def test_when_none(self): 47 | assert maybe_promise(None) is None 48 | 49 | def test_when_promise(self): 50 | p = promise() 51 | assert maybe_promise(p) is p 52 | 53 | def test_when_other(self): 54 | m = Mock() 55 | p = maybe_promise(m) 56 | assert isinstance(p, Thenable) 57 | 58 | 59 | def test_starpromise(): 60 | m = Mock() 61 | p = starpromise(m, 1, 2, z=3) 62 | p() 63 | m.assert_called_with(1, 2, z=3) 64 | 65 | 66 | def test_ready_promise(): 67 | m = Mock() 68 | p = ready_promise(m, 1, 2, 3) 69 | m.assert_called_with(1, 2, 3) 70 | assert p.ready 71 | 72 | 73 | def test_ppartial(): 74 | m = Mock() 75 | p = ppartial(m, 1) 76 | p() 77 | m.assert_called_with(1) 78 | p = ppartial(m, z=2) 79 | p() 80 | m.assert_called_with(z=2) 81 | 82 | 83 | def test_preplace(): 84 | m = Mock() 85 | p = promise(m) 86 | p2 = preplace(p, 1, 2, z=3) 87 | p2(4, 5, x=3) 88 | m.assert_called_with(1, 2, z=3) 89 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | 21 | jobs: 22 | analyze: 23 | name: Analyze 24 | runs-on: blacksmith-4vcpu-ubuntu-2204 25 | permissions: 26 | actions: read 27 | contents: read 28 | security-events: write 29 | 30 | strategy: 31 | fail-fast: false 32 | matrix: 33 | language: [ 'python' ] 34 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 35 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 36 | 37 | steps: 38 | - name: Checkout repository 39 | uses: actions/checkout@v6 40 | 41 | # Initializes the CodeQL tools for scanning. 42 | - name: Initialize CodeQL 43 | uses: github/codeql-action/init@v4 44 | with: 45 | languages: ${{ matrix.language }} 46 | # If you wish to specify custom queries, you can do so here or in a config file. 47 | # By default, queries listed here will override any specified in a config file. 48 | # Prefix the list here with "+" to use these queries and those in the config file. 49 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 50 | 51 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 52 | # If this step fails, then you should remove it and run the build manually (see below) 53 | - name: Autobuild 54 | uses: github/codeql-action/autobuild@v4 55 | 56 | # ℹ️ Command-line programs to run using the OS shell. 57 | # 📚 https://git.io/JvXDl 58 | 59 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 60 | # and modify them (or add more) to build your code if your project 61 | # uses a compiled language 62 | 63 | #- run: | 64 | # make bootstrap 65 | # make release 66 | 67 | - name: Perform CodeQL Analysis 68 | uses: github/codeql-action/analyze@v4 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2016 Ask Solem & contributors. All rights reserved. 2 | 3 | Vine is licensed under The BSD License (3 Clause, also known as 4 | the new BSD license). The license is an OSI approved Open Source 5 | license and is GPL-compatible(1). 6 | 7 | The license text can also be found here: 8 | http://www.opensource.org/licenses/BSD-3-Clause 9 | 10 | License 11 | ======= 12 | 13 | Redistribution and use in source and binary forms, with or without 14 | modification, are permitted provided that the following conditions are met: 15 | * Redistributions of source code must retain the above copyright 16 | notice, this list of conditions and the following disclaimer. 17 | * Redistributions in binary form must reproduce the above copyright 18 | notice, this list of conditions and the following disclaimer in the 19 | documentation and/or other materials provided with the distribution. 20 | * Neither the name of Ask Solem, nor the 21 | names of its contributors may be used to endorse or promote products 22 | derived from this software without specific prior written permission. 23 | 24 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 25 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 26 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 27 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Ask Solem OR CONTRIBUTORS 28 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 29 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 30 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 31 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 32 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 34 | POSSIBILITY OF SUCH DAMAGE. 35 | 36 | Documentation License 37 | ===================== 38 | 39 | The documentation portion of Vine (the rendered contents of the 40 | "docs" directory of a software distribution or checkout) is supplied 41 | under the "Creative Commons Attribution-ShareAlike 4.0 42 | International" (CC BY-SA 4.0) License as described by 43 | http://creativecommons.org/licenses/by-sa/4.0/ 44 | 45 | Footnotes 46 | ========= 47 | (1) A GPL-compatible license makes it possible to 48 | combine Vine with other software that is released 49 | under the GPL, it does not mean that we're distributing 50 | Vine under the GPL license. The BSD license, unlike the GPL, 51 | let you distribute a modified version without making your 52 | changes open source. 53 | -------------------------------------------------------------------------------- /docs/_templates/sidebardonations.html: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /vine/funtools.py: -------------------------------------------------------------------------------- 1 | """Functional utilities.""" 2 | from .abstract import Thenable 3 | from .promises import promise 4 | 5 | __all__ = [ 6 | 'maybe_promise', 'ensure_promise', 7 | 'ppartial', 'preplace', 'ready_promise', 8 | 'starpromise', 'transform', 'wrap', 9 | ] 10 | 11 | 12 | def maybe_promise(p): 13 | """Return None if p is undefined, otherwise make sure it's a promise.""" 14 | if p: 15 | if not isinstance(p, Thenable): 16 | return promise(p) 17 | return p 18 | 19 | 20 | def ensure_promise(p): 21 | """Ensure p is a promise. 22 | 23 | If p is not a promise, a new promise is created with p' as callback. 24 | """ 25 | if p is None: 26 | return promise() 27 | return maybe_promise(p) 28 | 29 | 30 | def ppartial(p, *args, **kwargs): 31 | """Create/modify promise with partial arguments.""" 32 | p = ensure_promise(p) 33 | if args: 34 | p.args = args + p.args 35 | if kwargs: 36 | p.kwargs.update(kwargs) 37 | return p 38 | 39 | 40 | def preplace(p, *args, **kwargs): 41 | """Replace promise arguments. 42 | 43 | This will force the promise to disregard any arguments 44 | the promise is fulfilled with, and to be called with the 45 | provided arguments instead. 46 | """ 47 | def _replacer(*_, **__): 48 | return p(*args, **kwargs) 49 | return promise(_replacer) 50 | 51 | 52 | def ready_promise(callback=None, *args): 53 | """Create promise that is already fulfilled.""" 54 | p = ensure_promise(callback) 55 | p(*args) 56 | return p 57 | 58 | 59 | def starpromise(fun, *args, **kwargs): 60 | """Create promise, using star arguments.""" 61 | return promise(fun, args, kwargs) 62 | 63 | 64 | def transform(filter_, callback, *filter_args, **filter_kwargs): 65 | """Filter final argument to a promise. 66 | 67 | E.g. to coerce callback argument to :class:`int`:: 68 | 69 | transform(int, callback) 70 | 71 | or a more complex example extracting something from a dict 72 | and coercing the value to :class:`float`: 73 | 74 | .. code-block:: python 75 | 76 | def filter_key_value(key, filter_, mapping): 77 | return filter_(mapping[key]) 78 | 79 | def get_page_expires(self, url, callback=None): 80 | return self.request( 81 | 'GET', url, 82 | callback=transform(get_key, callback, 'PageExpireValue', int), 83 | ) 84 | 85 | """ 86 | callback = ensure_promise(callback) 87 | P = promise(_transback, (filter_, callback, filter_args, filter_kwargs)) 88 | P.then(promise(), callback.throw) 89 | return P 90 | 91 | 92 | def _transback(filter_, callback, args, kwargs, ret): 93 | try: 94 | ret = filter_(*args + (ret,), **kwargs) 95 | except Exception: 96 | callback.throw() 97 | else: 98 | return callback(ret) 99 | 100 | 101 | def wrap(p): 102 | """Wrap promise. 103 | 104 | This wraps the promise such that if the promise is called with a promise as 105 | argument, we attach ourselves to that promise instead. 106 | """ 107 | def on_call(*args, **kwargs): 108 | if len(args) == 1 and isinstance(args[0], promise): 109 | return args[0].then(p) 110 | else: 111 | return p(*args, **kwargs) 112 | 113 | return on_call 114 | -------------------------------------------------------------------------------- /vine/synchronization.py: -------------------------------------------------------------------------------- 1 | """Synchronization primitives.""" 2 | from .abstract import Thenable 3 | from .promises import promise 4 | 5 | __all__ = ['barrier'] 6 | 7 | 8 | class barrier: 9 | """Barrier. 10 | 11 | Synchronization primitive to call a callback after a list 12 | of promises have been fulfilled. 13 | 14 | Example: 15 | 16 | .. code-block:: python 17 | 18 | # Request supports the .then() method. 19 | p1 = http.Request('http://a') 20 | p2 = http.Request('http://b') 21 | p3 = http.Request('http://c') 22 | requests = [p1, p2, p3] 23 | 24 | def all_done(): 25 | pass # all requests complete 26 | 27 | b = barrier(requests).then(all_done) 28 | 29 | # oops, we forgot we want another request 30 | b.add(http.Request('http://d')) 31 | 32 | Note that you cannot add new promises to a barrier after 33 | the barrier is fulfilled. 34 | """ 35 | __slots__ = ( 36 | 'p', 'args', 'kwargs', '_value', 'size', 37 | 'ready', 'reason', 'cancelled', 'finalized', 38 | '__weakref__', 39 | # adding '__dict__' to get dynamic assignment 40 | "__dict__", 41 | ) 42 | 43 | def __init__(self, promises=None, args=None, kwargs=None, 44 | callback=None, size=None): 45 | self.p = promise() 46 | self.args = args or () 47 | self.kwargs = kwargs or {} 48 | self._value = 0 49 | self.size = size or 0 50 | if not self.size and promises: 51 | # iter(l) calls len(l) so generator wrappers 52 | # can only return NotImplemented in the case the 53 | # generator is not fully consumed yet. 54 | plen = promises.__len__() 55 | if plen is not NotImplemented: 56 | self.size = plen 57 | self.ready = self.failed = False 58 | self.reason = None 59 | self.cancelled = False 60 | self.finalized = False 61 | 62 | [self.add_noincr(p) for p in promises or []] 63 | self.finalized = bool(promises or self.size) 64 | if callback: 65 | self.then(callback) 66 | 67 | def __call__(self, *args, **kwargs): 68 | if not self.ready and not self.cancelled: 69 | self._value += 1 70 | if self.finalized and self._value >= self.size: 71 | self.ready = True 72 | self.p(*self.args, **self.kwargs) 73 | 74 | def finalize(self): 75 | if not self.finalized and self._value >= self.size: 76 | self.p(*self.args, **self.kwargs) 77 | self.finalized = True 78 | 79 | def cancel(self): 80 | self.cancelled = True 81 | self.p.cancel() 82 | 83 | def add_noincr(self, p): 84 | if not self.cancelled: 85 | if self.ready: 86 | raise ValueError('Cannot add promise to full barrier') 87 | p.then(self) 88 | 89 | def add(self, p): 90 | if not self.cancelled: 91 | self.add_noincr(p) 92 | self.size += 1 93 | 94 | def then(self, callback, errback=None): 95 | self.p.then(callback, errback) 96 | 97 | def throw(self, *args, **kwargs): 98 | if not self.cancelled: 99 | self.p.throw(*args, **kwargs) 100 | throw1 = throw 101 | 102 | 103 | Thenable.register(barrier) 104 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import codecs 3 | import os 4 | import re 5 | import sys 6 | 7 | import setuptools 8 | import setuptools.command.test 9 | 10 | NAME = 'vine' 11 | 12 | # -*- Classifiers -*- 13 | 14 | classes = """ 15 | Development Status :: 5 - Production/Stable 16 | Programming Language :: Python 17 | Programming Language :: Python :: 3 18 | Programming Language :: Python :: 3.7 19 | Programming Language :: Python :: 3.8 20 | Programming Language :: Python :: 3.9 21 | Programming Language :: Python :: 3.10 22 | Programming Language :: Python :: 3.11 23 | Programming Language :: Python :: 3.12 24 | Programming Language :: Python :: 3 :: Only 25 | Programming Language :: Python :: Implementation :: CPython 26 | Programming Language :: Python :: Implementation :: PyPy 27 | License :: OSI Approved :: BSD License 28 | Intended Audience :: Developers 29 | Operating System :: OS Independent 30 | """ 31 | classifiers = [s.strip() for s in classes.split('\n') if s] 32 | 33 | # -*- Distribution Meta -*- 34 | 35 | re_meta = re.compile(r'__(\w+?)__\s*=\s*(.*)') 36 | re_doc = re.compile(r'^"""(.+?)"""') 37 | 38 | 39 | def add_default(m): 40 | attr_name, attr_value = m.groups() 41 | return ((attr_name, attr_value.strip("\"'")),) 42 | 43 | 44 | def add_doc(m): 45 | return (('doc', m.groups()[0]),) 46 | 47 | 48 | pats = {re_meta: add_default, 49 | re_doc: add_doc} 50 | here = os.path.abspath(os.path.dirname(__file__)) 51 | with open(os.path.join(here, 'vine', '__init__.py')) as meta_fh: 52 | meta = {} 53 | for line in meta_fh: 54 | if line.strip() == '# -eof meta-': 55 | break 56 | for pattern, handler in pats.items(): 57 | m = pattern.match(line.strip()) 58 | if m: 59 | meta.update(handler(m)) 60 | 61 | 62 | # -*- Installation Requires -*- 63 | 64 | py_version = sys.version_info 65 | is_jython = sys.platform.startswith('java') 66 | is_pypy = hasattr(sys, 'pypy_version_info') 67 | 68 | 69 | def strip_comments(line): 70 | return line.split('#', 1)[0].strip() 71 | 72 | 73 | def reqs(f): 74 | with open(os.path.join(os.getcwd(), "requirements", f)) as fp: 75 | return [r for r in (strip_comments(line) for line in fp) if r] 76 | 77 | # -*- Long Description -*- 78 | 79 | 80 | if os.path.exists('README.rst'): 81 | long_description = codecs.open('README.rst', 'r', 'utf-8').read() 82 | else: 83 | long_description = 'See https://pypi.org/project/vine/' 84 | 85 | # -*- Entry Points -*- # 86 | 87 | # -*- %%% -*- 88 | 89 | 90 | class pytest(setuptools.command.test.test): 91 | user_options = [('pytest-args=', 'a', 'Arguments to pass to py.test')] 92 | 93 | def initialize_options(self): 94 | super().initialize_options() 95 | self.pytest_args = [] 96 | 97 | def run_tests(self): 98 | import pytest 99 | sys.exit(pytest.main(self.pytest_args)) 100 | 101 | 102 | setuptools.setup( 103 | name=NAME, 104 | packages=setuptools.find_packages(exclude=['t', 't.*']), 105 | version=meta['version'], 106 | description=meta['doc'], 107 | long_description=long_description, 108 | keywords='promise promises lazy future futures', 109 | author=meta['author'], 110 | author_email=meta['contact'], 111 | url=meta['homepage'], 112 | platforms=['any'], 113 | classifiers=classifiers, 114 | license='BSD', 115 | python_requires=">=3.6", 116 | install_requires=[], 117 | tests_require=reqs('test.txt'), 118 | cmdclass={'test': pytest}, 119 | zip_safe=False, 120 | include_package_data=False, 121 | ) 122 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJ=vine 2 | PGPIDENT="Celery Security Team" 3 | PYTHON=python 4 | PYTEST=py.test 5 | GIT=git 6 | TOX=tox 7 | ICONV=iconv 8 | FLAKE8=flake8 9 | FLAKEPLUS=flakeplus 10 | PYDOCSTYLE=pydocstyle 11 | SPHINX2RST=sphinx2rst 12 | 13 | TESTDIR=t 14 | SPHINX_DIR=docs/ 15 | SPHINX_BUILDDIR="${SPHINX_DIR}/_build" 16 | README=README.rst 17 | README_SRC="docs/templates/readme.txt" 18 | CONTRIBUTING=CONTRIBUTING.rst 19 | CONTRIBUTING_SRC="docs/contributing.rst" 20 | SPHINX_HTMLDIR="${SPHINX_BUILDDIR}/html" 21 | DOCUMENTATION=Documentation 22 | FLAKEPLUSTARGET=2.7 23 | 24 | all: help 25 | 26 | help: 27 | @echo "docs - Build documentation." 28 | @echo "test-all - Run tests for all supported python versions." 29 | @echo "distcheck ---------- - Check distribution for problems." 30 | @echo " test - Run unittests using current python." 31 | @echo " lint ------------ - Check codebase for problems." 32 | @echo " apicheck - Check API reference coverage." 33 | @echo " readmecheck - Check README.rst encoding." 34 | @echo " contribcheck - Check CONTRIBUTING.rst encoding" 35 | @echo " flakes -------- - Check code for syntax and style errors." 36 | @echo " flakecheck - Run flake8 on the source code." 37 | @echo " flakepluscheck - Run flakeplus on the source code." 38 | @echo " pep257check - Run pep257 on the source code." 39 | @echo "readme - Regenerate README.rst file." 40 | @echo "contrib - Regenerate CONTRIBUTING.rst file" 41 | @echo "clean-dist --------- - Clean all distribution build artifacts." 42 | @echo " clean-git-force - Remove all uncomitted files." 43 | @echo " clean ------------ - Non-destructive clean" 44 | @echo " clean-pyc - Remove .pyc/__pycache__ files" 45 | @echo " clean-docs - Remove documentation build artifacts." 46 | @echo " clean-build - Remove setup artifacts." 47 | @echo "bump - Bump patch version number." 48 | @echo "bump-minor - Bump minor version number." 49 | @echo "bump-major - Bump major version number." 50 | @echo "release - Make PyPI release." 51 | 52 | clean: clean-docs clean-pyc clean-build 53 | 54 | clean-dist: clean clean-git-force 55 | 56 | bump: 57 | bumpversion patch 58 | 59 | bump-minor: 60 | bumpversion minor 61 | 62 | bump-major: 63 | bumpversion major 64 | 65 | release: 66 | python setup.py register sdist bdist_wheel upload --sign --identity="$(PGPIDENT)" 67 | 68 | Documentation: 69 | (cd "$(SPHINX_DIR)"; $(MAKE) html) 70 | mv "$(SPHINX_HTMLDIR)" $(DOCUMENTATION) 71 | 72 | docs: Documentation 73 | 74 | clean-docs: 75 | -rm -rf "$(SPHINX_BUILDDIR)" 76 | 77 | lint: flakecheck apicheck readmecheck 78 | 79 | apicheck: 80 | (cd "$(SPHINX_DIR)"; $(MAKE) apicheck) 81 | 82 | flakecheck: 83 | $(FLAKE8) "$(PROJ)" "$(TESTDIR)" 84 | 85 | flakediag: 86 | -$(MAKE) flakecheck 87 | 88 | flakepluscheck: 89 | $(FLAKEPLUS) --$(FLAKEPLUSTARGET) "$(PROJ)" "$(TESTDIR)" 90 | 91 | flakeplusdiag: 92 | -$(MAKE) flakepluscheck 93 | 94 | pep257check: 95 | $(PYDOCSTYLE) "$(PROJ)" 96 | 97 | flakes: flakediag flakeplusdiag pep257check 98 | 99 | clean-readme: 100 | -rm -f $(README) 101 | 102 | readmecheck: 103 | $(ICONV) -f ascii -t ascii $(README) >/dev/null 104 | 105 | $(README): 106 | $(SPHINX2RST) "$(README_SRC)" --ascii > $@ 107 | 108 | readme: clean-readme $(README) readmecheck 109 | 110 | clean-contrib: 111 | -rm -f "$(CONTRIBUTING)" 112 | 113 | $(CONTRIBUTING): 114 | $(SPHINX2RST) "$(CONTRIBUTING_SRC)" > $@ 115 | 116 | contrib: clean-contrib $(CONTRIBUTING) 117 | 118 | clean-pyc: 119 | -find . -type f -a \( -name "*.pyc" -o -name "*$$py.class" \) | xargs rm 120 | -find . -type d -name "__pycache__" | xargs rm -r 121 | 122 | removepyc: clean-pyc 123 | 124 | clean-build: 125 | rm -rf build/ dist/ .eggs/ *.egg-info/ .tox/ .coverage cover/ 126 | 127 | clean-git: 128 | $(GIT) clean -xdn 129 | 130 | clean-git-force: 131 | $(GIT) clean -xdf 132 | 133 | test-all: clean-pyc 134 | $(TOX) 135 | 136 | test: 137 | $(PYTHON) setup.py test 138 | 139 | cov: 140 | $(PYTEST) -x --cov=vine --cov-report=html 141 | 142 | build: 143 | $(PYTHON) setup.py sdist bdist_wheel 144 | 145 | distcheck: lint test clean 146 | 147 | dist: readme contrib clean-dist build 148 | -------------------------------------------------------------------------------- /Changelog: -------------------------------------------------------------------------------- 1 | Changes 2 | ======= 3 | 4 | .. _version-5.1.0: 5 | 6 | 5.1.0 7 | ===== 8 | :release-date: 2023-11-05 2:45 P.M UTC+6:00 9 | :release-by: ASIF SAIF UDDIN 10 | 11 | - Dropped Python 3.6 support. 12 | - Added new Python versions support. 13 | - Dropped old dependencies. 14 | - Added new GHA based CI. 15 | - Added slots support and impproved dynamic assignment. 16 | 17 | 18 | Contributed by **Asif Saif Uddin** 19 | 20 | 21 | .. _version-5.0.0: 22 | 23 | 5.0.0 24 | ===== 25 | :release-date: 2020-09-06 6:10 P.M UTC+3:00 26 | :release-by: Omer Katz 27 | 28 | - Dropped Python 3.5 support. 29 | 30 | Contributed by **Omer Katz** 31 | 32 | .. _version-5.0.0a1: 33 | 34 | 5.0.0a1 35 | ======= 36 | :release-date: 2019-04-01 4:30 P.M UTC+3:00 37 | :release-by: Omer Katz 38 | 39 | - Dropped Python 2.x support. 40 | 41 | Contributed by **Omer Katz** 42 | 43 | - Dropped Python 3.4 support. 44 | 45 | Contributed by **Omer Katz** 46 | 47 | - Removed the :mod:`vine.five` module. 48 | 49 | Contributed by **Omer Katz** 50 | 51 | - Removed the :mod:`vine.backports.weakref_backports` module. 52 | 53 | Contributed by **Omer Katz** 54 | 55 | .. _version-1.3.0: 56 | 57 | 1.3.0 58 | ===== 59 | :release-date: 2019-03-19 11:00 A.M UTC+2 60 | :release-by: Omer Katz 61 | 62 | - Added the option to ignore the result of a function and simply 63 | call the callback without arguments. 64 | 65 | Contributed by **Omer Katz** 66 | 67 | .. _version-1.2.0: 68 | 69 | 1.2.0 70 | ===== 71 | :release-date: 2018-01-06 4:30 P.M UTC+2 72 | :release-by: Omer Katz 73 | 74 | - Added Python 3.7 support. 75 | 76 | Contributed by **Jon Dufresne** & **:github_user:`dequis`** 77 | 78 | - Handle bound methods in weak reference promise instances. 79 | 80 | Contributed by **George Psarakis** 81 | 82 | Documentation fixes, CI adjustments and cleanups by: 83 | 84 | - **Omer Katz** 85 | - **Jon Dufresne** 86 | - **Edward Betts** 87 | - **Jacopo Notarstefano** 88 | - **Christopher Hoskin** 89 | - **Fahad Siddiqui** 90 | 91 | .. _version-1.1.4: 92 | 93 | 1.1.4 94 | ===== 95 | :release-date: 2017-07-16 10:30 P.M UTC+2 96 | :release-by: Ask Solem 97 | 98 | - Added official support for Python 3.5 & 3.6. 99 | - Improve Python 2/3 compatibility. 100 | - Don't set mutable default values to keyword arguments. 101 | 102 | .. _version-1.1.3: 103 | 104 | 1.1.3 105 | ===== 106 | :release-date: 2016-10-13 06:02 P.M PDT 107 | :release-by: Ask Solem 108 | 109 | - New ``promise(fun, weak=True)`` argument, creates weakref to callback. 110 | 111 | .. _version-1.1.2: 112 | 113 | 1.1.2 114 | ===== 115 | :release-date: 2016-09-07 04:18 P.M PDT 116 | :release-by: Ask Solem 117 | 118 | - barrier: now handles the case where len(promises) returns NotImplemented. 119 | 120 | .. _version-1.1.1: 121 | 122 | 1.1.1 123 | ===== 124 | :release-date: 2016-06-30 12:05 P.M PDT 125 | :release-by: Ask Solem 126 | 127 | - Requirements: Tests now depends on :pypi:`case` 1.2.2 128 | 129 | - Five: python_2_unicode_compatible now ensures `__repr__` returns 130 | bytes on Python 2. 131 | 132 | .. _version-1.1.0: 133 | 134 | 1.1.0 135 | ===== 136 | :release-date: 2016-04-21 01:30 P.M PDT 137 | :release-by: Ask Solem 138 | 139 | - :meth:`promise.throw() ` now passes partial 140 | args/kwargs to the errback: 141 | 142 | .. code-block:: pycon 143 | 144 | >>> p = promise(args=(self,), on_error=handle_error) 145 | >>> p.throw(exc) # --> handle_error(self, exc) 146 | 147 | - New :class:`vine.abstract.ThenableProxy` can be used to add 148 | promise-capabilities to a class by forwarding to a different promise. 149 | 150 | .. code-block:: python 151 | 152 | from vine import promise 153 | from vine.abstract import ThenableProxy 154 | 155 | class P(ThenableProxy): 156 | 157 | def __init__(self, on_success=None, on_error=None): 158 | self._set_promise_target(promise( 159 | args=(self,), callback=on_success, on_error=on_error, 160 | )) 161 | 162 | p = P() 163 | p.then(download_file(url)).then(extract_file) 164 | 165 | - :meth:`promise.throw() ` now supports a propagate 166 | argument that can be set to False to never reraise the exception. 167 | 168 | - :meth:`promise.throw() ` now also reraises the 169 | current exception from the stack, if the exc argument is passed and that 170 | value is the same as the current exception. 171 | 172 | - :meth:`Thenable.register() ` can now be 173 | used as a decorator. 174 | 175 | - Argument to :meth:`promise.throw1(exc) ` can now be 176 | :const:`None` to use the current exception. 177 | 178 | - ``monotonic()`` now uses ``librt.so.0`` as an alternative if ``librt.so.1`` 179 | does not exist. 180 | 181 | Contributed by Fahad Siddiqui. 182 | 183 | .. _version-1.0.2: 184 | 185 | 1.0.2 186 | ===== 187 | :release-date: 2016-04-11 05:30 P.M PDT 188 | :release-by: Ask Solem 189 | 190 | - ``promise.throw()`` now supports second ``traceback`` argument to 191 | throw exception with specific traceback. 192 | 193 | Contributed by Ionel Cristian Mărieș. 194 | 195 | .. _version-1.0.1: 196 | 197 | 1.0.1 198 | ===== 199 | :release-date: 2016-04-11 03:00 P.M PDT 200 | :release-by: Ask Solem 201 | 202 | - Adds vine.five.python_2_unicode_compatible. 203 | 204 | .. _version-1.0.0: 205 | 206 | 1.0.0 207 | ===== 208 | :release-date: 2016-04-07 06:02 P.M PDT 209 | :release-by: Ask Solem 210 | 211 | - Initial release. 212 | -------------------------------------------------------------------------------- /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 | goto end 43 | ) 44 | 45 | if "%1" == "clean" ( 46 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 47 | del /q /s %BUILDDIR%\* 48 | goto end 49 | ) 50 | 51 | 52 | REM Check if sphinx-build is available and fallback to Python version if any 53 | %SPHINXBUILD% 1>NUL 2>NUL 54 | if errorlevel 9009 goto sphinx_python 55 | goto sphinx_ok 56 | 57 | :sphinx_python 58 | 59 | set SPHINXBUILD=python -m sphinx.__init__ 60 | %SPHINXBUILD% 2> nul 61 | if errorlevel 9009 ( 62 | echo. 63 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 64 | echo.installed, then set the SPHINXBUILD environment variable to point 65 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 66 | echo.may add the Sphinx directory to PATH. 67 | echo. 68 | echo.If you don't have Sphinx installed, grab it from 69 | echo.http://sphinx-doc.org/ 70 | exit /b 1 71 | ) 72 | 73 | :sphinx_ok 74 | 75 | 76 | if "%1" == "html" ( 77 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 78 | if errorlevel 1 exit /b 1 79 | echo. 80 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 81 | goto end 82 | ) 83 | 84 | if "%1" == "dirhtml" ( 85 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 86 | if errorlevel 1 exit /b 1 87 | echo. 88 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 89 | goto end 90 | ) 91 | 92 | if "%1" == "singlehtml" ( 93 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 94 | if errorlevel 1 exit /b 1 95 | echo. 96 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 97 | goto end 98 | ) 99 | 100 | if "%1" == "pickle" ( 101 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 102 | if errorlevel 1 exit /b 1 103 | echo. 104 | echo.Build finished; now you can process the pickle files. 105 | goto end 106 | ) 107 | 108 | if "%1" == "json" ( 109 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished; now you can process the JSON files. 113 | goto end 114 | ) 115 | 116 | if "%1" == "htmlhelp" ( 117 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished; now you can run HTML Help Workshop with the ^ 121 | .hhp project file in %BUILDDIR%/htmlhelp. 122 | goto end 123 | ) 124 | 125 | if "%1" == "qthelp" ( 126 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 127 | if errorlevel 1 exit /b 1 128 | echo. 129 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 130 | .qhcp project file in %BUILDDIR%/qthelp, like this: 131 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\PROJ.qhcp 132 | echo.To view the help file: 133 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\PROJ.ghc 134 | goto end 135 | ) 136 | 137 | if "%1" == "devhelp" ( 138 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 139 | if errorlevel 1 exit /b 1 140 | echo. 141 | echo.Build finished. 142 | goto end 143 | ) 144 | 145 | if "%1" == "epub" ( 146 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 147 | if errorlevel 1 exit /b 1 148 | echo. 149 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 150 | goto end 151 | ) 152 | 153 | if "%1" == "epub3" ( 154 | %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 155 | if errorlevel 1 exit /b 1 156 | echo. 157 | echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. 158 | goto end 159 | ) 160 | 161 | if "%1" == "latex" ( 162 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 163 | if errorlevel 1 exit /b 1 164 | echo. 165 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 166 | goto end 167 | ) 168 | 169 | if "%1" == "latexpdf" ( 170 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 171 | cd %BUILDDIR%/latex 172 | make all-pdf 173 | cd %~dp0 174 | echo. 175 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 176 | goto end 177 | ) 178 | 179 | if "%1" == "latexpdfja" ( 180 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 181 | cd %BUILDDIR%/latex 182 | make all-pdf-ja 183 | cd %~dp0 184 | echo. 185 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 186 | goto end 187 | ) 188 | 189 | if "%1" == "text" ( 190 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 191 | if errorlevel 1 exit /b 1 192 | echo. 193 | echo.Build finished. The text files are in %BUILDDIR%/text. 194 | goto end 195 | ) 196 | 197 | if "%1" == "man" ( 198 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 199 | if errorlevel 1 exit /b 1 200 | echo. 201 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 202 | goto end 203 | ) 204 | 205 | if "%1" == "texinfo" ( 206 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 207 | if errorlevel 1 exit /b 1 208 | echo. 209 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 210 | goto end 211 | ) 212 | 213 | if "%1" == "gettext" ( 214 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 215 | if errorlevel 1 exit /b 1 216 | echo. 217 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 218 | goto end 219 | ) 220 | 221 | if "%1" == "changes" ( 222 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 223 | if errorlevel 1 exit /b 1 224 | echo. 225 | echo.The overview file is in %BUILDDIR%/changes. 226 | goto end 227 | ) 228 | 229 | if "%1" == "linkcheck" ( 230 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Link check complete; look for any errors in the above output ^ 234 | or in %BUILDDIR%/linkcheck/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "doctest" ( 239 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of doctests in the sources finished, look at the ^ 243 | results in %BUILDDIR%/doctest/output.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "coverage" ( 248 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Testing of coverage in the sources finished, look at the ^ 252 | results in %BUILDDIR%/coverage/python.txt. 253 | goto end 254 | ) 255 | 256 | if "%1" == "xml" ( 257 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 258 | if errorlevel 1 exit /b 1 259 | echo. 260 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 261 | goto end 262 | ) 263 | 264 | if "%1" == "pseudoxml" ( 265 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 266 | if errorlevel 1 exit /b 1 267 | echo. 268 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 269 | goto end 270 | ) 271 | 272 | :end 273 | -------------------------------------------------------------------------------- /vine/promises.py: -------------------------------------------------------------------------------- 1 | """Promise implementation.""" 2 | import inspect 3 | import sys 4 | from collections import deque 5 | from weakref import WeakMethod, ref 6 | 7 | from .abstract import Thenable 8 | from .utils import reraise 9 | 10 | __all__ = ['promise'] 11 | 12 | 13 | @Thenable.register 14 | class promise: 15 | """Promise of future evaluation. 16 | 17 | This is a special implementation of promises in that it can 18 | be used both for "promise of a value" and lazy evaluation. 19 | The biggest upside for this is that everything in a promise can also be 20 | a promise, e.g. filters, callbacks and errbacks can all be promises. 21 | 22 | Usage examples: 23 | 24 | .. code-block:: python 25 | 26 | >>> p = promise() 27 | >>> p.then(promise(print, ('OK',))) # noqa 28 | >>> p.on_error = promise(print, ('ERROR',)) # noqa 29 | >>> p(20) 30 | OK, 20 31 | >>> p.then(promise(print, ('hello',))) # noqa 32 | hello, 20 33 | 34 | 35 | >>> p.throw(KeyError('foo')) 36 | ERROR, KeyError('foo') 37 | 38 | 39 | >>> p2 = promise() 40 | >>> p2.then(print) # noqa 41 | >>> p2.cancel() 42 | >>> p(30) 43 | 44 | Example: 45 | .. code-block:: python 46 | 47 | from vine import promise, wrap 48 | 49 | class Protocol: 50 | 51 | def __init__(self): 52 | self.buffer = [] 53 | 54 | def receive_message(self): 55 | return self.read_header().then( 56 | self.read_body).then( 57 | wrap(self.prepare_body)) 58 | 59 | def read(self, size, callback=None): 60 | callback = callback or promise() 61 | tell_eventloop_to_read(size, callback) 62 | return callback 63 | 64 | def read_header(self, callback=None): 65 | return self.read(4, callback) 66 | 67 | def read_body(self, header, callback=None): 68 | body_size, = unpack('>L', header) 69 | return self.read(body_size, callback) 70 | 71 | def prepare_body(self, value): 72 | self.buffer.append(value) 73 | """ 74 | 75 | if not hasattr(sys, 'pypy_version_info'): # pragma: no cover 76 | __slots__ = ( 77 | 'fun', 'args', 'kwargs', 'ready', 'failed', 78 | 'value', 'ignore_result', 'reason', '_svpending', '_lvpending', 79 | 'on_error', 'cancelled', 'weak', '__weakref__', 80 | # adding '__dict__' to get dynamic assignment if needed 81 | "__dict__", 82 | ) 83 | 84 | def __init__(self, fun=None, args=None, kwargs=None, 85 | callback=None, on_error=None, weak=False, 86 | ignore_result=False): 87 | self.weak = weak 88 | self.ignore_result = ignore_result 89 | self.fun = self._get_fun_or_weakref(fun=fun, weak=weak) 90 | self.args = args or () 91 | self.kwargs = kwargs or {} 92 | self.ready = False 93 | self.failed = False 94 | self.value = None 95 | self.reason = None 96 | # Optimization 97 | # Most promises will only have one callback, so we optimize for this 98 | # case by using a list only when there are multiple callbacks. 99 | # s(calar) pending / l(ist) pending 100 | self._svpending = None 101 | self._lvpending = None 102 | self.on_error = on_error 103 | self.cancelled = False 104 | 105 | if callback is not None: 106 | self.then(callback) 107 | 108 | if self.fun: 109 | assert self.fun and callable(fun) 110 | 111 | @staticmethod 112 | def _get_fun_or_weakref(fun, weak): 113 | """Return the callable or a weak reference. 114 | 115 | Handles both bound and unbound methods. 116 | """ 117 | if not weak: 118 | return fun 119 | 120 | if inspect.ismethod(fun): 121 | return WeakMethod(fun) 122 | else: 123 | return ref(fun) 124 | 125 | def __repr__(self): 126 | return ('<{0} --> {1!r}>' if self.fun else '<{0}>').format( 127 | f'{type(self).__name__}@0x{id(self):x}', self.fun, 128 | ) 129 | 130 | def cancel(self): 131 | self.cancelled = True 132 | try: 133 | if self._svpending is not None: 134 | self._svpending.cancel() 135 | if self._lvpending is not None: 136 | for pending in self._lvpending: 137 | pending.cancel() 138 | if isinstance(self.on_error, Thenable): 139 | self.on_error.cancel() 140 | finally: 141 | self._svpending = self._lvpending = self.on_error = None 142 | 143 | def __call__(self, *args, **kwargs): 144 | retval = None 145 | if self.cancelled: 146 | return 147 | final_args = self.args + args if args else self.args 148 | final_kwargs = dict(self.kwargs, **kwargs) if kwargs else self.kwargs 149 | # self.fun may be a weakref 150 | fun = self._fun_is_alive(self.fun) 151 | if fun is not None: 152 | try: 153 | if self.ignore_result: 154 | fun(*final_args, **final_kwargs) 155 | ca = () 156 | ck = {} 157 | else: 158 | retval = fun(*final_args, **final_kwargs) 159 | self.value = (ca, ck) = (retval,), {} 160 | except Exception: 161 | return self.throw() 162 | else: 163 | self.value = (ca, ck) = final_args, final_kwargs 164 | self.ready = True 165 | svpending = self._svpending 166 | if svpending is not None: 167 | try: 168 | svpending(*ca, **ck) 169 | finally: 170 | self._svpending = None 171 | else: 172 | lvpending = self._lvpending 173 | try: 174 | while lvpending: 175 | p = lvpending.popleft() 176 | p(*ca, **ck) 177 | finally: 178 | self._lvpending = None 179 | return retval 180 | 181 | def _fun_is_alive(self, fun): 182 | return fun() if self.weak else self.fun 183 | 184 | def then(self, callback, on_error=None): 185 | if not isinstance(callback, Thenable): 186 | callback = promise(callback, on_error=on_error) 187 | if self.cancelled: 188 | callback.cancel() 189 | return callback 190 | if self.failed: 191 | callback.throw(self.reason) 192 | elif self.ready: 193 | args, kwargs = self.value 194 | callback(*args, **kwargs) 195 | if self._lvpending is None: 196 | svpending = self._svpending 197 | if svpending is not None: 198 | self._svpending, self._lvpending = None, deque([svpending]) 199 | else: 200 | self._svpending = callback 201 | return callback 202 | self._lvpending.append(callback) 203 | return callback 204 | 205 | def throw1(self, exc=None): 206 | if not self.cancelled: 207 | exc = exc if exc is not None else sys.exc_info()[1] 208 | self.failed, self.reason = True, exc 209 | if self.on_error: 210 | self.on_error(*self.args + (exc,), **self.kwargs) 211 | 212 | def throw(self, exc=None, tb=None, propagate=True): 213 | if not self.cancelled: 214 | current_exc = sys.exc_info()[1] 215 | exc = exc if exc is not None else current_exc 216 | try: 217 | self.throw1(exc) 218 | svpending = self._svpending 219 | if svpending is not None: 220 | try: 221 | svpending.throw1(exc) 222 | finally: 223 | self._svpending = None 224 | else: 225 | lvpending = self._lvpending 226 | try: 227 | while lvpending: 228 | lvpending.popleft().throw1(exc) 229 | finally: 230 | self._lvpending = None 231 | finally: 232 | if self.on_error is None and propagate: 233 | if tb is None and (exc is None or exc is current_exc): 234 | raise 235 | reraise(type(exc), exc, tb) 236 | 237 | @property 238 | def listeners(self): 239 | if self._lvpending: 240 | return self._lvpending 241 | return [self._svpending] 242 | -------------------------------------------------------------------------------- /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 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don\'t have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " epub3 to make an epub3" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | @echo " apicheck to make sure all modules are documented by autodoc" 51 | 52 | .PHONY: clean 53 | clean: 54 | rm -rf $(BUILDDIR)/* 55 | 56 | .PHONY: html 57 | html: 58 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 61 | 62 | .PHONY: dirhtml 63 | dirhtml: 64 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 65 | @echo 66 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 67 | 68 | .PHONY: singlehtml 69 | singlehtml: 70 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 71 | @echo 72 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 73 | 74 | .PHONY: pickle 75 | pickle: 76 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 77 | @echo 78 | @echo "Build finished; now you can process the pickle files." 79 | 80 | .PHONY: json 81 | json: 82 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 83 | @echo 84 | @echo "Build finished; now you can process the JSON files." 85 | 86 | .PHONY: htmlhelp 87 | htmlhelp: 88 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 89 | @echo 90 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 91 | ".hhp project file in $(BUILDDIR)/htmlhelp." 92 | 93 | .PHONY: qthelp 94 | qthelp: 95 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 96 | @echo 97 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 98 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 99 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PROJ.qhcp" 100 | @echo "To view the help file:" 101 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PROJ.qhc" 102 | 103 | .PHONY: applehelp 104 | applehelp: 105 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 106 | @echo 107 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 108 | @echo "N.B. You won't be able to view it unless you put it in" \ 109 | "~/Library/Documentation/Help or install it in your application" \ 110 | "bundle." 111 | 112 | .PHONY: devhelp 113 | devhelp: 114 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 115 | @echo 116 | @echo "Build finished." 117 | @echo "To view the help file:" 118 | @echo "# mkdir -p $$HOME/.local/share/devhelp/PROJ" 119 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PROJ" 120 | @echo "# devhelp" 121 | 122 | .PHONY: epub 123 | epub: 124 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 125 | @echo 126 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 127 | 128 | .PHONY: epub3 129 | epub3: 130 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 131 | @echo 132 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 133 | 134 | .PHONY: latex 135 | latex: 136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 137 | @echo 138 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 139 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 140 | "(use \`make latexpdf' here to do that automatically)." 141 | 142 | .PHONY: latexpdf 143 | latexpdf: 144 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 145 | @echo "Running LaTeX files through pdflatex..." 146 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 147 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 148 | 149 | .PHONY: latexpdfja 150 | latexpdfja: 151 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 152 | @echo "Running LaTeX files through platex and dvipdfmx..." 153 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 154 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 155 | 156 | .PHONY: text 157 | text: 158 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 159 | @echo 160 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 161 | 162 | .PHONY: man 163 | man: 164 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 165 | @echo 166 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 167 | 168 | .PHONY: texinfo 169 | texinfo: 170 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 171 | @echo 172 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 173 | @echo "Run \`make' in that directory to run these through makeinfo" \ 174 | "(use \`make info' here to do that automatically)." 175 | 176 | .PHONY: info 177 | info: 178 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 179 | @echo "Running Texinfo files through makeinfo..." 180 | make -C $(BUILDDIR)/texinfo info 181 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 182 | 183 | .PHONY: gettext 184 | gettext: 185 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 186 | @echo 187 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 188 | 189 | .PHONY: changes 190 | changes: 191 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 192 | @echo 193 | @echo "The overview file is in $(BUILDDIR)/changes." 194 | 195 | .PHONY: linkcheck 196 | linkcheck: 197 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 198 | @echo 199 | @echo "Link check complete; look for any errors in the above output " \ 200 | "or in $(BUILDDIR)/linkcheck/output.txt." 201 | 202 | .PHONY: doctest 203 | doctest: 204 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 205 | @echo "Testing of doctests in the sources finished, look at the " \ 206 | "results in $(BUILDDIR)/doctest/output.txt." 207 | 208 | .PHONY: coverage 209 | coverage: 210 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 211 | @echo "Testing of coverage in the sources finished, look at the " \ 212 | "results in $(BUILDDIR)/coverage/python.txt." 213 | 214 | .PHONY: apicheck 215 | apicheck: 216 | $(SPHINXBUILD) -b apicheck $(ALLSPHINXOPTS) $(BUILDDIR)/apicheck 217 | 218 | .PHONY: xml 219 | xml: 220 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 221 | @echo 222 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 223 | 224 | .PHONY: pseudoxml 225 | pseudoxml: 226 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 227 | @echo 228 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 229 | -------------------------------------------------------------------------------- /t/unit/test_promises.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import traceback 3 | import weakref 4 | from collections import deque 5 | from struct import pack, unpack 6 | from unittest.mock import Mock 7 | 8 | import pytest 9 | 10 | from vine.funtools import wrap 11 | from vine.promises import promise 12 | 13 | 14 | class test_promise: 15 | 16 | def test_example(self): 17 | 18 | _pending = deque() 19 | 20 | class Protocol: 21 | 22 | def __init__(self): 23 | self.buffer = [] 24 | 25 | def read(self, size, callback=None): 26 | callback = callback or promise() 27 | _pending.append((size, callback)) 28 | return callback 29 | 30 | def read_header(self, callback=None): 31 | return self.read(4, callback) 32 | 33 | def read_body(self, header, callback=None): 34 | body_size, = unpack('>L', header) 35 | return self.read(body_size, callback) 36 | 37 | def prepare_body(self, value): 38 | self.buffer.append(value) 39 | 40 | proto = Protocol() 41 | proto.read_header().then( 42 | proto.read_body).then(wrap(proto.prepare_body)) 43 | 44 | while _pending: 45 | size, callback = _pending.popleft() 46 | if size == 4: 47 | callback(pack('>L', 1231012302)) 48 | else: 49 | callback('Hello world') 50 | 51 | assert proto.buffer 52 | assert proto.buffer[0] == 'Hello world' 53 | 54 | def test_signal(self): 55 | callback = Mock(name='callback') 56 | a = promise() 57 | a.then(callback) 58 | a(42) 59 | callback.assert_called_once_with(42) 60 | 61 | def test_signal_callback_kwargs(self): 62 | callback = Mock(name='callback') 63 | a = promise(callback=callback) 64 | a(42) 65 | callback.assert_called_once_with(42) 66 | 67 | def test_call_ignore_result(self): 68 | fun = Mock(name='fun') 69 | callback = Mock(name='callback') 70 | a = promise(fun=fun, ignore_result=True) 71 | a.then(callback) 72 | a() 73 | fun.assert_called_once_with() 74 | callback.assert_called_once_with() 75 | 76 | def test_call_ignore_result_callback_kwarg(self): 77 | fun = Mock(name='fun') 78 | callback = Mock(name='callback') 79 | a = promise(fun=fun, ignore_result=True, callback=callback) 80 | a() 81 | callback.assert_called_once_with() 82 | 83 | def test_chained(self): 84 | 85 | def add(x, y): 86 | return x + y 87 | 88 | def pow2(x): 89 | return x ** 2 90 | 91 | adder = Mock(name='adder') 92 | adder.side_effect = add 93 | 94 | power = Mock(name='multiplier') 95 | power.side_effect = pow2 96 | 97 | final = Mock(name='final') 98 | 99 | p = promise() 100 | p.then(adder).then(power).then(final) 101 | 102 | p(42, 42) 103 | assert p.value == ((42, 42), {}) 104 | adder.assert_called_with(42, 42) 105 | power.assert_called_with(84) 106 | final.assert_called_with(7056) 107 | 108 | def test_shallow_filter(self): 109 | a, b = promise(Mock(name='a')), promise(Mock(name='b')) 110 | p = promise(a, callback=b) 111 | assert p._svpending is not None 112 | assert p._lvpending is None 113 | p(30) 114 | assert p._svpending is None 115 | a.fun.assert_called_with(30) 116 | b.fun.assert_called_with(a.fun.return_value) 117 | 118 | c, d = Mock(name='c'), Mock(name='d') 119 | promise(c, callback=d)(1) 120 | c.assert_called_with(1) 121 | d.assert_called_with(c.return_value) 122 | 123 | def test_deep_filter(self): 124 | a = promise(Mock(name='a')) 125 | b1, b2, b3 = ( 126 | promise(Mock(name='a1')), 127 | promise(Mock(name='a2')), 128 | promise(Mock(name='a3')), 129 | ) 130 | p = promise(a) 131 | p.then(b1) 132 | assert p._lvpending is None 133 | assert p._svpending is not None 134 | p.then(b2) 135 | assert p._lvpending is not None 136 | assert p._svpending is None 137 | p.then(b3) 138 | 139 | p(42) 140 | a.fun.assert_called_with(42) 141 | b1.fun.assert_called_with(a.fun.return_value) 142 | b2.fun.assert_called_with(a.fun.return_value) 143 | b3.fun.assert_called_with(a.fun.return_value) 144 | 145 | def test_chained_filter(self): 146 | a = promise(Mock(name='a')) 147 | b = promise(Mock(name='b')) 148 | c = promise(Mock(name='c')) 149 | d = promise(Mock(name='d')) 150 | 151 | p = promise(a) 152 | p.then(b).then(c).then(d) 153 | 154 | p(42, kw=300) 155 | 156 | a.fun.assert_called_with(42, kw=300) 157 | b.fun.assert_called_with(a.fun.return_value) 158 | c.fun.assert_called_with(b.fun.return_value) 159 | d.fun.assert_called_with(c.fun.return_value) 160 | 161 | def test_repr(self): 162 | assert repr(promise()) 163 | assert repr(promise(Mock())) 164 | 165 | def test_cancel(self): 166 | on_error = promise(Mock(name='on_error')) 167 | p = promise(on_error=on_error) 168 | a, b, c = ( 169 | promise(Mock(name='a')), 170 | promise(Mock(name='b')), 171 | promise(Mock(name='c')), 172 | ) 173 | a2 = promise(Mock(name='a1')) 174 | p.then(a).then(b).then(c) 175 | p.then(a2) 176 | 177 | p.cancel() 178 | p(42) 179 | assert p.cancelled 180 | assert a.cancelled 181 | assert a2.cancelled 182 | assert b.cancelled 183 | assert c.cancelled 184 | assert on_error.cancelled 185 | d = promise(Mock(name='d')) 186 | p.then(d) 187 | assert d.cancelled 188 | 189 | def test_svpending_raises(self): 190 | p = promise() 191 | a_on_error = promise(Mock(name='a_on_error')) 192 | a = promise(Mock(name='a'), on_error=a_on_error) 193 | p.then(a) 194 | exc = KeyError() 195 | a.fun.side_effect = exc 196 | 197 | p(42) 198 | a_on_error.fun.assert_called_with(exc) 199 | 200 | def test_empty_promise(self): 201 | p = promise() 202 | p(42) 203 | x = Mock(name='x') 204 | p.then(x) 205 | x.assert_called_with(42) 206 | 207 | def test_with_partial_args(self): 208 | m = Mock(name='m') 209 | p = promise(m, (1, 2, 3), {'foobar': 2}) 210 | p() 211 | m.assert_called_with(1, 2, 3, foobar=2) 212 | 213 | def test_with_partial_args_and_args(self): 214 | m = Mock(name='m') 215 | p = promise(m, (1, 2, 3), {'foobar': 2}) 216 | p(4, 5, bazbar=3) 217 | m.assert_called_with(1, 2, 3, 4, 5, foobar=2, bazbar=3) 218 | 219 | def test_lvpending_raises(self): 220 | p = promise() 221 | a_on_error = promise(Mock(name='a_on_error')) 222 | a = promise(Mock(name='a'), on_error=a_on_error) 223 | b_on_error = promise(Mock(name='b_on_error')) 224 | b = promise(Mock(name='a'), on_error=b_on_error) 225 | p.then(a) 226 | p.then(b) 227 | exc = KeyError() 228 | a.fun.side_effect = exc 229 | 230 | a.then(Mock(name='foobar')) 231 | a.then(Mock(name='foozi')) 232 | 233 | p.on_error = a_on_error 234 | p(42) 235 | a_on_error.fun.assert_called_with(exc) 236 | b.fun.assert_called_with(42) 237 | 238 | def test_cancel_sv(self): 239 | p = promise() 240 | a = promise(Mock(name='a')) 241 | p.then(a) 242 | p.cancel() 243 | assert p.cancelled 244 | assert a.cancelled 245 | 246 | p.throw(KeyError()) 247 | p.throw1(KeyError()) 248 | 249 | def test_cancel_no_cb(self): 250 | p = promise() 251 | p.cancel() 252 | assert p.cancelled 253 | assert p.on_error is None 254 | p.throw(KeyError()) 255 | 256 | def test_throw_no_exc(self): 257 | p = promise() 258 | with pytest.raises((TypeError, RuntimeError)): 259 | p.throw() 260 | 261 | def test_throw_no_excinfo(self): 262 | p = promise() 263 | with pytest.raises(KeyError): 264 | p.throw(KeyError()) 265 | 266 | def test_throw_with_tb(self): 267 | p = promise() 268 | 269 | def foo(): 270 | raise KeyError() 271 | 272 | try: 273 | foo() 274 | except KeyError: 275 | try: 276 | p.throw() 277 | except KeyError: 278 | err = traceback.format_exc() 279 | assert 'in foo\n raise KeyError()' in err 280 | else: 281 | raise AssertionError('Did not throw.') 282 | 283 | def test_throw_with_other_tb(self): 284 | p = promise() 285 | 286 | def foo(): 287 | raise KeyError() 288 | 289 | def bar(): 290 | raise ValueError() 291 | 292 | try: 293 | bar() 294 | except ValueError: 295 | tb = sys.exc_info()[2] 296 | 297 | try: 298 | foo() 299 | except KeyError as exc: 300 | try: 301 | p.throw(exc, tb) 302 | except KeyError: 303 | err = traceback.format_exc() 304 | assert 'in bar\n raise ValueError()' in err 305 | else: 306 | raise AssertionError('Did not throw.') 307 | 308 | def test_throw_None(self): 309 | try: 310 | raise KeyError() 311 | except Exception: 312 | with pytest.raises(KeyError): 313 | promise().throw() 314 | 315 | def test_listeners(self): 316 | p = promise() 317 | p.then(Mock()) 318 | assert len(p.listeners) == 1 319 | p.then(Mock()) 320 | assert len(p.listeners) == 2 321 | 322 | def test_throw_from_cb(self): 323 | ae = promise(Mock(name='ae')) 324 | a = Mock(name='a') 325 | be = promise(Mock(name='be')) 326 | b = promise(Mock(name='b'), on_error=be) 327 | ce = promise(Mock(name='ce')) 328 | c = promise(Mock(name='c'), on_error=ce) 329 | 330 | exc = a.side_effect = KeyError() 331 | p1 = promise(a, on_error=ae) 332 | p1.then(b) 333 | assert p1._svpending 334 | p1(42) 335 | p1.on_error.fun.assert_called_with(exc) 336 | 337 | p2 = promise(a) 338 | p2.then(b).then(c) 339 | with pytest.raises(KeyError): 340 | p2(42) 341 | 342 | de = promise(Mock(name='de')) 343 | d = promise(Mock(name='d'), on_error=de) 344 | p2.then(d) 345 | de.fun.assert_called_with(exc) 346 | 347 | def test_weak_reference_unbound(self): 348 | def f(x): 349 | return x ** 2 350 | 351 | promise_f = promise(f, weak=True) 352 | 353 | assert isinstance(promise_f.fun, weakref.ref) 354 | assert promise_f(2) == 4 355 | 356 | def test_weak_reference_bound(self): 357 | class Example: 358 | def __init__(self, y): 359 | self.y = y 360 | 361 | def f(self, x): 362 | return self.y + x ** 2 363 | 364 | example = Example(5) 365 | promise_f = promise(example.f, weak=True) 366 | 367 | assert isinstance(promise_f.fun, weakref.ref) 368 | assert promise_f(2) == 9 369 | --------------------------------------------------------------------------------