Source code for gentools.core

import sys
from functools import partial, reduce
from operator import attrgetter, itemgetter

from .types import (GeneratorCallable, GeneratorType, ReusableGenerator,
                    T_send, T_yield)
from .utils import PY2, compose

__all__ = [
    'reusable',
    'oneyield',
    'sendreturn',

    'relay',
    'map_yield',
    'map_send',
    'map_return',

    'py2_compatible',
    'return_',

    'compose',
    'stopiter_value',

    'imap_yield',
    'imap_send',
    'imap_return',
    'irelay',
]


if PY2:  # pragma: no cover
    from funcsigs import signature
else:
    from inspect import signature


def _is_just_started(gen):
    return gen.gi_frame.f_lasti == -1


class GeneratorReturn(BaseException):
    pass


[docs]def py2_compatible(func): """Decorate a generator function to make it Python 2/3 compatible. Use together with :func:`return_`. Example ------- >>> @py2_compatible ... def my_max(value): ... while value < 100: ... newvalue = yield value ... if newvalue > value: ... value = newvalue ... return_(value) is equivalent to: >>> def my_max(value): ... while value < 100: ... newvalue = yield value ... if newvalue > value: ... value = newvalue ... return value Note ---- This is necessary because PEP479 makes it impossible to replace ``return`` with ``raise StopIteration`` in newer python 3 versions. Warning ------- Although the wrapped generator acts like a generator, it is not an strict generator instance. For most purposes (e.g. ``yield from``) it works fine, but :func:`~inspect.isgenerator` will return ``False``. See also -------- `PEP 479 <https://www.python.org/dev/peps/pep-0479/>`_ """ return compose(GeneratorProxy, func)
class _catch_genreturn_context(object): __slots__ = () def __enter__(self): pass def __exit__(self, exc_type, exc, tb): if exc_type and issubclass(exc_type, GeneratorReturn): raise StopIteration(exc.args[0]) _catch_genreturn = _catch_genreturn_context() class GeneratorProxy(object): """a python2&3-compatible generator proxy This is needed to provide a consistent way to "return" from a generator """ __slots__ = '_gen' def __init__(self, gen): assert isinstance(gen, GeneratorType) self._gen = gen gi_running = property(attrgetter('_gen.gi_running')) gi_frame = property(attrgetter('_gen.gi_frame')) gi_code = property(attrgetter('_gen.gi_code')) def __iter__(self): return self def send(self, value): with _catch_genreturn: return self._gen.send(value) def __next__(self): with _catch_genreturn: return next(self._gen) if PY2: # pragma: no cover next = __next__ def close(self): try: self._gen.close() except GeneratorReturn as e: pass def throw(self, *args): with _catch_genreturn: return self._gen.throw(*args) __del__ = close
[docs]def return_(value): """Python 2/3 compatible way to return a value from a generator Use only with the :func:`py2_compatible` decorator""" raise GeneratorReturn(value)
[docs]def reusable(func): """Create a reusable class from a generator function Parameters ---------- func: GeneratorCallable[T_yield, T_send, T_return] the function to wrap Note ---- * the callable must have an inspectable signature * If bound to a class, the new reusable generator is callable as a method. To opt out of this, add a :func:`staticmethod` decorator above this decorator. """ sig = signature(func) origin = func while hasattr(origin, '__wrapped__'): origin = origin.__wrapped__ return type( origin.__name__, (ReusableGenerator, ), dict([ ('__doc__', origin.__doc__), ('__module__', origin.__module__), ('__signature__', sig), ('__wrapped__', staticmethod(func)), ] + [ (name, property(compose(itemgetter(name), attrgetter('_bound_args.arguments')))) for name in sig.parameters ] + ([ ('__qualname__', origin.__qualname__), ] if sys.version_info > (3, ) else [])))
[docs]class oneyield(GeneratorCallable[T_yield, T_send, T_send]): """Decorate a function to turn it into a basic generator The resulting generator yields the function's return value once, and then returns the value it is sent (with ``send()``). """
[docs] def __init__(self, func): self.__wrapped__ = func
@py2_compatible def __call__(self, *args, **kwargs): return_((yield self.__wrapped__(*args, **kwargs)))
def stopiter_value(exc): try: return exc.args[0] except IndexError: return class yield_from(object): """Use this class to build python2/3-compatible ``yield from``-patterns Example ------- >>> @py2_compatible ... def delegator(gen): ... yielder = yield_from(gen) ... for item in yielder: ... with yielder: ... yielder.send((yield item)) ... return_(yielder.result) is equivalent to: >>> def delegator(gen) ... return (yield from gen) See also -------- `PEP 380 <https://www.python.org/dev/peps/pep-0380/#formal-semantics>`_ """ __slots__ = ('result', '_finished', '_gen', '_sent', '_next') def __init__(self, gen): self._finished = False self._gen = iter(gen) try: self._next = next(self._gen) except StopIteration as e: self.result = stopiter_value(e) self._finished = True def __iter__(self): return self def __next__(self): if self._finished: raise StopIteration() self._sent = None return self._next if PY2: # pragma: no cover next = __next__ def send(self, value): self._sent = value def __enter__(self): return self def __exit__(self, exc_cls, exc, tb): if exc_cls is None: try: if self._sent is None: self._next = next(self._gen) else: self._next = self._gen.send(self._sent) except StopIteration as _e: self._finished = True self.result = stopiter_value(_e) elif issubclass(exc_cls, GeneratorExit): try: close = self._gen.close except AttributeError: pass else: close() raise exc else: _x = (exc_cls, exc, tb) try: throw = self._gen.throw except AttributeError: raise exc else: try: self._next = throw(*_x) except StopIteration as _e: self._finished = True self.result = stopiter_value(_e) return True
[docs]def sendreturn(gen, value): """Send an item into a generator expecting a final return value Parameters ---------- gen: ~typing.Generator[T_yield, T_send, T_return] the generator to send the value to value: T_send the value to send Raises ------ RuntimeError if the generator did not return as expected Returns ------- T_return the generator's return value """ try: gen.send(value) except StopIteration as e: return stopiter_value(e) else: raise RuntimeError('generator did not return as expected')
@py2_compatible def imap_yield(func, gen): """Apply a function to all ``yield`` values of a generator Parameters ---------- func: ~typing.Callable[[T_yield], T_mapped] the function to apply gen: Generable[T_yield, T_send, T_return] the generator iterable. Returns ------- ~typing.Generator[T_mapped, T_send, T_return] the mapped generator """ gen = iter(gen) assert _is_just_started(gen) yielder = yield_from(gen) for item in yielder: with yielder: yielder.send((yield func(item))) return_(yielder.result) @py2_compatible def imap_send(func, gen): """Apply a function to all ``send`` values of a generator Parameters ---------- func: ~typing.Callable[[T_send], T_mapped] the function to apply gen: Generable[T_yield, T_mapped, T_return] the generator iterable. Returns ------- ~typing.Generator[T_yield, T_send, T_return] the mapped generator """ gen = iter(gen) assert _is_just_started(gen) yielder = yield_from(gen) for item in yielder: with yielder: yielder.send(func((yield item))) return_(yielder.result) @py2_compatible def imap_return(func, gen): """Apply a function to the ``return`` value of a generator Parameters ---------- func: ~typing.Callable[[T_return], T_mapped] the function to apply gen: Generable[T_yield, T_send, T_return] the generator iterable. Returns ------- ~typing.Generator[T_yield, T_send, T_mapped] """ gen = iter(gen) assert _is_just_started(gen) yielder = yield_from(gen) for item in yielder: with yielder: yielder.send((yield item)) return_(func(yielder.result)) @py2_compatible def irelay(gen, thru): """Create a new generator by relaying yield/send interactions through another generator Parameters ---------- gen: Generable[T_yield, T_send, T_return] the original generator thru: ~typing.Callable[[T_yield], ~typing.Generator] the generator callable through which each interaction is relayed Returns ------- ~typing.Generator the relayed generator """ gen = iter(gen) assert _is_just_started(gen) yielder = yield_from(gen) for item in yielder: with yielder: subgen = thru(item) subyielder = yield_from(subgen) for subitem in subyielder: with subyielder: subyielder.send((yield subitem)) yielder.send(subyielder.result) return_(yielder.result)
[docs]class map_yield: """Decorate a generator callable to apply a function to each ``yield`` value Example ------- >>> @map_yield('the current max is: {}'.format) ... def my_max(value): ... while value < 100: ... newvalue = yield value ... if newvalue > value: ... value = newvalue ... return value ... >>> gen = my_max(5) >>> next(gen) 'the current max is: 5' >>> gen.send(11) 'the current max is: 11' >>> gen.send(104) StopIteration(104) See also -------- :func:`~gentools.core.imap_yield` """
[docs] def __init__(self, *funcs): self._mapper = compose(*funcs)
[docs] def __call__(self, func): return compose(partial(imap_yield, self._mapper), func)
[docs]class map_send: """Decorate a generator callable to apply functions to each ``send`` value Example ------- >>> @map_send(int) ... def my_max(value): ... while value < 100: ... newvalue = yield value ... if newvalue > value: ... value = newvalue ... return value ... >>> gen = my_max(5) >>> next(gen) 5 >>> gen.send(11.3) 11 >>> gen.send('104') 104 See also -------- :func:`~gentools.core.imap_send` """
[docs] def __init__(self, *funcs): self._mapper = compose(*funcs)
[docs] def __call__(self, func): return compose(partial(imap_send, self._mapper), func)
[docs]class map_return: """Decorate a generator callable to apply functions to the ``return`` value Example ------- >>> @map_return('final value: {}'.format) ... def my_max(value): ... while value < 100: ... newvalue = yield value ... if newvalue > value: ... value = newvalue ... return value ... >>> gen = my_max(5) >>> next(gen) 5 >>> gen.send(11.3) 11.3 >>> gen.send(104) StopIteration('final value: 104') See also -------- :func:`~gentools.core.imap_return` """
[docs] def __init__(self, *funcs): self._mapper = compose(*funcs)
[docs] def __call__(self, func): return compose(partial(imap_return, self._mapper), func)
[docs]class relay: """Decorate a generator callable to relay yield/send values through another generator Example ------- >>> def try_until_positive(outvalue): ... value = yield outvalue ... while value < 0: ... value = yield 'not positive, try again' ... return value ... >>> @relay(try_until_positive) ... def my_max(value): ... while value < 100: ... newvalue = yield value ... if newvalue > value: ... value = newvalue ... return value ... >>> gen = my_max(5) >>> next(gen) 5 >>> gen.send(-4) 'not positive, try again' >>> gen.send(-1) 'not positive, try again' >>> gen.send(8) 8 >>> gen.send(104) StopIteration(104) See also -------- :func:`~gentools.core.irelay` """
[docs] def __init__(self, *genfuncs): self._genfuncs = genfuncs
[docs] def __call__(self, func): return compose(partial(reduce, irelay, self._genfuncs), func)