mopidy.ext — Mopidy 1.0.8 documentation

# Source code for mopidy.ext

    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__)

    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. For example, to register custom GStreamer
            elements::

                def setup(self, registry):
                    from .mixer import SoundspotMixer
                    gobject.type_register(SoundspotMixer)
                    gst.element_register(
                        SoundspotMixer, 'soundspotmixer', gst.RANK_MARGINAL)

            :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)
            extension = extension_class()
            extension.entry_point = entry_point
            installed_extensions.append(extension)
            logger.debug(
                'Loaded extension: %s %s', extension.dist_name, extension.version)

        names = (e.ext_name for e in installed_extensions)
        logger.debug('Discovered extensions: %s', ', '.join(names))
        return installed_extensions

    def validate_extension(extension):
        """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 != extension.entry_point.name:
            logger.warning(
                'Disabled extension %(ep)s: entry point name (%(ep)s) '
                'does not match extension name (%(ext)s)',
                {'ep': extension.entry_point.name, 'ext': extension.ext_name})
            return False

        try:
            extension.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

        return True