1 3
import collections
2 3
import logging
3 3
from collections.abc import Mapping
4

5 3
import pkg_resources
6

7 3
from mopidy import config as config_lib
8 3
from mopidy import exceptions
9 3
from mopidy.internal import path
10

11 3
logger = logging.getLogger(__name__)
12

13

14 3
_extension_data_fields = [
15
    "extension",
16
    "entry_point",
17
    "config_schema",
18
    "config_defaults",
19
    "command",
20
]
21

22 3
ExtensionData = collections.namedtuple("ExtensionData", _extension_data_fields)
23

24

25 3
class Extension:
26

27
    """Base class for Mopidy extensions"""
28

29 3
    dist_name = None
30
    """The extension's distribution name, as registered on PyPI
31

32
    Example: ``Mopidy-Soundspot``
33
    """
34

35 3
    ext_name = None
36
    """The extension's short name, as used in setup.py and as config section
37
    name
38

39
    Example: ``soundspot``
40
    """
41

42 3
    version = None
43
    """The extension's version
44

45
    Should match the :attr:`__version__` attribute on the extension's main
46
    Python module and the version registered on PyPI.
47
    """
48

49 3
    def get_default_config(self):
50
        """The extension's default config as a bytestring
51

52
        :returns: bytes or unicode
53
        """
54 3
        raise NotImplementedError(
55
            'Add at least a config section with "enabled = true"'
56
        )
57

58 3
    def get_config_schema(self):
59
        """The extension's config validation schema
60

61
        :returns: :class:`~mopidy.config.schemas.ConfigSchema`
62
        """
63 3
        schema = config_lib.ConfigSchema(self.ext_name)
64 3
        schema["enabled"] = config_lib.Boolean()
65 3
        return schema
66

67 3
    @classmethod
68 2
    def get_cache_dir(cls, config):
69
        """Get or create cache directory for the extension.
70

71
        Use this directory to cache data that can safely be thrown away.
72

73
        :param config: the Mopidy config object
74
        :return: pathlib.Path
75
        """
76 3
        if cls.ext_name is None:
77 3
            raise AssertionError
78 3
        cache_dir_path = (
79
            path.expand_path(config["core"]["cache_dir"]) / cls.ext_name
80
        )
81 3
        path.get_or_create_dir(cache_dir_path)
82 3
        return cache_dir_path
83

84 3
    @classmethod
85 2
    def get_config_dir(cls, config):
86
        """Get or create configuration directory for the extension.
87

88
        :param config: the Mopidy config object
89
        :return: pathlib.Path
90
        """
91 3
        if cls.ext_name is None:
92 3
            raise AssertionError
93 3
        config_dir_path = (
94
            path.expand_path(config["core"]["config_dir"]) / cls.ext_name
95
        )
96 3
        path.get_or_create_dir(config_dir_path)
97 3
        return config_dir_path
98

99 3
    @classmethod
100 2
    def get_data_dir(cls, config):
101
        """Get or create data directory for the extension.
102

103
        Use this directory to store data that should be persistent.
104

105
        :param config: the Mopidy config object
106
        :returns: pathlib.Path
107
        """
108 3
        if cls.ext_name is None:
109 3
            raise AssertionError
110 3
        data_dir_path = (
111
            path.expand_path(config["core"]["data_dir"]) / cls.ext_name
112
        )
113 3
        path.get_or_create_dir(data_dir_path)
114 3
        return data_dir_path
115

116 3
    def get_command(self):
117
        """Command to expose to command line users running ``mopidy``.
118

119
        :returns:
120
          Instance of a :class:`~mopidy.commands.Command` class.
121
        """
122 3
        pass
123

124 3
    def validate_environment(self):
125
        """Checks if the extension can run in the current environment.
126

127
        Dependencies described by :file:`setup.py` are checked by Mopidy, so
128
        you should not check their presence here.
129

130
        If a problem is found, raise :exc:`~mopidy.exceptions.ExtensionError`
131
        with a message explaining the issue.
132

133
        :raises: :exc:`~mopidy.exceptions.ExtensionError`
134
        :returns: :class:`None`
135
        """
136 3
        pass
137

138 3
    def setup(self, registry):
139
        """
140
        Register the extension's components in the extension :class:`Registry`.
141

142
        For example, to register a backend::
143

144
            def setup(self, registry):
145
                from .backend import SoundspotBackend
146
                registry.add('backend', SoundspotBackend)
147

148
        See :class:`Registry` for a list of registry keys with a special
149
        meaning. Mopidy will instantiate and start any classes registered under
150
        the ``frontend`` and ``backend`` registry keys.
151

152
        This method can also be used for other setup tasks not involving the
153
        extension registry.
154

155
        :param registry: the extension registry
156
        :type registry: :class:`Registry`
157
        """
158 3
        raise NotImplementedError
159

160

161 3
class Registry(Mapping):
162

163
    """Registry of components provided by Mopidy extensions.
164

165
    Passed to the :meth:`~Extension.setup` method of all extensions. The
166
    registry can be used like a dict of string keys and lists.
167

168
    Some keys have a special meaning, including, but not limited to:
169

170
    - ``backend`` is used for Mopidy backend classes.
171
    - ``frontend`` is used for Mopidy frontend classes.
172

173
    Extensions can use the registry for allow other to extend the extension
174
    itself. For example the ``Mopidy-Local`` historically used the
175
    ``local:library`` key to allow other extensions to register library
176
    providers for ``Mopidy-Local`` to use. Extensions should namespace
177
    custom keys with the extension's :attr:`~Extension.ext_name`,
178
    e.g. ``local:foo`` or ``http:bar``.
179
    """
