This commit is contained in:
Iliyan Angelov
2025-09-14 23:24:25 +03:00
commit c67067a2a4
71311 changed files with 6800714 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
"""Python promises."""
import re
from collections import namedtuple
from .abstract import Thenable
from .funtools import (
ensure_promise,
maybe_promise,
ppartial,
preplace,
starpromise,
transform,
wrap,
)
from .promises import promise
from .synchronization import barrier
__version__ = '5.1.0'
__author__ = 'Ask Solem'
__contact__ = 'auvipy@gmail.com'
__homepage__ = 'https://github.com/celery/vine'
__docformat__ = 'restructuredtext'
# -eof meta-
version_info_t = namedtuple('version_info_t', (
'major', 'minor', 'micro', 'releaselevel', 'serial',
))
# bump version can only search for {current_version}
# so we have to parse the version here.
_temp = re.match(
r'(\d+)\.(\d+).(\d+)(.+)?', __version__).groups()
VERSION = version_info = version_info_t(
int(_temp[0]), int(_temp[1]), int(_temp[2]), _temp[3] or '', '')
del (_temp)
del (re)
__all__ = [
'Thenable', 'promise', 'barrier',
'maybe_promise', 'ensure_promise',
'ppartial', 'preplace', 'starpromise', 'transform', 'wrap',
]

View File

@@ -0,0 +1,68 @@
"""Abstract classes."""
import abc
from collections.abc import Callable
__all__ = ['Thenable']
class Thenable(Callable, metaclass=abc.ABCMeta): # pragma: no cover
"""Object that supports ``.then()``."""
__slots__ = ()
@abc.abstractmethod
def then(self, on_success, on_error=None):
raise NotImplementedError()
@abc.abstractmethod
def throw(self, exc=None, tb=None, propagate=True):
raise NotImplementedError()
@abc.abstractmethod
def cancel(self):
raise NotImplementedError()
@classmethod
def __subclasshook__(cls, C):
if cls is Thenable:
if any('then' in B.__dict__ for B in C.__mro__):
return True
return NotImplemented
@classmethod
def register(cls, other):
# overide to return other so `register` can be used as a decorator
type(cls).register(cls, other)
return other
@Thenable.register
class ThenableProxy:
"""Proxy to object that supports ``.then()``."""
def _set_promise_target(self, p):
self._p = p
def then(self, on_success, on_error=None):
return self._p.then(on_success, on_error)
def cancel(self):
return self._p.cancel()
def throw1(self, exc=None):
return self._p.throw1(exc)
def throw(self, exc=None, tb=None, propagate=True):
return self._p.throw(exc, tb=tb, propagate=propagate)
@property
def cancelled(self):
return self._p.cancelled
@property
def ready(self):
return self._p.ready
@property
def failed(self):
return self._p.failed

View File

@@ -0,0 +1,113 @@
"""Functional utilities."""
from .abstract import Thenable
from .promises import promise
__all__ = [
'maybe_promise', 'ensure_promise',
'ppartial', 'preplace', 'ready_promise',
'starpromise', 'transform', 'wrap',
]
def maybe_promise(p):
"""Return None if p is undefined, otherwise make sure it's a promise."""
if p:
if not isinstance(p, Thenable):
return promise(p)
return p
def ensure_promise(p):
"""Ensure p is a promise.
If p is not a promise, a new promise is created with p' as callback.
"""
if p is None:
return promise()
return maybe_promise(p)
def ppartial(p, *args, **kwargs):
"""Create/modify promise with partial arguments."""
p = ensure_promise(p)
if args:
p.args = args + p.args
if kwargs:
p.kwargs.update(kwargs)
return p
def preplace(p, *args, **kwargs):
"""Replace promise arguments.
This will force the promise to disregard any arguments
the promise is fulfilled with, and to be called with the
provided arguments instead.
"""
def _replacer(*_, **__):
return p(*args, **kwargs)
return promise(_replacer)
def ready_promise(callback=None, *args):
"""Create promise that is already fulfilled."""
p = ensure_promise(callback)
p(*args)
return p
def starpromise(fun, *args, **kwargs):
"""Create promise, using star arguments."""
return promise(fun, args, kwargs)
def transform(filter_, callback, *filter_args, **filter_kwargs):
"""Filter final argument to a promise.
E.g. to coerce callback argument to :class:`int`::
transform(int, callback)
or a more complex example extracting something from a dict
and coercing the value to :class:`float`:
.. code-block:: python
def filter_key_value(key, filter_, mapping):
return filter_(mapping[key])
def get_page_expires(self, url, callback=None):
return self.request(
'GET', url,
callback=transform(get_key, callback, 'PageExpireValue', int),
)
"""
callback = ensure_promise(callback)
P = promise(_transback, (filter_, callback, filter_args, filter_kwargs))
P.then(promise(), callback.throw)
return P
def _transback(filter_, callback, args, kwargs, ret):
try:
ret = filter_(*args + (ret,), **kwargs)
except Exception:
callback.throw()
else:
return callback(ret)
def wrap(p):
"""Wrap promise.
This wraps the promise such that if the promise is called with a promise as
argument, we attach ourselves to that promise instead.
"""
def on_call(*args, **kwargs):
if len(args) == 1 and isinstance(args[0], promise):
return args[0].then(p)
else:
return p(*args, **kwargs)
return on_call

