Use only plugins/disabled_plugins config in plugin loading

This commit is contained in:
Šarūnas Nejus 2025-08-07 10:37:29 +01:00
parent e9feb41709
commit 54b31d01e9
No known key found for this signature in database
GPG key ID: DD28F6704DBE3435
5 changed files with 58 additions and 41 deletions

View file

@ -31,6 +31,7 @@ from typing_extensions import ParamSpec
import beets import beets
from beets import logging from beets import logging
from beets.util import unique_list
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Callable, Iterable, Sequence from collections.abc import Callable, Iterable, Sequence
@ -275,9 +276,7 @@ class BeetsPlugin(metaclass=abc.ABCMeta):
return helper return helper
def get_plugin_names( def get_plugin_names() -> list[str]:
include: set[str] | None = None, exclude: set[str] | None = None
) -> set[str]:
"""Discover and return the set of plugin names to be loaded. """Discover and return the set of plugin names to be loaded.
Configures the plugin search paths and resolves the final set of plugins 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 # For backwards compatibility, also support plugin paths that
# *contain* a `beetsplug` package. # *contain* a `beetsplug` package.
sys.path += paths 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 # TODO: Remove in v3.0.0
if "musicbrainz" in beets.config and beets.config["musicbrainz"].get().get( if (
"enabled" "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: def _get_plugin(name: str) -> BeetsPlugin | None:
@ -342,7 +345,7 @@ def _get_plugin(name: str) -> BeetsPlugin | None:
_instances: list[BeetsPlugin] = [] _instances: list[BeetsPlugin] = []
def load_plugins(*args, **kwargs) -> None: def load_plugins() -> None:
"""Initialize the plugin system by loading all configured plugins. """Initialize the plugin system by loading all configured plugins.
Performs one-time plugin discovery and instantiation, storing loaded plugin Performs one-time plugin discovery and instantiation, storing loaded plugin
@ -350,7 +353,7 @@ def load_plugins(*args, **kwargs) -> None:
to notify other components. to notify other components.
""" """
if not _instances: if not _instances:
names = get_plugin_names(*args, **kwargs) names = get_plugin_names()
log.info("Loading plugins: {}", ", ".join(sorted(names))) log.info("Loading plugins: {}", ", ".join(sorted(names)))
_instances.extend(filter(None, map(_get_plugin, names))) _instances.extend(filter(None, map(_get_plugin, names)))

View file

@ -1579,13 +1579,7 @@ def _setup(
""" """
config = _configure(options) config = _configure(options)
def _parse_list(option: str | None) -> set[str]: plugins.load_plugins()
return set((option or "").split(",")) - {""}
plugins.load_plugins(
include=_parse_list(options.plugins),
exclude=_parse_list(options.exclude),
)
# Get the default subcommands. # Get the default subcommands.
from beets.ui.commands import default_commands from beets.ui.commands import default_commands
@ -1704,16 +1698,31 @@ def _raw_main(args: list[str], lib=None) -> None:
parser.add_option( parser.add_option(
"-c", "--config", dest="config", help="path to configuration file" "-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( parser.add_option(
"-p", "-p",
"--plugins", "--plugins",
dest="plugins", dest="plugins",
action="callback",
callback=parse_csl_callback,
help="a comma-separated list of plugins to load", help="a comma-separated list of plugins to load",
) )
parser.add_option( parser.add_option(
"-P", "-P",
"--disable-plugins", "--disable-plugins",
dest="exclude", dest="disabled_plugins",
action="callback",
callback=parse_csl_callback,
help="a comma-separated list of plugins to disable", help="a comma-separated list of plugins to disable",
) )
parser.add_option( parser.add_option(

View file

@ -25,7 +25,7 @@ class LoadExtPlugin(BeetsPlugin):
super().__init__() super().__init__()
if not Database.supports_extensions: if not Database.supports_extensions:
self._log.warn( self._log.warning(
"loadext is enabled but the current SQLite " "loadext is enabled but the current SQLite "
"installation does not support extensions" "installation does not support extensions"
) )

View file

@ -97,6 +97,9 @@ For plugin developers:
type checking for downstream users of the beets API. type checking for downstream users of the beets API.
* ``plugins.find_plugins`` function does not anymore load plugins. You need to * ``plugins.find_plugins`` function does not anymore load plugins. You need to
explicitly call ``plugins.load_plugins()`` to load them. 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: Other changes:

View file

@ -91,9 +91,6 @@ class PluginImportTestCase(ImportHelper, PluginTestCase):
class EventsTest(PluginImportTestCase): class EventsTest(PluginImportTestCase):
def setUp(self):
super().setUp()
def test_import_task_created(self): def test_import_task_created(self):
self.importer = self.setup_importer(pretend=True) self.importer = self.setup_importer(pretend=True)
@ -472,10 +469,22 @@ def get_available_plugins():
] ]
class TestImportAllPlugins(PluginMixin): class TestImportPlugin(PluginMixin):
def unimport_plugins(self): @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.""" """Unimport plugins before each test to avoid conflicts."""
self.unload_plugins() super().unload_plugins()
for mod in list(sys.modules): for mod in list(sys.modules):
if mod.startswith("beetsplug."): if mod.startswith("beetsplug."):
del sys.modules[mod] del sys.modules[mod]
@ -483,28 +492,21 @@ class TestImportAllPlugins(PluginMixin):
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def cleanup(self): def cleanup(self):
"""Ensure plugins are unimported before and after each test.""" """Ensure plugins are unimported before and after each test."""
self.unimport_plugins() self.unload_plugins()
yield yield
self.unimport_plugins() self.unload_plugins()
@pytest.mark.skipif( @pytest.mark.skipif(
os.environ.get("GITHUB_ACTIONS") != "true", os.environ.get("GITHUB_ACTIONS") != "true",
reason="Requires all dependencies to be installed, " reason=(
+ "which we can't guarantee in the local environment.", "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):
def test_import_plugin(self, caplog, plugin_name): # """Test that a plugin is importable without an error."""
"""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}")
caplog.set_level(logging.WARNING) caplog.set_level(logging.WARNING)
caplog.clear() self.load_plugins(plugin_name)
plugins.load_plugins(include={plugin_name})
assert "PluginImportError" not in caplog.text, ( assert "PluginImportError" not in caplog.text, (
f"Plugin '{plugin_name}' has issues during import." f"Plugin '{plugin_name}' has issues during import."