Source code for m4us.core.coroutines

# -*- coding: utf-8 -*-

#---Header---------------------------------------------------------------------

# This file is part of Message For You Sir (m4us).
# Copyright © 2009-2012 Krys Lawrence
#
# Message For You Sir is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at your
# option) any later version.
#
# Message For You Sir is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License
# for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Message For You Sir.  If not, see <http://www.gnu.org/licenses/>.


"""Provides support for using regular Python_ `coroutines`.

.. _Python: http://python.org

"""


#---Imports--------------------------------------------------------------------

#---  Standard library imports
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
## pylint: disable=W0622, W0611
from future_builtins import ascii, filter, hex, map, oct, zip  ## NOQA
## pylint: enable=W0622, W0611

import types

#---  Third-party imports
## pylint: disable=F0401
from zope import interface
## pylint: enable=F0401
import decorator

#---  Project imports
from m4us.core import utils, interfaces, messages
# This is imported directly for the sample coroutine
from m4us.core.utils import is_shutdown


#---Globals--------------------------------------------------------------------

## pylint: disable=C0103
_coroutine_stand_in_registry = None
## pylint: enable=C0103

_COROUTINE_FACTORY_TEMPLATE = """\
def %(name)s(%(signature)s):
    from {module_path} import {factory_name} as factory
    return factory(_func_, %(signature)s)
"""


#---Functions------------------------------------------------------------------

def _interface_adapter_hook(interface_, coroutine_, registry=None):
    """`Adapter` to adapt Python_ `coroutines` to their provided `interfaces`.

    `Components`, classes and regular functions can (and are expected to) just
    declare provided interfaces directly.  They do not need this adapter.

    Python_ `coroutines`, on the other hand, are really instances of
    :data:`types.GeneratorType`.  They cannot be made to directly declare
    proivided interfaces because one cannot add additional attributes to the
    `generator` object.  That is where this `adapter` comes in.

    This `adapter` just checks to see if the `coroutine` has a registered
    stand-in object in the registry and that the stand-in provides the given
    `interface`.  If it does, the `coroutine` is returned unchanged.
    Otherwise, :obj:`None` is returned, which signals that adaptation was not
    possible.

    Since the :func:`coroutine` decorator automatically registers interfaces
    implemented by the `coroutine`'s `factory` function as those provided by
    the `coroutine` itself, adaptation to the given `interface` happens
    transparently.

    :param zope.interface.Interface interface_: The `interface` that must be
      provided.
    :param m4us.core.interfaces.ICoroutine coroutine_: The `coroutine` to
      verify.

    :returns: The given `coroutine` if it provides the `interface` or
      :obj:`None` otherwise.
    :rtype: :class:`m4us.core.interfaces.ICoroutine` or :obj:`None`

    """
    if registry is None:
        registry = _coroutine_stand_in_registry
    try:
        stand_in = registry[coroutine_]
    except (KeyError, TypeError):
        return
    if interface_.providedBy(stand_in):
        return coroutine_


def _curried_coroutine_factory(function, coroutine_factory_path):
    """Return a `coroutine` factory that calls the factory with function.

    This function is a bit difficult to explain.  It basically curries the
    given function into the `coroutine` factory function given by it's import
    path.  It also fully preserves the function's signature, exept for the
    first parameter, which is stripped.  It is meant to be used in decorators
    like :func:`filter_`.

    It uses a template to construct the curried factory, which is why
    *coroutine_factory_path* is given as an import path string.  E.g.
    ``'m4us.core.coroutines.filter_from_callable'``.  The use of the template
    is required to preserve the function's signature on the curried factory.

    The idea is that *coroutine_factory_path* would point to a `coroutine`
    factory like :func:`filter_from_callable` which takes a function whose
    first parameter is the message object.  The resulting factory function
    accepts all additional arguments except the first one in case the function
    requires additional arguments.

    :param types.FunctionType function: The function to wrap.  It must accept
      at least one argument.
    :param str coroutine_factory_path: The import path to the coroutine factory
      function to call.

    :returns: A coroutine factory with the function curried into it.
    :rtype: :class:`m4us.core.interfaces.ICoroutineFactory`

    :raises exceptions.TypeError: If the given function is not a function.
    :raises exceptions.ValueError: If the given function does not accept at
      least one positional argument.

    :seealso: :func:`~m4us.core.utils._function_wrapper_from_template` for
      additional details.

    """
    module_path, factory_name = coroutine_factory_path.rsplit('.', 1)
    template = _COROUTINE_FACTORY_TEMPLATE.format(module_path=module_path,
      factory_name=factory_name)
    ## pylint: disable=W0212
    factory = utils._function_wrapper_from_template(function, template,
      strip_first_parameter=True)
    ## pylint: enable=W0212
    interface.alsoProvides(factory, interfaces.ICoroutineFactory)
    return factory