View File

@@ -0,0 +1,241 @@
"""Promise implementation."""
import inspect
import sys
from collections import deque
from weakref import WeakMethod, ref
from .abstract import Thenable
from .utils import reraise
__all__ = ['promise']
@Thenable.register
class promise:
"""Promise of future evaluation.
This is a special implementation of promises in that it can
be used both for "promise of a value" and lazy evaluation.
The biggest upside for this is that everything in a promise can also be
a promise, e.g. filters, callbacks and errbacks can all be promises.
Usage examples:
.. code-block:: python
>>> p = promise()
>>> p.then(promise(print, ('OK',))) # noqa
>>> p.on_error = promise(print, ('ERROR',)) # noqa
>>> p(20)
OK, 20
>>> p.then(promise(print, ('hello',))) # noqa
hello, 20
>>> p.throw(KeyError('foo'))
ERROR, KeyError('foo')
>>> p2 = promise()
>>> p2.then(print) # noqa
>>> p2.cancel()
>>> p(30)
Example:
.. code-block:: python
from vine import promise, wrap
class Protocol:
def __init__(self):
self.buffer = []
def receive_message(self):
return self.read_header().then(
self.read_body).then(
wrap(self.prepare_body))
def read(self, size, callback=None):
callback = callback or promise()
tell_eventloop_to_read(size, callback)
return callback
def read_header(self, callback=None):
return self.read(4, callback)
def read_body(self, header, callback=None):
body_size, = unpack('>L', header)
return self.read(body_size, callback)
def prepare_body(self, value):
self.buffer.append(value)
"""
if not hasattr(sys, 'pypy_version_info'): # pragma: no cover
__slots__ = (
'fun', 'args', 'kwargs', 'ready', 'failed',
'value', 'ignore_result', 'reason', '_svpending', '_lvpending',
'on_error', 'cancelled', 'weak', '__weakref__',
# adding '__dict__' to get dynamic assignment if needed
"__dict__",
)
def __init__(self, fun=None, args=None, kwargs=None,
callback=None, on_error=None, weak=False,
ignore_result=False):
self.weak = weak
self.ignore_result = ignore_result
self.fun = self._get_fun_or_weakref(fun=fun, weak=weak)
self.args = args or ()
self.kwargs = kwargs or {}
self.ready = False
self.failed = False
self.value = None
self.reason = None
# Optimization
# Most promises will only have one callback, so we optimize for this
# case by using a list only when there are multiple callbacks.
# s(calar) pending / l(ist) pending
self._svpending = None
self._lvpending = None
self.on_error = on_error
self.cancelled = False
if callback is not None:
self.then(callback)
if self.fun:
assert self.fun and callable(fun)
@staticmethod
def _get_fun_or_weakref(fun, weak):
"""Return the callable or a weak reference.
Handles both bound and unbound methods.
"""
if not weak:
return fun
if inspect.ismethod(fun):
return WeakMethod(fun)
else:
return ref(fun)
def __repr__(self):
return ('<{0} --> {1!r}>' if self.fun else '<{0}>').format(
f'{type(self).__name__}@0x{id(self):x}', self.fun,
)
def cancel(self):
self.cancelled = True
try:
if self._svpending is not None:
self._svpending.cancel()
if self._lvpending is not None:
for pending in self._lvpending:
pending.cancel()
if isinstance(self.on_error, Thenable):
self.on_error.cancel()
finally:
self._svpending = self._lvpending = self.on_error = None
def __call__(self, *args, **kwargs):
retval = None
if self.cancelled:
return
final_args = self.args + args if args else self.args
final_kwargs = dict(self.kwargs, **kwargs) if kwargs else self.kwargs
# self.fun may be a weakref
fun = self._fun_is_alive(self.fun)
if fun is not None:
try:
if self.ignore_result:
fun(*final_args, **final_kwargs)
ca = ()
ck = {}
else:
retval = fun(*final_args, **final_kwargs)
self.value = (ca, ck) = (retval,), {}
except Exception:
return self.throw()
else:
self.value = (ca, ck) = final_args, final_kwargs
self.ready = True
svpending = self._svpending
if svpending is not None:
try:
svpending(*ca, **ck)
finally:
self._svpending = None
else:
lvpending = self._lvpending
try:
while lvpending:
p = lvpending.popleft()
p(*ca, **ck)
finally:
self._lvpending = None
return retval
def _fun_is_alive(self, fun):
return fun() if self.weak else self.fun
def then(self, callback, on_error=None):
if not isinstance(callback, Thenable):
callback = promise(callback, on_error=on_error)
if self.cancelled:
callback.cancel()
return callback
if self.failed:
callback.throw(self.reason)
elif self.ready:
args, kwargs = self.value
callback(*args, **kwargs)
if self._lvpending is None:
svpending = self._svpending
if svpending is not None:
self._svpending, self._lvpending = None, deque([svpending])
else:
self._svpending = callback
return callback
self._lvpending.append(callback)
return callback
def throw1(self, exc=None):
if not self.cancelled:
exc = exc if exc is not None else sys.exc_info()[1]
self.failed, self.reason = True, exc
if self.on_error:
self.on_error(*self.args + (exc,), **self.kwargs)
def throw(self, exc=None, tb=None, propagate=True):
if not self.cancelled:
current_exc = sys.exc_info()[1]
exc = exc if exc is not None else current_exc
try:
self.throw1(exc)
svpending = self._svpending
if svpending is not None:
try:
svpending.throw1(exc)
finally:
self._svpending = None
else:
lvpending = self._lvpending
try:
while lvpending:
lvpending.popleft().throw1(exc)
finally:
self._lvpending = None
finally:
if self.on_error is None and propagate:
if tb is None and (exc is None or exc is current_exc):
raise
reraise(type(exc), exc, tb)
@property
def listeners(self):
if self._lvpending:
return self._lvpending
return [self._svpending]

