Source code for m4us.core.interfaces

# -*- 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 the `interface` definitions for all important core objects."""

## pylint: disable=E0211,R0903,E0213,W0232


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


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


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


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

#---  Messages interfaces

[docs]class IMessageFactory(interface.Interface): """`Interface` for `callables` that return :class:`IMessage` objects."""
[docs] def __call__(**kwargs): # pragma: no branch """Return the desired `message`. The `callable` should only be called with keyword arguments. :param collections.Mapping kwargs: Keywords arguments to pass to the `factory` `callable`. :returns: The desired `message` object. :rtype: :class:`IMessage` .. note:: All unrecognized keyword arguments should be set as attributes on the :class:`IMessage` object. """
[docs]class IMessage(interface.Interface): """`Interface` for special `messages` passed between `coroutines`."""
[docs]class IShutdown(IMessage): """`Interface` for `messages` that tell `coroutines` to shutdown. Upon receipt of an :class:`IShutdown` `message`, `coroutines` should clean up any loose ends, forward on the :class:`IShutdown` `message` on it's ``signal`` `outbox` and then shutdown. """
[docs]class IProducerFinished(IShutdown): """`Interface` for `messages` signalling that a `producer` is done.""" #--- Coroutines interfaces
[docs]class ICoroutineFactory(interface.Interface): """`Interface` for `callables` that return :class:`ICoroutine` objects."""
[docs] def __call__(*args, **kwargs): # pragma: no branch """Return the desired `coroutine`. :param collections.Sequence args: Any arguments to pass to the `factory` `callable`. :param collections.Mapping kwargs: Keywords arguments to pass to the `factory` `callable`. :returns: The desired `coroutine`. :rtype: :class:`ICoroutine` """
[docs]class ICoroutine(interface.Interface): """`Interface` that defines a Python_ `coroutine`. Python_ `coroutines` are defined in :PEP:`342`. .. note:: While Python_ `coroutines` are an extension of `generators`, and as such define a ``next()`` method, this `interface` does not include the ``next()`` method as it really only makes sense in the context of actual `generators` rather than for `coroutines`. .. note:: By default, all Python_ `coroutines` (i.e. :data:`types.GeneratorType`) have been automatically configured to provide :class:`ICoroutine`. This makes working with Python_ `coroutines` more natural. .. _Python: http://python.org/ """
[docs] def send(message): # pragma: no branch """Send the `message` to the `coroutine`. :param message: The `message` object to send into the `coroutine`. For this project it will normally be a 2-:obj:`tuple` of the `inbox` and the `message`. :returns: Any `message` :keyword:`yield`\ed by the `coroutine`. For this project it will normally be a 2-:obj:`tuple` of the `outbox` and the `message`. :raises exceptions.StopIteration: If the `coroutine` has terminated, either by :keyword:`return`\ing (as opposed to :keyword:`yield`\ing) or because the :meth:`close` method was called. .. note:: The first call to this method must pass :obj:`None` as the sole argument. This activates the `coroutine`. The response from the first call is undefined. """
[docs] def throw(exception): # pragma: no branch """Send the exception to the `coroutine`. The `coroutine` is expected to raise the exception in it's context at the reception point. It may then catch it and handle it or not. :param exceptions.Exception exception: The exception to send to the `coroutine`. :returns: If the exception is caught, then any `message` :keyword:`yield`\ed by the `coroutine`. :raises exceptions.Exception: Any uncaught exception passed in via this method. """
[docs] def close(): # pragma: no branch """Terminate the `coroutine`. This method sends the :exc:`~exceptions.GeneratorExit` exception into the `coroutine`, which is expected to perform any necessary clean up and shutdown work and then terminate. .. note:: After this method is called, all subsequent calls to :meth:`send` **must** result in a :exc:`~exceptions.StopIteration` exception being raised. """
[docs]class INotLazy(interface.Interface): """Marker `interface` signalling that a `coroutine` is not `lazy`. A `coroutine` is called `lazy`, if it should only be executed when there are new incoming `messages` for it. It is *not* `lazy` if the `coroutine` should be executed even when there are no incoming `messages` (i.e. for polling, etc.) :class:`INotLazy` `coroutines` will receive :obj:`None` objects as messages on their ``control`` `inbox` when there are no other `messages` to for them. .. note:: By default, all objects providing :class:`ICoroutine` are presumptively `lazy`. They need to provide this `interface` to indicate otherwise. This is more efficient and more natural for `coroutines`. It also more accurately distinguishes them from regular `generators`. """ #--- Postoffices interfaces
[docs]class IPostOfficeFactory(interface.Interface): """`Interface` for `callables` that return :class:`IPostOffice` objects."""
[docs] def __call__(link_ignores_duplicates=False, # pragma: no branch unlink_ignores_missing=False): """Return the desired `post office`. :param bool link_ignores_duplicates: If :obj:`True`, the :meth:`IPostOffice.register` method becomes `idempotent` and will not raise an exception when adding an already added `link`. :param bool unlink_ignores_missing: If :obj:`True`, the :meth:`IPostOffice.unregister` method becomes `idempotent` and will not raise an exception when removing a `link` that was not already added. :returns: The desired `post office`. :rtype: :class:`IPostOffice` """
[docs]class IPostOffice(interface.Interface): """`Interface` that defines a `post office`. `Post offices` are objects that are responsible for delivering posted `messages` from `inboxes` to `outboxes`. They are also responsible for keeping track of the `links` between `mailboxes`. That said, `message` posting and retrieval (and subsequent delivery to `coroutines`) is someone else's responsibility. Usually it is the job of the `scheduler`. """
[docs] def register(first_link, *other_links): # pragma: no branch """Register `links` between `coroutines`. When a `source` `coroutine`'s `outbox` is linked to a `sink` `coroutine`'s `inbox`, any `messages` posted from the `source`'s `outbox` will automatically be delivered to the message queue for the `sink`'s `inbox`, where it can be later retrieved for delivery to the `sink` `coroutine`. :param tuple first_link: The first `link` to register. :param collections.Sequence other_links: Any other `links` to register. Each link should be a 4-:obj:`tuple` in the form of :samp:`({source}, {outbox}, {sink}, {inbox})`, where: :param m4us.core.interfaces.ICoroutine source: The `source` `coroutine` to link. :param unicode outbox: The `source`'s `outbox` to link. :param m4us.core.interfaces.ICoroutine sink: The `sink` `coroutine` to link. :param unicode inbox: The `sink`'s `inbox` to link. :raises m4us.core.exceptions.LinkExistsError: If a `source`'s `outbox` is already linked to a `sink`'s `inbox`, unless the `post office` was created with ``link_ignores_duplicates=True``. .. seealso:: The :class:`IPostOfficeFactory` `interface` for details on the ``link_ignores_duplicates`` parameter. """
[docs] def unregister(first_link, *other_links): # pragma: no branch """Unregister previously registered `links`. This is the opposite of the :meth:`unregister` method. :param tuple first_link: The first `link` to unregister. :param collections.Sequence other_links: Any other `links` to unregister. Each link should be a 4-:obj:`tuple` in the same format as defined in the :meth:`register` method documentation. :raises m4us.core.exceptions.NoLinkError: If a `link` was not previously registered, unless the `post office` was created with ``unlink_ignores_missing=True``. .. note:: Unregistering one `link` should not affect any other links involving the `source` `outbox` or the `sink` `inbox`. .. note:: Any outstanding messages in the `sink`'s `inbox` message queue should still be kept for delivery. The message queue should only be removed after all outstanding `messages` have been retrieved. .. seealso:: The :meth:`register` method for details on the format of a link. .. seealso:: The :class:`IPostOfficeFactory` `interface` for details on the ``unlink_ignores_missing`` parameter. """
[docs] def post(source, outbox, message): # pragma: no branch """Post a `message` from the `source` `outbox`. When a `message` is posted, it is automatically sent to the message queues of all linked `sink` `inboxes` for later retrieval and delivery. :param m4us.core.interfaces.ICoroutine source: The `source` `coroutine` that produced the `message`. :param unicode outbox: The `outbox` on which the `source` sent the `message`. :param message: The `message` that the `source` sent. :raises m4us.core.exceptions.NoLinkError: If the `source` `outbox` has not already been registered as a `source` in a `link`. """
[docs] def retrieve(sink): # pragma: no branch """Retrieve all outstanding `messages` for a `sink` `coroutine`. As `messages` are posted, they are accumulated in message queues based on the registered `links`. When this method is called, all the accumulated `messages` for the given `sink` `coroutine` are returned and the message queue for that `sink` is emptied. If there are no outstanding `messages` waiting in the message queue, then an empty iterable is returned. :param m4us.core.interfaces.ICoroutine sink: The `sink` `coroutine` for which to retrieve `messages`. :returns: All outstanding `messages` in the form of :samp:`({inbox}, {message})`. :rtype: :class:`collections.Iterable` :raises m4us.core.exceptions.NotASinkError: If the given `coroutine` is not registered as a `sink` in any of the registered `links`. .. note:: It is the responsibility of the caller to make sure all the retrieved `messages` are delivered to the `sink` `coroutine`. """ #--- Schedulers interfaces
[docs]class ISchedulerFactory(interface.Interface): """`Interface` for `callables` that return :class:`IScheduler` objects."""
[docs] def __call__(post_office, # pragma: no branch add_ignores_duplicates=False, remove_ignores_missing=False): """Return the desired `scheduler`. :param m4us.core.interfaces.IPostOffice post_office: The `post office` through which to post and retrieve `messages`. :param bool add_ignores_duplicates: If :obj:`True`, the :meth:`IScheduler.register` method becomes `idempotent` and will not raise an exception when adding an already added `coroutine`. :param bool remove_ignores_missing: If :obj:`True`, the :meth:`IScheduler.unregister` method becomes `idempotent` and will not raise an exception when removing a `coroutine` that was not already added. :returns: The desired `scheduler`. :rtype: :class:`IScheduler` :raises exceptions.TypeError: If the given `post office` does not provide the :class:`IPostOffice` `interface`. """
[docs]class IScheduler(interface.Interface): """`Interface` that defines a `scheduler`. `Scheduler` objects are responsible for being the main program loop. Their job is to run `coroutines`. They are also responsible for retrieving and delivering `messages` from a `post office` to the `coroutines`, as well as posting back to the `post office` any `messages` emitted by the `coroutines`. """
[docs] def register(first_coroutine, *other_coroutines): # pragma: no branch """Register one or more `coroutines` with the `scheduler`. In order for a `scheduler` to run a `coroutine`, it must first be added to (i.e. registered with) the `scheduler`. :param m4us.core.interfaces.ICoroutine first_coroutine: The first `coroutine` to add. :param collections.Sequence other_coroutines: Any other `coroutines` to add. :raises exceptions.TypeError: If any given argument does not provide or cannot be adapted to :class:`ICoroutine`. :raises m4us.core.exceptions.DuplicateError: If any given `coroutine` has already been added, unless the `scheduler` was created with ``add_ignores_duplicates=True``. .. note:: `Schedulers` are not *required* to preserve or guarantee any particular order in which the `coroutines` will be run. They may, however, *choose* to do so, if desired. .. seealso:: The :class:`ISchedulerFactory` `interface` for details on the ``add_ignores_duplicates`` parameter. """
[docs] def unregister(first_coroutine, *other_coroutines): # pragma: no branch """Unregister one or more `coroutines` from the `scheduler`. This is the opposite of the :meth:`register` method. :param m4us.core.interfaces.ICoroutine first_coroutine: The first `coroutine` to remove. :param collections.Sequence other_coroutines: Any other `coroutines` to remove. :raises m4us.core.exceptions.NotAddedError: If any given `coroutine` is not registered with the `scheduler`, unless the `scheduler` was created with ``remove_ignores_missing=True``. .. note:: This method should also call each `coroutine`'s :meth:`!close` method. .. seealso:: The :class:`ISchedulerFactory` `interface` for details on the ``remove_ignores_missing`` parameter. """
[docs] def step(): # pragma: no branch """Run one `coroutine`. This is the smallest unit of execution. A single registered `coroutine` is run once for each `message` currently accumulated in its `post office` message queue. Any resulting `messages` are also posted back to the `post office`. This means that one call to this method can trigger repeated calls to the current `coroutine`, but only one `coroutine` should ever be executed. If a `coroutine` is `lazy`, it should only be executed when there are `messages` waiting to be delivered to it (or it is in the "shutting down" state, see below). If it is *not* `lazy` (i.e. it provides the :class:`INotLazy` marker `interface`) and there are no `messages` waiting (as is the case with `producers`, for example), then a :obj:`None` object should be sent as the `message` to the `coroutine` on its ``control`` `inbox`. If a `coroutine` :keyword:`yield`\s only a :obj:`None` object (without even an `outbox`), the :obj:`None` should just be discarded. This lets the `coroutine` indicate it has no `message` to send. If a `coroutine` :keyword:`yield`\s an :class:`IShutdown` `message`, that `message` should be posted to the `post office`, like a normal `message`, to allow the the `message` to cascade to other `coroutines`. Additionally, if the `coroutine` :keyword:`yield`\s an :class:`IShutdown` `message` on it's ``signal`` `outbox`, it should then be considered in a "shutting down" state. Finally, if the :keyword:`yield`\ed :class:`IShutdown` cannot be posted to the `post office` (i.e. the `post office` raises an :class:`~m4us.core.exceptions.NoLinkError`), the the message should just be discarded. This is so that `consumer`/`sink` `coroutines` can also emit :class:`IShutdown` messages to signal that they are shutting down. When in the "shutting down" state, no futher `post office` `messages` should be sent to the `coroutine`. Instead, an :class:`IShutdown` `message` should be sent to it's ``control`` `inbox` every time this method is called, until the `coroutine` exits (i.e. raises :exc:`~exceptions.StopIteration`). This allows `coroutines` to emit additional `messages` before shutting down and ensures that `lazy` `coroutines` always get enough `messages` to be able to shutdown properly. Finally, any `coroutine` that exits (i.e. raises :exc:`~exceptions.StopIteration`) should be removed from any run queues and have its :meth:`~m4us.core.interfaces.ICoroutine.close` method called. After that no `messages` should ever be sent to it again. :raises m4us.core.exceptions.NeverRunError: If a `coroutine` is `lazy` but the `post office` says it is not configured to receive any `messages` (i.e. raises :exc:`~m4us.core.exceptions.NotASinkError`). :raises m4us.core.exceptions.NoLinkError: If the `post office` raises the exception and the message is not an :class:`IShutdown` `message`. :class:`IShutdown` `messages` are expected to be cascaded (even from `consumers`), so they do not cause the exception to be re-raised. .. note:: At least one `coroutine` is expected to be added before calling this method. It is an error to do otherwise. .. seealso:: :class:`INotLazy` for details about the non-`lazy` `coroutines`. .. seealso:: :meth:`IPostOffice.post` for details on the :class:`~m4us.core.exceptions.NoLinkError` exception. """
[docs] def cycle(): # pragma: no branch """Cycle once through the main loop, running all eligible `coroutines`. `Schedulers` represent the main execution loop. This method triggers a single cycle through that execution loop, calling the :meth:`step` method as many times as is appropriate. Ideally all registered `coroutines` should have a chance to run. .. seealso:: The :meth:`step` method for details on how `coroutines` are run and on the exceptions that this method can raise as a result. """
[docs] def run(cycles=None): # pragma: no branch """Start the `scheduler`, running all registered `coroutines`. This is the main execution loop. :param cycles: The number of cycles of the main loop to run through. One cycle is defined as a one call to the :meth:`cycle` method. If :obj:`None` or not specified, then this method should run until all registered `coroutines` have shutdown (i.e. raised :exc:`~exceptions.StopIteration`). :type cycles: :obj:`int` or :obj:`None` .. note:: If *cycles* is specified, it is expected that subsequent calls to this method should continue from where the last call left off. .. seealso:: The :meth:`cycle` method for details on what a single cycle entails and what exceptions may be raised as a result. """ #--- Containers interfaces
[docs]class IContainerFactory(interface.Interface): """`Interface` for `callables` that return :class:`IContainer` objects."""
[docs] def __call__(*args, **kwargs): # pragma: no branch """Return the desired container. :param collections.Sequence args: Any positional arguments that the `factory` may accept. :param collections.Mapping kwargs: Any keyword arguments that the `factory` may accept. :returns: The desired container. :rtype: :class:`IContainer` :raises m4us.core.exceptions.InvalidLinkError: If any of the given or calculated `links` are invalid. """
[docs]class IContainer(interface.Interface): """`Interface` that defines a container. Containers are responsible for containing :class:`ICoroutine` objects and the `links` that connect them both to each other and to the container itself, if appropriate. All containers should calculate and provide :attr:`IContainer.coroutines` and :attr:`IContainer.links` attributes before they are used. The contents of :attr:`IContainer.coroutines` should be able to be passed to the appropriate :meth:`IScheduler.register` method and the contents of :attr:`IContainer.links` should be able to be passed to the appropriate :meth:`IPostOffice.register` method. Additionally, containers can contain other :class:`IContainer` objects, in other words other containers. In such a case, :attr:`IContainer.coroutines` and :attr:`IContainer.links` should include the `coroutines` and `links` from the sub-containers, excluding any duplicates from the other `coroutines` and `links` in the parent container. .. note:: Containers will usually provide :class:`ICoroutine` as well, but that is not strictly required by this interface. The calculation and use of the above-mentioned two attributes makes it not a strict requirement. """ coroutines = interface.Attribute( """All the contained `coroutines` to be added to the `scheduler`. The `coroutines` of any sub-containers should also be included for convenience. The contained `coroutines` can be added to the `scheduler` with code similar to :samp:`{scheduler}.add(*{container}.coroutines)`. :type: :class:`collections.Sequence` .. seealso:: :meth:`IScheduler.register` for details about adding `coroutines` to `schedulers`. """) links = interface.Attribute( """All the `post office` `links` to be added to the `post office`. The `links` of any sub-containers should also be included for convenience. The `links` between the contained `coroutines` can be added to the `post office` with code similar to :samp:`{post_office}.register(*{container}.links)`. :type: :class:`collections.Sequence` .. seealso:: :meth:`IPostOffice.register` for details about adding `links` to `post offices`. """) #---Module initialization------------------------------------------------------ #---Late Imports--------------------------------------------------------------- #---Late Globals--------------------------------------------------------------- #---Late Functions------------------------------------------------------------- #---Late Classes--------------------------------------------------------------- #---Late Module initialization-------------------------------------------------