from __future__ import absolute_import, unicode_literals
import collections
import logging
import os
import pkg_resources
from mopidy import config as config_lib, exceptions
from mopidy.internal import path
logger = logging.getLogger(__name__)
_extension_data_fields = ['extension', 'entry_point', 'config_schema',
'config_defaults', 'command']
ExtensionData = collections.namedtuple('ExtensionData', _extension_data_fields)
[docs]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.
"""
[docs] 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"')
[docs] def get_config_schema(self):
"""The extension's config validation schema
:returns: :class:`~mopidy.config.schemas.ConfigSchema`
"""
schema = config_lib.ConfigSchema(self.ext_name)
schema['enabled'] = config_lib.Boolean()
return schema
[docs] @classmethod
def get_cache_dir(cls, config):
"""Get or create cache directory for the extension.
Use this directory to cache data that can safely be thrown away.
:param config: the Mopidy config object
:return: string
"""
assert cls.ext_name is not None
cache_dir_path = bytes(os.path.join(config['core']['cache_dir'],
cls.ext_name))
path.get_or_create_dir(cache_dir_path)
return cache_dir_path
[docs] @classmethod
def get_config_dir(cls, config):
"""Get or create configuration directory for the extension.
:param config: the Mopidy config object
:return: string
"""
assert cls.ext_name is not None
config_dir_path = bytes(os.path.join(config['core']['config_dir'],
cls.ext_name))
path.get_or_create_dir(config_dir_path)
return config_dir_path
[docs] @classmethod
def get_data_dir(cls, config):
"""Get or create data directory for the extension.
Use this directory to store data that should be persistent.
:param config: the Mopidy config object
:returns: string
"""
assert cls.ext_name is not None
data_dir_path = bytes(os.path.join(config['core']['data_dir'],
cls.ext_name))
path.get_or_create_dir(data_dir_path)
return data_dir_path
[docs] def get_command(self):
"""Command to expose to command line users running ``mopidy``.
:returns:
Instance of a :class:`~mopidy.commands.Command` class.
"""
pass
[docs] 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
[docs] 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
[docs]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 = {}
[docs] 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)
[docs]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)
try:
extension_class = entry_point.load(require=False)
except Exception as e:
logger.exception("Failed to load extension %s: %s" % (
entry_point.name, e))
continue
try:
if not issubclass(extension_class, Extension):
raise TypeError # issubclass raises TypeError on non-class
except TypeError:
logger.error('Entry point %s did not contain a valid extension'
'class: %r', entry_point.name, extension_class)
continue
try:
extension = extension_class()
config_schema = extension.get_config_schema()
default_config = extension.get_default_config()
command = extension.get_command()
except Exception:
logger.exception('Setup of extension from entry point %s failed, '
'ignoring extension.', entry_point.name)
continue
installed_extensions.append(ExtensionData(
extension, entry_point, config_schema, default_config, command))
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
[docs]def validate_extension_data(data):
"""Verify extension's dependencies and environment.
:param extensions: an extension to check
:returns: if extension should be run
"""
logger.debug('Validating extension: %s', data.extension.ext_name)
if data.extension.ext_name != data.entry_point.name:
logger.warning(
'Disabled extension %(ep)s: entry point name (%(ep)s) '
'does not match extension name (%(ext)s)',
{'ep': data.entry_point.name, 'ext': data.extension.ext_name})
return False
try:
data.entry_point.require()
except pkg_resources.DistributionNotFound as ex:
logger.info(
'Disabled extension %s: Dependency %s not found',
data.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',
data.extension.ext_name, required, found, found.location)
else:
logger.info(
'Disabled extension %s: %s', data.extension.ext_name, ex)
return False
try:
data.extension.validate_environment()
except exceptions.ExtensionError as ex:
logger.info(
'Disabled extension %s: %s', data.extension.ext_name, ex.message)
return False
except Exception:
logger.exception('Validating extension %s failed with an exception.',
data.extension.ext_name)
return False
if not data.config_schema:
logger.error('Extension %s does not have a config schema, disabling.',
data.extension.ext_name)
return False
elif not isinstance(data.config_schema.get('enabled'), config_lib.Boolean):
logger.error('Extension %s does not have the required "enabled" config'
' option, disabling.', data.extension.ext_name)
return False
for key, value in data.config_schema.items():
if not isinstance(value, config_lib.ConfigValue):
logger.error('Extension %s config schema contains an invalid value'
' for the option "%s", disabling.',
data.extension.ext_name, key)
return False
if not data.config_defaults:
logger.error('Extension %s does not have a default config, disabling.',
data.extension.ext_name)
return False
return True