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

View file

@ -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(

View file

@ -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"
)

View file

@ -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:

View file

@ -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."