Source code for m4us.core.containers

# -*- 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 `components` that can contain and link other `components`.

Containers are `components` that contain other `components` and `coroutines`,
linking them in various ways.  These objects make it convenient to group
`coroutines` together to make larger reusable `components`.  The `coroutines`
contained within containers can be linked not only to each other, but to the
containers as well, allowing the containers externally to act like any other
`components`.

"""


#---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

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

#---  Project imports
from m4us.core import components, utils, exceptions, interfaces


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


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


#---Classes--------------------------------------------------------------------

@interface.implementer(interfaces.IContainer)
@interface.provider(interfaces.IContainerFactory, interfaces.ICoroutineFactory)
[docs]class Graphline(components.Component): """Container `component` of `coroutines` with fully configurable `links`. This corresponds to Kamaelia_'s :class:`!Graphline` class. This class is useful for grouping several `coroutines` together, while still being able to specify special (i.e. non-standard) `links` among them. Where the :class:`Pipeline` class provides a strait linear linkage between it's `coroutines`, this class allows you to specify your own `links` as required. Only the `links` need to be specified. The unique collection of `coroutines` will be automatically extracted from them. Additionally, if a `source` or `sink` is specified as ``'self'``, then a `link` will be created with the :class:`Graphline` instance. This allows `messages` sent to the :class:`Graphline` instance to be forwarded on to the appropriate child `coroutine` and `messages` from a child to be sent out as the output from the :class:`Graphline`. Note, however, that unlike regular `links`, when using ``'self'``, `links` should be `outbox` to `outbox` or `inbox` to `inbox`. Kamaelia_ calls these types of `links` *pass-through linkages*. Example: >>> graphline = Graphline( ... ('self', 'inbox', coroutine1, 'inbox'), ... ('self', 'control', coroutine1, 'control'), ... (coroutine1, 'outbox', coroutine2, 'inbox'), ... (coroutine1, 'signal', coroutine2, 'control'), ... (coroutine1, 'outbox', coroutine3, 'inbox'), ... (coroutine1, 'signal', coroutine3, 'control'), ... (coroutine2, 'outbox', 'self', 'outbox'), ... (coroutine2, 'signal', 'self', 'signal'), ... (coroutine3, 'outbox', 'self', 'outbox'), ... (coroutine3, 'signal', 'self', 'signal'), ... ) >>> scheduler.register(*graphline.coroutines) >>> post_office.register(*graphline.links) In this example, `messages` sent to *graphline* will be forwarded on to *coroutine1*. `Messages` from *coroutine1* will be sent to both *coroutine2* and *coroutine3*. And `messages` from both *coroutine2* and *coroutine3* will be forwarded out from *graphline*. This is a good example of non-standard `links`. Assuming there is a `producer` and a `consumer` connected to either side of the graphline, then visually, the above example looks like this: .. digraph:: graphline rankdir=LR producer -> graphline [ label = "1" ] graphline -> consumer [ label = "5" ] graphline -> coroutine1 [ label = "2" ] coroutine1 -> coroutine2 [ label = "3" ] coroutine1 -> coroutine3 [ label = "3" ] coroutine2 -> graphline [ label = "4" ] coroutine3 -> graphline [ label = "4" ] :param collections.Sequence links: The `links` and `coroutines` to contain. Each `link` should be in the form of :samp:`({source}, {outbox}, {sink}, {inbox})`. :param collections.Mapping kwargs: Any keyword arguments will be set as attributes on the instance. :raises m4us.core.exceptions.InvalidLinkError: If an invalid `link` involving the :class:`Graphline`'s `mailboxes` is attempted. :implements: :class:`m4us.core.interfaces.ICoroutine` and :class:`m4us.core.interfaces.IContainer` :provides: :class:`m4us.core.interfaces.ICoroutineFactory` and :class:`m4us.core.interfaces.IContainerFactory` .. seealso:: The :class:`~m4us.core.interfaces.ICoroutine` and :class:`~m4us.core.interfaces.IContainer` `interfaces` for details on the methods and attributes provided by instances of this class. .. _Kamaelia: http://kamaelia.org """ def __init__(self, *links, **kwargs): """See class docstring for this method's documentation.""" components.Component.__init__(self, **kwargs) self.coroutines = self._get_coroutines_attribute(links) self.links = self._prepare_links(links) ## pylint: disable=R0201 def _add_sub_container(self, coroutine, coroutines, coroutines_set): """If the `coroutine` is a `container`, add include it's children. If the `coroutine` is an :class:`~m4us.core.interfaces.IContainer`, then it's `coroutines` are added to the :class:`list` and :class:`set` of `coroutines. :param ICoroutine coroutine: The `coroutine` to check. :param list coroutines: Pre-existing list of `coroutines` to extend. :param set coroutines_set: Pre-existing set of `coroutines` to extend. """ container = interfaces.IContainer(coroutine, None) if container: for sub_coroutine in container.coroutines: if sub_coroutine not in coroutines_set: coroutines.append(sub_coroutine) coroutines_set.add(sub_coroutine) ## pylint: enable=R0201 def _get_coroutines_attribute(self, links): """Extract and return the `coroutines` from the `links`. The `coroutines` are all unique `coroutines` from the given `links`, including the :class:`Graphline` instance. If any `coroutine` is also an :class:`~m4us.core.interfaces.IContainer`, then it's `coroutines` are included automatically. :param collections.Iterable links: The `post office` `links` given at instantiation. """ coroutines, coroutines_set = [self], set() for source, _, sink, _ in links: for coroutine in (source, sink): if coroutine != 'self' and coroutine not in coroutines_set: coroutines.append(coroutine) coroutines_set.add(coroutine) self._add_sub_container(coroutine, coroutines, coroutines_set) return tuple(coroutines) def _prepare_links(self, links): """Prepare and return the modified `links`. Any `links` that use ``'self'`` as the `source` or `sink` are converted to `links` to or from the :class:`Graphline` instance. :param collections.Iterable links: The `post office` `links` given at instantiation. If any `coroutine` is also an :class:`~m4us.core.interfaces.IContainer`, then it's `links` are included automatically. :raises m4us.core.exceptions.InvalidLinkError: If an invalid `link` involving the :class:`Graphline`'s `mailboxes` is attempted. .. seealso:: The class's docstring for more details on how ``'self'`` `links` are handled. """ prepared_links = set() for source, outbox, sink, inbox in links: if source == 'self': if outbox in ('outbox', 'signal'): ## pylint: disable=E1101 raise exceptions.InvalidLinkError(source_outbox=(source, outbox), sink_inbox=(sink, inbox)) ## pylint: enable=E1101 source = self if outbox == 'inbox': outbox = '_outbox_to_child' elif outbox == 'control': outbox = '_signal_to_child' if sink == 'self': if inbox in ('inbox', 'control'): ## pylint: disable=E1101 raise exceptions.InvalidLinkError(source_outbox=(source, outbox), sink_inbox=(sink, inbox)) ## pylint: enable=E1101 sink = self if inbox == 'outbox': inbox = '_inbox_from_child' elif inbox == 'signal': inbox = '_control_from_child' prepared_links.add((source, outbox, sink, inbox)) for coroutine in source, sink: if coroutine is self: continue container = interfaces.IContainer(coroutine, None) if container: prepared_links.update(container.links) return frozenset(prepared_links) ## pylint: disable=R0201 def _main(self): """Forward incoming and outgoing `messages` to and from the children. `Messages` sent in to this class are passed on to the appropriate child `coroutines` and `messages` from configured child `coroutines` are sent out from this class. :returns: The container's `coroutine`. :rtype: :class:`m4us.core.interfaces.ICoroutine` .. seealso:: The :meth:`~m4us.core.interfaces.IComponent._main` method for more details on this method. """ mailbox_map = { 'control': '_signal_to_child', '_inbox_from_child': 'outbox', '_control_from_child': 'signal', } inbox, message = (yield) while True: outbox = mailbox_map.get(inbox, '_outbox_to_child') if utils.is_shutdown(outbox, message, 'signal'): yield outbox, message break inbox, message = (yield outbox, message) ## pylint: enable=R0201
@interface.provider(interfaces.IContainerFactory, interfaces.ICoroutineFactory)
[docs]class Pipeline(Graphline): """Container `component` of `coroutines` that `links` them in a pipeline. This corresponds to Kamaelia_'s :class:`!Pipeline` class. A pipeline consists of chaining each of the given `coroutines` together in order such that the ``outbox`` and ``signal`` `outboxes` of each `coroutine` are linked to the ``inbox`` and ``control`` `inboxes` of the next one. Additionally, the :class:`Pipeline`'s ``inbox`` and ``control`` `inboxes` are linked to the first `coroutine`'s ``inbox`` and ``control`` `inboxes`. Similarily, the :class:`Pipeline`'s ``outbox`` and ``signal`` `outboxes` are linked to the last `coroutine`'s ``outbox`` and ``signal`` `outboxes`. This means that the pipeline can be treated like any other `coroutine`, responding to incomming `messages` and emitting outgoing `messages`. Example: >>> pipeline = Pipeline(coroutine1, coroutine2, coroutine3) >>> scheduler.register(*pipeline.coroutines) >>> post_office.register(*pipeline.links) In this example, `messages` sent to *pipeline* will be forwarded on to *coroutine1*, who's `messages` will be sent to *coroutine2*, who's `messages` will be sent to *coroutine3*, who's `messages` will be forwarded out of *pipeline*. Assuming there is a `producer` and a `consumer` connected to either side of the pipeline, then visually, the above example looks like this: .. digraph:: pipeline rankdir=LR producer -> pipeline [ label = "1" ] pipeline -> consumer [ label = "6" ] pipeline -> coroutine1 [ label = "2" ] coroutine1 -> coroutine2 [ label = "3" ] coroutine2 -> coroutine3 [ label = "4" ] coroutine3 -> pipeline [ label = "5" ] :param m4us.core.interfaces.ICoroutine first_coroutine: The first `coroutine` in the pipeline. It will receive incomming `messages` to the pipeline. :param m4us.core.interfaces.ICoroutine second_coroutine: The second `coroutine` in the pipeline. It will receive incomming `messages` to the pipeline. :param collections.Sequence other_coroutines: Any other `coroutines` in the pipeline, in order. `Messages` emmitted by the last one will be emitted by the pipeline. :param collections.Mapping kwargs: Like other `components`, any given keyword arguments will be set as instance attributes. :implements: :class:`m4us.core.interfaces.ICoroutine` and :class:`m4us.core.interfaces.IContainer` :provides: :class:`m4us.core.interfaces.ICoroutineFactory` and :class:`m4us.core.interfaces.IContainerFactory` .. seealso:: The :class:`~m4us.core.interfaces.ICoroutine` and :class:`~m4us.core.interfaces.IContainer` `interfaces` for details on the methods and attributes provided by instances of this class. """ def __init__(self, first_coroutine, second_coroutine, *other_coroutines, **kwargs): """See class docstring for this method's documentation.""" self._initial_coroutines = ((first_coroutine, second_coroutine) + other_coroutines) links = self._build_links(first_coroutine, second_coroutine, *other_coroutines) ## pylint: disable=W0142 Graphline.__init__(self, *links, **kwargs) ## pylint: enable=W0142 ## pylint: disable=R0201 def _build_links(self, *coroutines): """Construct and return the set of pipeline `links`. This method generates a set of `links` chaining all the given `coroutines` together in a pipeline. The set of links is in the format that the :class:`Graphline` parent class accepts. Additionally, the class's ``inbox``, ``control``, ``outbox`` and ``signal`` `mailboxes` are linked to the first and last `coroutines` appropriately so that `messages` to and from the class itself are delivered as expected. :param collections.Sequence coroutines: The `coroutines` to link together. :returns: The collection of `links` to pass to the parent :class:`Graphline` class. :rtype: :obj:`set` .. seealso:: The :class:`Graphline` class for details on the `link` format and the use of ``'self'`` in them. """ links = utils.easy_link(*coroutines) links.update(( ('self', 'inbox', coroutines[0], 'inbox'), ('self', 'control', coroutines[0], 'control'), (coroutines[-1], 'outbox', 'self', 'outbox'), (coroutines[-1], 'signal', 'self', 'signal'), )) return links ## pylint: enable=R0201 def _get_coroutines_attribute(self, links): """Extract and return the `coroutines` in the `links`. This method has been overridden to handle sub-containers while still preserving the coroutine order as best as it can. :param collections.Iterable links: The `post office` `links` given at instantiation. *(Not used)*. """ coroutines, coroutines_set = [self], set() for coroutine in self._initial_coroutines: if coroutine not in coroutines_set: coroutines.append(coroutine) coroutines_set.add(coroutine) # This order is important. sub_container coroutine should be # ordered immediately after the parent container to keep actual # execution order fair. self._add_sub_container(coroutine, coroutines, coroutines_set) return tuple(coroutines) #---Module initialization------------------------------------------------------ #---Late Imports--------------------------------------------------------------- #---Late Globals--------------------------------------------------------------- #---Late Functions------------------------------------------------------------- #---Late Classes--------------------------------------------------------------- #---Late Module initialization-------------------------------------------------