Source code for aioxmpp.service
"""
:mod:`~aioxmpp.service` --- Utilities for implementing :class:`~aioxmpp.node.AbstractClient` services
#####################################################################################################
Protocol extensions or in general support for parts of the XMPP protocol are
implemented using :class:`Service` classes, or rather, classes which use the
:class:`Meta` metaclass.
Both of these are provided in this module.
.. autoclass:: Service
.. autoclass:: Meta([inherit_dependencies=True])
"""
import abc
import asyncio
import logging
import warnings
[docs]class Meta(abc.ABCMeta):
"""
The metaclass for services. The :class:`Service` class uses it and in
general you should just inherit from :class:`Service` and define the
dependency attributes as needed.
Services have dependencies. A :class:`Meta` instance (i.e. a service class)
can declare dependencies using the following two attributes.
.. attribute:: ORDER_BEFORE
An iterable of :class:`Service` classes before which the class which is
currently being declared needs to be instanciated.
Thus, any service which occurs in :attr:`ORDER_BEFORE` will be
instanciated *after* this class (if at all).
.. versionadded:: 0.3
.. attribute:: SERVICE_BEFORE
Before 0.3, this was the name of the :attr:`ORDER_BEFORE` attribute. It
is still supported, but use emits a :data:`DeprecationWarning`. It must
not be mixed with :attr:`ORDER_BEFORE` or :attr:`ORDER_AFTER` on a class
declaration, or the declaration will raise :class:`ValueError`.
.. deprecated:: 0.3
Support for this attribute will be removed in 1.0; starting with 1.0,
using this attribute will raise a :class:`TypeError` on class
declaration and a :class:`AttributeError` when accessing it on a
class or instance.
.. attribute:: ORDER_AFTER
An iterable of :class:`Service` classes which would be instanciated
after the class which is currently being declared, if at all.
Classes which are declared in this attribute are not forced to be
instanciated (unlike with :attr:`ORDER_BEFORE`). However, if any of
these classes is requested, it is made sure that *this* class is
instanciated before.
.. versionadded:: 0.3
.. attribute:: SERVICE_AFTER
Before 0.3, this was the name of the :attr:`ORDER_AFTER` attribute. It
is still supported, but use emits a :data:`DeprecationWarning`. It must
not be mixed with :attr:`ORDER_BEFORE` or :attr:`ORDER_AFTER` on a class
declaration, or the declaration will raise :class:`ValueError`.
.. deprecated:: 0.3
See :attr:`SERVICE_BEFORE` for details on the deprecation cycle.
The dependencies are inherited from bases unless the `inherit_dependencies`
keyword argument is set to false.
After a class has been instanciated, the full set of dependencies is
provided in the attributes, including all transitive relationships. These
attributes are updated when new classes are declared.
Dependency relationships must not have cycles; a cycle results in a
:class:`ValueError` when the class causing the cycle is declared.
Example::
class Foo(metaclass=service.Meta):
pass
class Bar(metaclass=service.Meta):
ORDER_BEFORE = [Foo]
class Baz(metaclass=service.Meta):
ORDER_BEFORE = [Bar]
class Fourth(metaclass=service.Meta):
ORDER_BEFORE = [Bar]
``Baz`` and ``Fourth`` will be instanciated before ``Bar`` and ``Bar`` will
be instanciated before ``Foo``. There is no dependency relationship between
``Baz`` and ``Fourth``.
Inheritance works too::
class Foo(metaclass=service.Meta):
pass
class Bar(metaclass=service.Meta):
ORDER_BEFORE = [Foo]
class Baz(Bar):
# has ORDER_BEFORE == {Foo}
pass
class Fourth(Bar, inherit_dependencies=False):
# has empty ORDER_BEFORE
pass
"""
@classmethod
def transitive_collect(mcls, classes, attr, seen):
for cls in classes:
yield cls
yield from mcls.transitive_collect(getattr(cls, attr), attr, seen)
@classmethod
def collect_and_inherit(mcls, bases, namespace, attr,
inherit_dependencies):
classes = set(namespace.get(attr, []))
if inherit_dependencies:
for base in bases:
if isinstance(base, mcls):
classes.update(getattr(base, attr))
classes.update(
mcls.transitive_collect(
list(classes),
attr,
set())
)
return classes
def __new__(mcls, name, bases, namespace, inherit_dependencies=True):
if "SERVICE_BEFORE" in namespace or "SERVICE_AFTER" in namespace:
if "ORDER_BEFORE" in namespace or "ORDER_AFTER" in namespace:
raise ValueError("declaration mixes old and new ordering "
"attribute names (SERVICE_* vs. ORDER_*)")
warnings.warn(
"SERVICE_BEFORE/AFTER used on class; use ORDER_BEFORE/AFTER",
DeprecationWarning)
try:
namespace["ORDER_BEFORE"] = namespace.pop("SERVICE_BEFORE")
except KeyError:
pass
try:
namespace["ORDER_AFTER"] = namespace.pop("SERVICE_AFTER")
except KeyError:
pass
before_classes = mcls.collect_and_inherit(
bases,
namespace,
"ORDER_BEFORE",
inherit_dependencies)
after_classes = mcls.collect_and_inherit(
bases,
namespace,
"ORDER_AFTER",
inherit_dependencies)
if before_classes & after_classes:
raise ValueError("dependency loop: {} loops through {}".format(
name,
next(iter(before_classes & after_classes)).__qualname__
))
namespace["ORDER_BEFORE"] = set(before_classes)
namespace["ORDER_AFTER"] = set(after_classes)
return super().__new__(mcls, name, bases, namespace)
def __init__(self, name, bases, namespace, inherit_dependencies=True):
super().__init__(name, bases, namespace)
for cls in self.ORDER_BEFORE:
cls.ORDER_AFTER.add(self)
for cls in self.ORDER_AFTER:
cls.ORDER_BEFORE.add(self)
self.SERVICE_BEFORE = self.ORDER_BEFORE
self.SERVICE_AFTER = self.ORDER_AFTER
def __lt__(self, other):
return other in self.ORDER_BEFORE
def __le__(self, other):
return self < other
[docs]class Service(metaclass=Meta):
"""
A :class:`Service` is used to implement XMPP or XEP protocol parts, on top
of the more or less fixed stanza handling implemented in
:mod:`aioxmpp.node` and :mod:`aioxmpp.stream`.
:class:`Service` is a base class which can be used by extension developers
to implement support for custom or standardized protocol extensions. Some
of the features for which :mod:`aioxmpp` has support are also implemented
using :class:`Service` subclasses.
`client` must be a :class:`~aioxmpp.node.AbstractClient` to which the
service will be attached. The `client` cannot be changed later, for the
sake of simplicity.
`logger_base` may be a :class:`logging.Logger` instance or :data:`None`. If
it is :data:`None`, a logger is automatically created, by taking the fully
qualified name of the :class:`Service` subclass which is being
instanciated. Otherwise, the logger is passed to :meth:`derive_logger` and
the result is used as value for the :attr:`logger` attribute.
To implement your own service, derive from :class:`Service`. If your
service depends on other services (such as :mod:`aioxmpp.pubsub` or
:mod:`aioxmpp.disco`), these dependencies *must* be declared as documented
in the service meta class :class:`Meta`.
To stay forward compatible, accept arbitrary keyword arguments and pass
them down to :class:`Service`. As it is not possible to directly pass
arguments to :class:`Service`\ s on construction (due to the way
:meth:`aioxmpp.node.AbstractClient.summon` works), there is no need for you
to introduce custom arguments, and thus there should be no conflicts.
.. autoattribute:: client
.. automethod:: derive_logger
.. automethod:: shutdown
"""
def __init__(self, client, *, logger_base=None):
super().__init__()
self._client = client
if logger_base is None:
self.logger = logging.getLogger(".".join([
type(self).__module__, type(self).__qualname__
]))
else:
self.logger = self.derive_logger(logger_base)
[docs] def derive_logger(self, logger):
"""
Return a child of `logger` specific for this instance. This is called
after :attr:`_client` has been set, from the constructor.
The child name is calculated by the default implementation in a way
specific for aioxmpp services; it is not meant to be used by
non-:mod:`aioxmpp` classes; do not rely on the way how the child name
is calculated.
"""
parts = type(self).__module__.split(".")[1:]
if parts[-1] == "service" and len(parts) > 1:
del parts[-1]
return logger.getChild(".".join(
parts+[type(self).__qualname__]
))
@property
[docs] def client(self):
"""
The client to which the :class:`Service` is bound. This attribute is
read-only.
If the service has been shut down using :meth:`shutdown`, this reads as
:data:`None`.
"""
return self._client
@asyncio.coroutine
def _shutdown(self):
"""
Actual implementation of the shut down process.
This *must* be called using super from inheriting classes after their
own shutdown procedure. Inheriting classes *must* override this method
instead of :meth:`shutdown`.
"""
@asyncio.coroutine
[docs] def shutdown(self):
"""
Close the service and wait for it to completely shut down.
Some services which are still running may depend on this service. In
that case, the service may refuse to shut down instead of shutting
down, by raising a :class:`RuntimeError` exception.
.. note::
Developers creating subclasses of :class:`Service` to implement
services should not override this method. Instead, they should
override the :meth:`_shutdown` method.
"""
yield from self._shutdown()
self._client = None