View File

@@ -0,0 +1,104 @@
"""Synchronization primitives."""
from .abstract import Thenable
from .promises import promise
__all__ = ['barrier']
class barrier:
"""Barrier.
Synchronization primitive to call a callback after a list
of promises have been fulfilled.
Example:
.. code-block:: python
# Request supports the .then() method.
p1 = http.Request('http://a')
p2 = http.Request('http://b')
p3 = http.Request('http://c')
requests = [p1, p2, p3]
def all_done():
pass # all requests complete
b = barrier(requests).then(all_done)
# oops, we forgot we want another request
b.add(http.Request('http://d'))
Note that you cannot add new promises to a barrier after
the barrier is fulfilled.
"""
def __init__(self, promises=None, args=None, kwargs=None,
callback=None, size=None):
self.p = promise()
self.args = args or ()
self.kwargs = kwargs or {}
self._value = 0
self.size = size or 0
if not self.size and promises:
# iter(l) calls len(l) so generator wrappers
# can only return NotImplemented in the case the
# generator is not fully consumed yet.
plen = promises.__len__()
if plen is not NotImplemented:
self.size = plen
self.ready = self.failed = False
self.reason = None
self.cancelled = False
self.finalized = False
[self.add_noincr(p) for p in promises or []]
self.finalized = bool(promises or self.size)
if callback:
self.then(callback)
__slots__ = ( # noqa
'p', 'args', 'kwargs', '_value', 'size',
'ready', 'reason', 'cancelled', 'finalized',
'__weakref__',
# adding '__dict__' to get dynamic assignment
"__dict__",
)
def __call__(self, *args, **kwargs):
if not self.ready and not self.cancelled:
self._value += 1
if self.finalized and self._value >= self.size:
self.ready = True
self.p(*self.args, **self.kwargs)
def finalize(self):
if not self.finalized and self._value >= self.size:
self.p(*self.args, **self.kwargs)
self.finalized = True
def cancel(self):
self.cancelled = True
self.p.cancel()
def add_noincr(self, p):
if not self.cancelled:
if self.ready:
raise ValueError('Cannot add promise to full barrier')
p.then(self)
def add(self, p):
if not self.cancelled:
self.add_noincr(p)
self.size += 1
def then(self, callback, errback=None):
self.p.then(callback, errback)
def throw(self, *args, **kwargs):
if not self.cancelled:
self.p.throw(*args, **kwargs)
throw1 = throw
Thenable.register(barrier)

View File

@@ -0,0 +1,27 @@
"""Python compatibility utilities."""
from functools import WRAPPER_ASSIGNMENTS, WRAPPER_UPDATES, partial
from functools import update_wrapper as _update_wrapper
__all__ = ['update_wrapper', 'wraps']
def update_wrapper(wrapper, wrapped, *args, **kwargs):
"""Update wrapper, also setting .__wrapped__."""
wrapper = _update_wrapper(wrapper, wrapped, *args, **kwargs)
wrapper.__wrapped__ = wrapped
return wrapper
def wraps(wrapped,
assigned=WRAPPER_ASSIGNMENTS,
updated=WRAPPER_UPDATES):
"""Backport of Python 3.5 wraps that adds .__wrapped__."""
return partial(update_wrapper, wrapped=wrapped,
assigned=assigned, updated=updated)
def reraise(tp, value, tb=None):
"""Reraise exception."""
if value.__traceback__ is not tb:
raise value.with_traceback(tb)
raise value