180

181 3
    def __init__(self):
182 0
        self._registry = {}
183

184 3
    def add(self, name, cls):
185
        """Add a component to the registry.
186

187
        Multiple classes can be registered to the same name.
188
        """
189 0
        self._registry.setdefault(name, []).append(cls)
190

191 3
    def __getitem__(self, name):
192 0
        return self._registry.setdefault(name, [])
193

194 3
    def __iter__(self):
195 0
        return iter(self._registry)
196

197 3
    def __len__(self):
198 0
        return len(self._registry)
199

200

201 3
def load_extensions():
202
    """Find all installed extensions.
203

204
    :returns: list of installed extensions
205
    """
206

207 3
    installed_extensions = []
208

209 3
    for entry_point in pkg_resources.iter_entry_points("mopidy.ext"):
210 3
        logger.debug("Loading entry point: %s", entry_point)
211 3
        try:
212 3
            extension_class = entry_point.resolve()
213 0
        except Exception as e:
214 0
            logger.exception(
215
                f"Failed to load extension {entry_point.name}: {e}"
216
            )
217 0
            continue
218

219 3
        try:
220 3
            if not issubclass(extension_class, Extension):
221 3
                raise TypeError  # issubclass raises TypeError on non-class
222 3
        except TypeError:
223 3
            logger.error(
224
                "Entry point %s did not contain a valid extension" "class: %r",
225
                entry_point.name,
226
                extension_class,
227
            )
228 3
            continue
229

230 3
        try:
231 3
            extension = extension_class()
232 3
            config_schema = extension.get_config_schema()
233 3
            default_config = extension.get_default_config()
234 3
            command = extension.get_command()
235 3
        except Exception:
236 3
            logger.exception(
237
                "Setup of extension from entry point %s failed, "
238
                "ignoring extension.",
239
                entry_point.name,
240
            )
241 3
            continue
242

243 3
        installed_extensions.append(
244
            ExtensionData(
245
                extension, entry_point, config_schema, default_config, command
246
            )
247
        )
248

249 3
        logger.debug(
250
            "Loaded extension: %s %s", extension.dist_name, extension.version
251
        )
252

253 3
    names = (ed.extension.ext_name for ed in installed_extensions)
254 3
    logger.debug("Discovered extensions: %s", ", ".join(names))
255 3
    return installed_extensions
256

257

258 3
def validate_extension_data(data):
259
    """Verify extension's dependencies and environment.
260

261
    :param extensions: an extension to check
262
    :returns: if extension should be run
263
    """
264

265 3
    logger.debug("Validating extension: %s", data.extension.ext_name)
266

267 3
    if data.extension.ext_name != data.entry_point.name:
268 3
        logger.warning(
269
            "Disabled extension %(ep)s: entry point name (%(ep)s) "
270
            "does not match extension name (%(ext)s)",
271
            {"ep": data.entry_point.name, "ext": data.extension.ext_name},
272
        )
273 3
        return False
274

275 3
    try:
276 3
        data.entry_point.require()
277 3
    except pkg_resources.DistributionNotFound as exc:
278 3
        logger.info(
279
            "Disabled extension %s: Dependency %s not found",
280
            data.extension.ext_name,
281
            exc,
282
        )
283 3
        return False
284 3
    except pkg_resources.VersionConflict as exc:
285 3
        if len(exc.args) == 2:
286 0
            found, required = exc.args
287 0
            logger.info(
288
                "Disabled extension %s: %s required, but found %s at %s",
289
                data.extension.ext_name,
290
                required,
291
                found,
292
                found.location,
293
            )
294
        else:
295 3
            logger.info(
296
                "Disabled extension %s: %s", data.extension.ext_name, exc
297
            )
298 3
        return False
299

300 3
    try:
301 3
        data.extension.validate_environment()
302 3
    except exceptions.ExtensionError as exc:
303 3
        logger.info("Disabled extension %s: %s", data.extension.ext_name, exc)
304 3
        return False
305 3
    except Exception:
306 3
        logger.exception(
307
            "Validating extension %s failed with an exception.",
308
            data.extension.ext_name,
309
        )
310 3
        return False
311

312 3
    if not data.config_schema:
313 3
        logger.error(
314
            "Extension %s does not have a config schema, disabling.",
315
            data.extension.ext_name,
316
        )
317 3
        return False
318 3
    elif not isinstance(data.config_schema.get("enabled"), config_lib.Boolean):
319 3
        logger.error(
320
            'Extension %s does not have the required "enabled" config'
321
            " option, disabling.",
322
            data.extension.ext_name,
323
        )
324 3
        return False
325

326 3
    for key, value in data.config_schema.items():
327 3
        if not isinstance(value, config_lib.ConfigValue):
328 3
            logger.error(
329
                "Extension %s config schema contains an invalid value"
330
                ' for the option "%s", disabling.',
331
                data.extension.ext_name,
332
                key,
333
            )
334 3
            return False
335

336 3
    if not data.config_defaults:
337 3
        logger.error(
338
            "Extension %s does not have a default config, disabling.",
339
            data.extension.ext_name,
340
        )
341 3
        return False
342

343 0
    return True

Read our documentation on viewing source code .

Loading