mopidy/mopidy/ext.py
Thomas Adamcik 5550785146 ext: Wrap extension state in a ExtensionData tuple
This allows us to do more of the data loading that might fail safely in the
mopidy.ext module instead of having things spread all over the place.

Note that only minimal changes have been made to __main__ to make things work.
Further refactoring should follow.
2015-05-11 00:33:41 +02:00

234 lines
7.0 KiB
Python

from __future__ import absolute_import, unicode_literals
import collections
import logging
import pkg_resources
from mopidy import config as config_lib, exceptions
logger = logging.getLogger(__name__)
_extension_data_fields = ['extension', 'entry_point', 'config_schema',
'config_defaults', 'command']
ExtensionData = collections.namedtuple('ExtensionData', _extension_data_fields)
class Extension(object):
"""Base class for Mopidy extensions"""
dist_name = None
"""The extension's distribution name, as registered on PyPI
Example: ``Mopidy-Soundspot``
"""
ext_name = None
"""The extension's short name, as used in setup.py and as config section
name
Example: ``soundspot``
"""
version = None
"""The extension's version
Should match the :attr:`__version__` attribute on the extension's main
Python module and the version registered on PyPI.
"""
def get_default_config(self):
"""The extension's default config as a bytestring
:returns: bytes or unicode
"""
raise NotImplementedError(
'Add at least a config section with "enabled = true"')
def get_config_schema(self):
"""The extension's config validation schema
:returns: :class:`~mopidy.config.schema.ExtensionConfigSchema`
"""
schema = config_lib.ConfigSchema(self.ext_name)
schema['enabled'] = config_lib.Boolean()
return schema
def get_command(self):
"""Command to expose to command line users running ``mopidy``.
:returns:
Instance of a :class:`~mopidy.commands.Command` class.
"""
pass
def validate_environment(self):
"""Checks if the extension can run in the current environment.
Dependencies described by :file:`setup.py` are checked by Mopidy, so
you should not check their presence here.
If a problem is found, raise :exc:`~mopidy.exceptions.ExtensionError`
with a message explaining the issue.
:raises: :exc:`~mopidy.exceptions.ExtensionError`
:returns: :class:`None`
"""
pass
def setup(self, registry):
"""
Register the extension's components in the extension :class:`Registry`.
For example, to register a backend::
def setup(self, registry):
from .backend import SoundspotBackend
registry.add('backend', SoundspotBackend)
See :class:`Registry` for a list of registry keys with a special
meaning. Mopidy will instantiate and start any classes registered under
the ``frontend`` and ``backend`` registry keys.
This method can also be used for other setup tasks not involving the
extension registry.
:param registry: the extension registry
:type registry: :class:`Registry`
"""
raise NotImplementedError
class Registry(collections.Mapping):
"""Registry of components provided by Mopidy extensions.
Passed to the :meth:`~Extension.setup` method of all extensions. The
registry can be used like a dict of string keys and lists.
Some keys have a special meaning, including, but not limited to:
- ``backend`` is used for Mopidy backend classes.
- ``frontend`` is used for Mopidy frontend classes.
- ``local:library`` is used for Mopidy-Local libraries.
Extensions can use the registry for allow other to extend the extension
itself. For example the ``Mopidy-Local`` use the ``local:library`` key to
allow other extensions to register library providers for ``Mopidy-Local``
to use. Extensions should namespace custom keys with the extension's
:attr:`~Extension.ext_name`, e.g. ``local:foo`` or ``http:bar``.
"""
def __init__(self):
self._registry = {}
def add(self, name, cls):
"""Add a component to the registry.
Multiple classes can be registered to the same name.
"""
self._registry.setdefault(name, []).append(cls)
def __getitem__(self, name):
return self._registry.setdefault(name, [])
def __iter__(self):
return iter(self._registry)
def __len__(self):
return len(self._registry)
def load_extensions():
"""Find all installed extensions.
:returns: list of installed extensions
"""
installed_extensions = []
for entry_point in pkg_resources.iter_entry_points('mopidy.ext'):
logger.debug('Loading entry point: %s', entry_point)
extension_class = entry_point.load(require=False)
# TODO: start using _extension_error_handling(...) pattern
try:
if not issubclass(extension_class, Extension):
continue # TODO: log this
except TypeError:
continue # TODO: log that extension_class is not a class
try:
extension = extension_class()
except Exception:
continue # TODO: log this
# TODO: handle exceptions and validate result...
config_schema = extension.get_config_schema()
default_config = extension.get_default_config()
command = extension.get_command()
installed_extensions.append(ExtensionData(
extension, entry_point, config_schema, default_config, command))
# TODO: call validate_extension here?
# TODO: do basic config tests like schema contains enabled?
logger.debug(
'Loaded extension: %s %s', extension.dist_name, extension.version)
names = (ed.extension.ext_name for ed in installed_extensions)
logger.debug('Discovered extensions: %s', ', '.join(names))
return installed_extensions
def validate_extension(extension, entry_point):
"""Verify extension's dependencies and environment.
:param extensions: an extension to check
:returns: if extension should be run
"""
logger.debug('Validating extension: %s', extension.ext_name)
if extension.ext_name != entry_point.name:
logger.warning(
'Disabled extension %(ep)s: entry point name (%(ep)s) '
'does not match extension name (%(ext)s)',
{'ep': entry_point.name, 'ext': extension.ext_name})
return False
try:
entry_point.require()
except pkg_resources.DistributionNotFound as ex:
logger.info(
'Disabled extension %s: Dependency %s not found',
extension.ext_name, ex)
return False
except pkg_resources.VersionConflict as ex:
if len(ex.args) == 2:
found, required = ex.args
logger.info(
'Disabled extension %s: %s required, but found %s at %s',
extension.ext_name, required, found, found.location)
else:
logger.info(
'Disabled extension %s: %s', extension.ext_name, ex)
return False
try:
extension.validate_environment()
except exceptions.ExtensionError as ex:
logger.info(
'Disabled extension %s: %s', extension.ext_name, ex.message)
return False
except Exception:
return False # TODO: log
return True