diff --git a/beets/plugins.py b/beets/plugins.py index 7fa0e660a..a8e803efd 100644 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -228,9 +228,9 @@ class BeetsPlugin(metaclass=abc.ABCMeta): # In order to verify the config we need to make sure the plugin is fully # configured (plugins usually add the default configuration *after* # calling super().__init__()). - self.register_listener("pluginload", self.verify_config) + self.register_listener("pluginload", self._verify_config) - def verify_config(self, *_, **__) -> None: + def _verify_config(self, *_, **__) -> None: """Verify plugin configuration. If deprecated 'source_weight' option is explicitly set by the user, they @@ -422,6 +422,12 @@ def _get_plugin(name: str) -> BeetsPlugin | None: Attempts to import the plugin module, locate the appropriate plugin class within it, and return an instance. Handles import failures gracefully and logs warnings for missing plugins or loading errors. + + Note we load the *last* plugin class found in the plugin namespace. This + allows plugins to define helper classes that inherit from BeetsPlugin + without those being loaded as the main plugin class. + + Returns None if the plugin could not be loaded for any reason. """ try: try: @@ -429,7 +435,7 @@ def _get_plugin(name: str) -> BeetsPlugin | None: except Exception as exc: raise PluginImportError(name) from exc - for obj in namespace.__dict__.values(): + for obj in reversed(namespace.__dict__.values()): if ( inspect.isclass(obj) and not isinstance( diff --git a/docs/changelog.rst b/docs/changelog.rst index 5f4afe58d..6d08d6bdb 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -15,6 +15,9 @@ New features: Bug fixes: +- |BeetsPlugin|: load the last plugin class defined in the plugin namespace. + :bug:`6093` + For packagers: - Fixed dynamic versioning install not disabled for source distribution builds. @@ -23,7 +26,7 @@ For packagers: Other changes: - Removed outdated mailing list contact information from the documentation - (:bug:`5462`). + :bug:`5462`. - :doc:`guides/main`: Modernized the *Getting Started* guide with tabbed sections and dropdown menus. Installation instructions have been streamlined, and a new subpage now provides additional setup details. @@ -66,7 +69,7 @@ Bug fixes: - :doc:`plugins/discogs` Fixed inconsistency in stripping disambiguation from artists but not labels. :bug:`5366` - :doc:`plugins/chroma` :doc:`plugins/bpsync` Fix plugin loading issue caused by - an import of another :class:`beets.plugins.BeetsPlugin` class. :bug:`6033` + an import of another |BeetsPlugin| class. :bug:`6033` - :doc:`/plugins/fromfilename`: Fix :bug:`5218`, improve the code (refactor regexps, allow for more cases, add some logging), add tests. - Metadata source plugins: Fixed data source penalty calculation that was @@ -188,8 +191,8 @@ For plugin developers: art sources might need to be adapted. - We split the responsibilities of plugins into two base classes - 1. :class:`beets.plugins.BeetsPlugin` is the base class for all plugins, any - plugin needs to inherit from this class. + 1. |BeetsPlugin| is the base class for all plugins, any plugin needs to + inherit from this class. 2. :class:`beets.metadata_plugin.MetadataSourcePlugin` allows plugins to act like metadata sources. E.g. used by the MusicBrainz plugin. All plugins in the beets repo are opted into this class where applicable. If you are @@ -5072,7 +5075,7 @@ BPD). To "upgrade" an old database, you can use the included ``albumify`` plugin list of plugin names) and ``pluginpath`` (a colon-separated list of directories to search beyond ``sys.path``). Plugins are just Python modules under the ``beetsplug`` namespace package containing subclasses of - ``beets.plugins.BeetsPlugin``. See `the beetsplug directory`_ for examples or + |BeetsPlugin|. See `the beetsplug directory`_ for examples or :doc:`/plugins/index` for instructions. - As a consequence of adding album art, the database was significantly refactored to keep track of some information at an album (rather than item) diff --git a/docs/conf.py b/docs/conf.py index 057141d22..a027b3005 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -82,6 +82,7 @@ man_pages = [ rst_epilog = """ .. |Album| replace:: :class:`~beets.library.models.Album` .. |AlbumInfo| replace:: :class:`beets.autotag.hooks.AlbumInfo` +.. |BeetsPlugin| replace:: :class:`beets.plugins.BeetsPlugin` .. |ImportSession| replace:: :class:`~beets.importer.session.ImportSession` .. |ImportTask| replace:: :class:`~beets.importer.tasks.ImportTask` .. |Item| replace:: :class:`~beets.library.models.Item` diff --git a/docs/dev/plugins/autotagger.rst b/docs/dev/plugins/autotagger.rst index 1cae5295e..8b6df6fb5 100644 --- a/docs/dev/plugins/autotagger.rst +++ b/docs/dev/plugins/autotagger.rst @@ -95,9 +95,9 @@ starting points include: Migration guidance ------------------ -Older metadata plugins that extend :py:class:`beets.plugins.BeetsPlugin` should -be migrated to :py:class:`MetadataSourcePlugin`. Legacy support will be removed -in **beets v3.0.0**. +Older metadata plugins that extend |BeetsPlugin| should be migrated to +:py:class:`MetadataSourcePlugin`. Legacy support will be removed in **beets +v3.0.0**. .. seealso:: diff --git a/docs/dev/plugins/index.rst b/docs/dev/plugins/index.rst index d258e7df6..a8feb32d9 100644 --- a/docs/dev/plugins/index.rst +++ b/docs/dev/plugins/index.rst @@ -40,8 +40,8 @@ or your plugin subpackage anymore. The meat of your plugin goes in ``myawesomeplugin.py``. Every plugin has to -extend the :class:`beets.plugins.BeetsPlugin` abstract base class [2]_ . For -instance, a minimal plugin without any functionality would look like this: +extend the |BeetsPlugin| abstract base class [2]_ . For instance, a minimal +plugin without any functionality would look like this: .. code-block:: python @@ -52,6 +52,12 @@ instance, a minimal plugin without any functionality would look like this: class MyAwesomePlugin(BeetsPlugin): pass +.. attention:: + + If your plugin is composed of intermediate |BeetsPlugin| subclasses, make + sure that your plugin is defined *last* in the namespace. We only load the + last subclass of |BeetsPlugin| we find in your plugin namespace. + To use your new plugin, you need to package [3]_ your plugin and install it into your ``beets`` (virtual) environment. To enable your plugin, add it it to the beets configuration diff --git a/docs/reference/config.rst b/docs/reference/config.rst index 30582d12c..eae9deb21 100644 --- a/docs/reference/config.rst +++ b/docs/reference/config.rst @@ -77,10 +77,10 @@ pluginpath ~~~~~~~~~~ Directories to search for plugins. Each Python file or directory in a plugin -path represents a plugin and should define a subclass of :class:`BeetsPlugin`. A -plugin can then be loaded by adding the filename to the ``plugins`` -configuration. The plugin path can either be a single string or a list of -strings---so, if you have multiple paths, format them as a YAML list like so: +path represents a plugin and should define a subclass of |BeetsPlugin|. A plugin +can then be loaded by adding the plugin name to the ``plugins`` configuration. +The plugin path can either be a single string or a list of strings---so, if you +have multiple paths, format them as a YAML list like so: :: diff --git a/extra/release.py b/extra/release.py index b47de8966..afa762baf 100755 --- a/extra/release.py +++ b/extra/release.py @@ -19,6 +19,8 @@ from packaging.version import Version, parse from sphinx.ext import intersphinx from typing_extensions import TypeAlias +from docs.conf import rst_epilog + BASE = Path(__file__).parent.parent.absolute() PYPROJECT = BASE / "pyproject.toml" CHANGELOG = BASE / "docs" / "changelog.rst" @@ -104,11 +106,21 @@ def create_rst_replacements() -> list[Replacement]: plugins = "|".join( r.split("/")[-1] for r in refs if r.startswith("plugins/") ) + explicit_replacements = dict( + line.removeprefix(".. ").split(" replace:: ") + for line in filter(None, rst_epilog.splitlines()) + ) return [ - # Replace Sphinx :ref: and :doc: directives by documentation URLs + # Replace explicitly defined substitutions from rst_epilog + # |BeetsPlugin| -> :class:`beets.plugins.BeetsPlugin` + ( + r"\|\w[^ ]*\|", + lambda m: explicit_replacements.get(m[0], m[0]), + ), + # Replace Sphinx directives by documentation URLs, e.g., # :ref:`/plugins/autobpm` -> [AutoBPM Plugin](DOCS/plugins/autobpm.html) ( - r":(?:ref|doc):`+(?:([^`<]+)<)?/?([\w./_-]+)>?`+", + r":(?:ref|doc|class):`+(?:([^`<]+)<)?/?([\w./_-]+)>?`+", lambda m: make_ref_link(m[2], m[1]), ), # Convert command references to documentation URLs