diff --git a/beets/plugins.py b/beets/plugins.py index db1000b34..947066da8 100644 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -31,6 +31,7 @@ from typing_extensions import ParamSpec import beets from beets import logging +from beets.util import unique_list if TYPE_CHECKING: from collections.abc import Callable, Iterable, Sequence @@ -275,9 +276,7 @@ class BeetsPlugin(metaclass=abc.ABCMeta): return helper -def get_plugin_names( - include: set[str] | None = None, exclude: set[str] | None = None -) -> set[str]: +def get_plugin_names() -> list[str]: """Discover and return the set of plugin names to be loaded. Configures the plugin search paths and resolves the final set of plugins @@ -298,14 +297,18 @@ def get_plugin_names( # For backwards compatibility, also support plugin paths that # *contain* a `beetsplug` package. sys.path += paths - plugins = include or set(beets.config["plugins"].as_str_seq()) + plugins = unique_list(beets.config["plugins"].as_str_seq()) # TODO: Remove in v3.0.0 - if "musicbrainz" in beets.config and beets.config["musicbrainz"].get().get( - "enabled" + if ( + "musicbrainz" not in plugins + and "musicbrainz" in beets.config + and beets.config["musicbrainz"].get().get("enabled") ): - plugins.add("musicbrainz") + plugins.append("musicbrainz") - return plugins - (exclude or set()) + beets.config.add({"disabled_plugins": []}) + disabled_plugins = set(beets.config["disabled_plugins"].as_str_seq()) + return [p for p in plugins if p not in disabled_plugins] def _get_plugin(name: str) -> BeetsPlugin | None: @@ -342,7 +345,7 @@ def _get_plugin(name: str) -> BeetsPlugin | None: _instances: list[BeetsPlugin] = [] -def load_plugins(*args, **kwargs) -> None: +def load_plugins() -> None: """Initialize the plugin system by loading all configured plugins. Performs one-time plugin discovery and instantiation, storing loaded plugin @@ -350,7 +353,7 @@ def load_plugins(*args, **kwargs) -> None: to notify other components. """ if not _instances: - names = get_plugin_names(*args, **kwargs) + names = get_plugin_names() log.info("Loading plugins: {}", ", ".join(sorted(names))) _instances.extend(filter(None, map(_get_plugin, names))) diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index cc421782f..01030a977 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -1579,13 +1579,7 @@ def _setup( """ config = _configure(options) - def _parse_list(option: str | None) -> set[str]: - return set((option or "").split(",")) - {""} - - plugins.load_plugins( - include=_parse_list(options.plugins), - exclude=_parse_list(options.exclude), - ) + plugins.load_plugins() # Get the default subcommands. from beets.ui.commands import default_commands @@ -1704,16 +1698,31 @@ def _raw_main(args: list[str], lib=None) -> None: parser.add_option( "-c", "--config", dest="config", help="path to configuration file" ) + + def parse_csl_callback( + option: optparse.Option, _, value: str, parser: SubcommandsOptionParser + ): + """Parse a comma-separated list of values.""" + setattr( + parser.values, + option.dest, # type: ignore[arg-type] + list(filter(None, value.split(","))), + ) + parser.add_option( "-p", "--plugins", dest="plugins", + action="callback", + callback=parse_csl_callback, help="a comma-separated list of plugins to load", ) parser.add_option( "-P", "--disable-plugins", - dest="exclude", + dest="disabled_plugins", + action="callback", + callback=parse_csl_callback, help="a comma-separated list of plugins to disable", ) parser.add_option( diff --git a/beetsplug/loadext.py b/beetsplug/loadext.py index cc673dab2..f20580217 100644 --- a/beetsplug/loadext.py +++ b/beetsplug/loadext.py @@ -25,7 +25,7 @@ class LoadExtPlugin(BeetsPlugin): super().__init__() if not Database.supports_extensions: - self._log.warn( + self._log.warning( "loadext is enabled but the current SQLite " "installation does not support extensions" ) diff --git a/docs/changelog.rst b/docs/changelog.rst index 5b0fd546b..75a11956b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -97,6 +97,9 @@ For plugin developers: type checking for downstream users of the beets API. * ``plugins.find_plugins`` function does not anymore load plugins. You need to explicitly call ``plugins.load_plugins()`` to load them. +* ``plugins.load_plugins`` function does not anymore accept the list of plugins + to load. Instead, it loads all plugins that are configured by + :ref:`plugins-config` configuration. Other changes: diff --git a/test/test_plugins.py b/test/test_plugins.py index 7d603e0e2..a5e031e66 100644 --- a/test/test_plugins.py +++ b/test/test_plugins.py @@ -91,9 +91,6 @@ class PluginImportTestCase(ImportHelper, PluginTestCase): class EventsTest(PluginImportTestCase): - def setUp(self): - super().setUp() - def test_import_task_created(self): self.importer = self.setup_importer(pretend=True) @@ -472,10 +469,22 @@ def get_available_plugins(): ] -class TestImportAllPlugins(PluginMixin): - def unimport_plugins(self): +class TestImportPlugin(PluginMixin): + @pytest.fixture(params=get_available_plugins()) + def plugin_name(self, request): + """Fixture to provide the name of each available plugin.""" + name = request.param + + # skip gstreamer plugins on windows + gstreamer_plugins = {"bpd", "replaygain"} + if sys.platform == "win32" and name in gstreamer_plugins: + pytest.skip(f"GStreamer is not available on Windows: {name}") + + return name + + def unload_plugins(self): """Unimport plugins before each test to avoid conflicts.""" - self.unload_plugins() + super().unload_plugins() for mod in list(sys.modules): if mod.startswith("beetsplug."): del sys.modules[mod] @@ -483,28 +492,21 @@ class TestImportAllPlugins(PluginMixin): @pytest.fixture(autouse=True) def cleanup(self): """Ensure plugins are unimported before and after each test.""" - self.unimport_plugins() + self.unload_plugins() yield - self.unimport_plugins() + self.unload_plugins() @pytest.mark.skipif( os.environ.get("GITHUB_ACTIONS") != "true", - reason="Requires all dependencies to be installed, " - + "which we can't guarantee in the local environment.", + reason=( + "Requires all dependencies to be installed, which we can't" + " guarantee in the local environment." + ), ) - @pytest.mark.parametrize("plugin_name", get_available_plugins()) - def test_import_plugin(self, caplog, plugin_name): # - """Test that a plugin is importable without an error using the - load_plugins function.""" - - # skip gstreamer plugins on windows - gstreamer_plugins = ["bpd", "replaygain"] - if sys.platform == "win32" and plugin_name in gstreamer_plugins: - pytest.xfail("GStreamer is not available on Windows: {plugin_name}") - + def test_import_plugin(self, caplog, plugin_name): + """Test that a plugin is importable without an error.""" caplog.set_level(logging.WARNING) - caplog.clear() - plugins.load_plugins(include={plugin_name}) + self.load_plugins(plugin_name) assert "PluginImportError" not in caplog.text, ( f"Plugin '{plugin_name}' has issues during import."