[docs]def init(): """Initialize `coroutine` `interface` support. This function must be called before any other part of m4us is used. It sets up global interface and adapter registrations neccessary for the proper functioning of m4us. It has been made an explicit function in order to avoid import-time side-effects, which is considered bad practice. It is recommended that this function be called either at import time or as the first call in a program's main function. """ ## pylint: disable=W0603 global _coroutine_stand_in_registry ## pylint: enable=W0603 if _coroutine_stand_in_registry is not None: return # All Python coroutines provide ICoroutine. interface.classImplements(types.GeneratorType, interfaces.ICoroutine) # Python coroutines cannot have extra attributes (like __provided__ used by # zope.interface, so an attribute stand-in can be used by getting it from # this registry. ## pylint: disable=W0212 _coroutine_stand_in_registry = utils._ObjectStandInRegistry() ## pylint: enable=W0212 interface.interface.adapter_hooks.append(_interface_adapter_hook)
[docs]def coroutine(lazy=True): """Decorator factory to automatically activate Python_ `coroutines`. Before a Python_ `coroutine` can be used, it needs to be activated by sending :obj:`None` as it's first `message`. This decorator does this automatically. `Coroutines` are presumptively `lazy`. This decorator can flag the given `coroutine` as not `lazy` by making it adaptable to the :class:`~m4us.core.interfaces.INotLazy` marker `interface`. This decorator also registers the function as providing :class:`~m4us.core.interfaces.ICoroutineFactory` and registers all `interfaces` implemented by the function as being provided by the `coroutine` that the function returns. :param bool lazy: Specifies whether or not the `coroutine` is `lazy`. :returns: A wrapper function that will activate and return the `coroutine` when called. :rtype: :data:`types.FunctionType` .. warning:: When using this decorator with the zope.interface.implementer_ and/or zope.interface.provider_ decorators, make sure this decorator is the outer-most one. Otherwise interface declarations will get lost. .. _zope.interface.implementer: http://docs.zope.org/zope.interface/README.html #declaring-implemented-interfaces .. _zope.interface.provider: http://docs.zope.org/zope.interface/README.html #declaring-provided-interfaces """ def _coroutine_factory(function, *args, **kwargs): """Return an activated coroutine, with/without `lazy` registration.""" coroutine_ = function(*args, **kwargs) coroutine_.send(None) interfaces_to_provide = list(interface.implementedBy(function)) # non-lazy Python coroutines need to be adapted to INotLazy. if not lazy: interfaces_to_provide.append(interfaces.INotLazy) if interfaces_to_provide: stand_in = _coroutine_stand_in_registry.register(coroutine_) ## pylint: disable=W0142 interface.directlyProvides(stand_in, *interfaces_to_provide) ## pylint: enable=W0142 return coroutine_ def _coroutine_decorator(function): """Decorator for creating `coroutine` factories.""" coroutine_factory = decorator.decorator(_coroutine_factory, function) interface.alsoProvides(coroutine_factory, interfaces.ICoroutineFactory) return coroutine_factory return _coroutine_decorator # [[start sample_coroutine]]
@coroutine()
[docs]def sample_coroutine(): """Pass all messages through.""" inbox, message = (yield) while True: if is_shutdown(inbox, message): yield 'signal', message break ## Your code goes here. inbox, message = (yield 'outbox', message) # [[end sample_coroutine]] # The docstring for sample_coroutine is set like this so that it can include # it's own source code in the docs. The literalinclude directive won't work # completely correctly if the docstring is inline. ## pylint: disable=W0622
sample_coroutine.__doc__ = """Pass all `messages` through. This `coroutine` is meant to provide a canonical example of what a `coroutine` used with this project looks like. Any `messages` sent to it on any `inbox` will be sent back out on it's ``outbox`` `outbox`. It is also well behaved in that it will shutdown on any :class:`~m4us.core.interfaces.IShutdown` `message`, forwarding it on before quitting. The full code for this `coroutine` is: .. literalinclude:: ../../../../m4us/core/coroutines.py :linenos: :start-after: # [[start sample_coroutine]] :end-before: # [[end sample_coroutine]] :returns: A pass-through `coroutine` :rtype: :data:`types.GeneratorType` :implements: :class:`m4us.core.interfaces.ICoroutine` :provides: :class:`m4us.core.interfaces.ICoroutineFactory` .. note:: `Producers` and `filters` need a minimum of 2 :keyword:`yield` statements as the output of the first one is always thrown away. The output of the second one is the first `message` delivered. On the other hand, the first :keyword:`yield` will be the one that gets the first incoming `message`. """ ## pylint: enable=W0622 @coroutine()
[docs]def null_sink(): """Swallow all messages except :class:`~m4us.core.interfaces.IShutdown`. This `coroutine` can serve as an end point in a series of connected `coroutines`. All `messages` sent to it, except :class:`~m4us.core.interfaces.IShutdown` `messages` are ignored and not re-emitted. The `coroutine` will shutdown on any :class:`~m4us.core.interfaces.IShutdown` `message`, forwarding it on before quitting. :returns: A null sink `coroutine` :rtype: :data:`types.GeneratorType` :implements: :class:`m4us.core.interfaces.ICoroutine` :provides: :class:`m4us.core.interfaces.ICoroutineFactory` """ while True: inbox, message = (yield) if utils.is_shutdown(inbox, message): yield 'signal', message break
@coroutine()
[docs]def filter_from_callable(callable_, *args, **kwargs): """Turn an ordinary callable (function, etc.) into a proper `coroutine`. This is a convenience `coroutine` that turns ordinary callables (functions, etc.) into proper `coroutines`. The callable is passed ``inbox`` `messages` and it's return value is emited on ``outbox``. The given callable must accept at least a single positional argument, the incomming ``inbox`` `message`, and return whatever should be the ``outbox`` `message` to emit. Any additional arguments passed to this function will be passed to the callable each time it is called. Any ``control`` or non-``inbox`` `messages` sent in will be ignored and not passed to the callable. Additionally, by default, if the callable returns :obj:`None`, the :obj:`None` will be emitted on ``outbox`` as the `message`. This behaviour can be changed by passing ``suppress_none=True`` as a keyword argument. When *suppress_none* is True, if the callable returns :obj:`None`, only a plain :obj:`None` will be yielded. This means that no `message` will be passed on to any other `coroutine`. Non-:obj:`None` `messages` will still be emitted on the ``outbox`` `outbox`, however. :param collections.Callable callable\_: The callable to which to pass messages. :param bool suppress_none: Indicate whether or not :obj:`None` should be emitted on the ``outbox`` `outbox`. (Default: :obj:`True`) :param collections.Sequence args: Any additional positional arguments to pass to the callable. :param collections.Mapping kwargs: Any additional keyword arguments to pass to the callable. :returns: A filter `coroutine` :rtype: :data:`types.GeneratorType` :raises exceptions.TypeError: If the given callable is not actually a callable. :implements: :class:`m4us.core.interfaces.ICoroutine` :provides: :class:`m4us.core.interfaces.ICoroutineFactory` """ if not callable(callable_): raise TypeError('Argument must be a callable.') suppress_none = kwargs.pop('suppress_none', False) inbox, message = (yield) while True: if is_shutdown(inbox, message): yield 'signal', message break if inbox != 'inbox': inbox, message = (yield) continue message = callable_(message, *args, **kwargs) if interfaces.IShutdown(message, False): yield 'signal', message break if message is None and suppress_none: inbox, message = (yield) else: inbox, message = (yield 'outbox', message)
[docs]def filter_(function): """Decorator to turn an ordinary function into a proper `coroutine`. This decorator returns a factory function that when called, just calls :func:`filter_from_callable` and returns the result. Any additional arguments passed the to factory function are passed on to :func:`filter_from_callable`. The factory function is also registered as providing :class:`~m4us.core.interfaces.ICoroutineFactory`. :param types.FunctionType function: The function to turn into a `coroutine`. It must accept at least one argument. :returns: A filter coroutine factory function. :rtype: :class:`m4us.core.interfaces.ICoroutineFactory` :raises exceptions.TypeError: If the given function is not a function. :raises exceptions.ValueError: If the given function does not accept at least one positional argument. :seealso: :func:`filter_from_callable` for the specifics. """ return _curried_coroutine_factory(function, 'm4us.core.coroutines.filter_from_callable')
@coroutine(lazy=False)
[docs]def producer_from_iterable(iterable): """Return a `producer` `coroutine` that iterates over an iterable. This is a convenience `coroutine` that emits ``outbox`` messages that are the results of iterating over an iterable. When all the iterable results have been emitted, an :class:`~m4us.core.interfaces.IProducerFinished` messages is emitted. Then the `producer` terminates. :param collections.Iterable iterable: The iterable object over which to iterate. :returns: A non-lazy producer `coroutine` :rtype: :data:`types.GeneratorType` :raises exceptions.TypeError: If the given object is not an iterable or it does not produce a valid iterator. :implements: :class:`m4us.core.interfaces.ICoroutine` :provides: :class:`m4us.core.interfaces.ICoroutineFactory` """ iterator = iter(iterable) inbox, message = (yield) for element in iterator: if is_shutdown(inbox, message): yield 'signal', message return inbox, message = (yield 'outbox', element) yield 'signal', messages.ProducerFinished()
[docs]def producer(generator_factory): """Decorator that turns a generator function into a `producer` `coroutine`. This decorator returns a factory function that when called, just calls the generator function and passes the result to :func:`producer_from_iterable`, returning the result. Any additional arguments passed the to factory function are passed on to to the generator factory. The factory function is also registered as providing :class:`~m4us.core.interfaces.ICoroutineFactory`. :param types.FunctionType generator_factory: The generator function to turn into a `producer`. :returns: A `producer` `coroutine` factory function. :rtype: :class:`m4us.core.interfaces.ICoroutineFactory` :raises exceptions.TypeError: If the given generator factory is not a callable. .. note:: Technically this decorator will work with any function that returns an iterable. A generator function is not strictly required, though that is the expected typical use case. :seealso: :func:`producer_from_iterable` for the specifics. """ def _producer_factory(generator_factory, *args, **kwargs): """Return a `producer` `coroutine` from the iterable factory.""" generator = generator_factory(*args, **kwargs) return producer_from_iterable(generator) if not callable(generator_factory): raise TypeError('Argument must be a callable that returns an iterable') producer_factory = decorator.decorator(_producer_factory, generator_factory) interface.alsoProvides(producer_factory, interfaces.ICoroutineFactory) return producer_factory
@interface.provider(interfaces.ICoroutineFactory)
[docs]def sink_from_callable(callable_, *args, **kwargs): """Turn an ordinary callable (function, etc.) into a `sink` `coroutine`. This is a convenience function that turns ordinary callables (functions, etc.) into `sink` `coroutines`. The callable is passed ``inbox`` `messages` and it's return value suppressed, unless it is an :class:`~m4us.core.interfaces.IShutdown` message. The given callable must accept at least a single positional argument, the incomming ``inbox`` `message`, and return nothing. It may return an :class:`~m4us.core.interfaces.IShutdown` message, however, if appropriate, but it should not normally be necessary. Any additional arguments passed to this function will be passed to the callable each time it is called. Any ``control`` or non-``inbox`` `messages` sent in will be ignored and not passed to the callable. :param collections.Callable callable\_: The callable to which to pass messages. :param collections.Sequence args: Any additional positional arguments to pass to the callable. :param collections.Mapping kwargs: Any additional keyword arguments to pass to the callable. :returns: A `sink` `coroutine` :rtype: :data:`types.GeneratorType` :raises exceptions.TypeError: If the given callable is not actually a callable. :implements: :class:`m4us.core.interfaces.ICoroutine` :provides: :class:`m4us.core.interfaces.ICoroutineFactory` """ def _sink_from_callable(*args, **kwargs): """Return a `sink` `coroutine` from that calls the callable.""" result = callable_(*args, **kwargs) if interfaces.IShutdown(result, False): return result if not callable(callable_): raise TypeError('Argument must be a callable.') kwargs.pop('suppress_none', None) return filter_from_callable(_sink_from_callable, suppress_none=True, *args, **kwargs)
[docs]def sink(function): """Decorator to turn an ordinary function into a `sink` `coroutine`. This decorator returns a factory function that when called, just calls :func:`sink_from_callable` and returns the result. Any additional arguments passed the to factory function are passed on to :func:`sink_from_callable`. The factory function is also registered as providing :class:`~m4us.core.interfaces.ICoroutineFactory`. :param types.FunctionType function: The function to turn into a `sink` `coroutine`. It must accept at least one argument. :returns: A `sink` `coroutine` factory function. :rtype: :class:`m4us.core.interfaces.ICoroutineFactory` :raises exceptions.TypeError: If the given function is not a function. :raises exceptions.ValueError: If the given function does not accept at least one positional argument. .. seealso:: :func:`sink_from_callable` for the specifics. """ return _curried_coroutine_factory(function, 'm4us.core.coroutines.sink_from_callable') #---Classes-------------------------------------------------------------------- #---Module initialization------------------------------------------------------ #---Late Imports--------------------------------------------------------------- #---Late Globals--------------------------------------------------------------- #---Late Functions------------------------------------------------------------- #---Late Classes--------------------------------------------------------------- #---Late Module initialization-------------------------------------------------