Skip to content

Commit 8a862ba

Browse files
committed
plugins: actually load plugins in load_plugins()
Instead of the first call to find_plugins(). This could change behavior slightly, since plugin load (and errors that happen in the process) could occur earlier now. But: - I'd argue this is better anyway, since plugin load errors now happen in the initial call to load_plugins() instead of somewhere in the middle of the first operation that uses plugins. - In our ui code, both happen immediately after each other anyway: ui._setup calls 1) ui._load_plugins -> plugins.load_plugins 2) plugins.types, plugins.named_queries, ...
1 parent 000157c commit 8a862ba

File tree

4 files changed

+39
-50
lines changed

4 files changed

+39
-50
lines changed

beets/plugins.py

Lines changed: 30 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747

4848

4949
if TYPE_CHECKING:
50-
from collections.abc import Iterable
50+
from collections.abc import Iterable, Iterator
5151

5252
from confuse import ConfigView
5353

@@ -342,7 +342,7 @@ def helper(func: TFunc[Item]) -> TFunc[Item]:
342342
return helper
343343

344344

345-
_classes: set[type[BeetsPlugin]] = set()
345+
_instances: dict[type[BeetsPlugin], BeetsPlugin] = {}
346346

347347

348348
def load_plugins(names: Sequence[str] = ()) -> None:
@@ -351,6 +351,8 @@ def load_plugins(names: Sequence[str] = ()) -> None:
351351
package in sys.path; the module indicated should contain the
352352
BeetsPlugin subclasses desired.
353353
"""
354+
classes = set()
355+
354356
for name in names:
355357
try:
356358
mod = import_module(f".{name}", package=PLUGIN_NAMESPACE)
@@ -365,43 +367,39 @@ def load_plugins(names: Sequence[str] = ()) -> None:
365367
)
366368
continue
367369

368-
for _name, obj in inspect.getmembers(mod, inspect.isclass):
370+
for _name, cls in inspect.getmembers(mod, inspect.isclass):
369371
if (
370-
issubclass(obj, BeetsPlugin)
371-
and obj != BeetsPlugin
372-
and obj != MetadataSourcePlugin
372+
issubclass(cls, BeetsPlugin)
373+
and cls != BeetsPlugin
374+
and cls != MetadataSourcePlugin
375+
and cls not in _instances
373376
):
374-
_classes.add(obj)
377+
classes.add(cls)
375378

379+
for cls in classes:
380+
_register_plugin(cls)
376381

377-
_instances: dict[type[BeetsPlugin], BeetsPlugin] = {}
378382

383+
def _register_plugin(cls: type[BeetsPlugin]) -> None:
384+
if cls in _instances:
385+
return
379386

380-
def find_plugins() -> list[BeetsPlugin]:
381-
"""Returns a list of BeetsPlugin subclass instances from all
382-
currently loaded beets plugins. Loads the default plugin set
383-
first.
387+
try:
388+
_instances[cls] = cls()
389+
except Exception:
390+
log.error(
391+
"failed to initialize plugin class {}.{}",
392+
cls.__module__,
393+
cls.__qualname__,
394+
)
395+
raise
396+
397+
398+
def find_plugins() -> Iterator[BeetsPlugin]:
399+
"""Returns an iterator of BeetsPlugin subclass instances from all
400+
currently loaded beets plugins.
384401
"""
385-
if _instances:
386-
# After the first call, use cached instances for performance reasons.
387-
# See https://github.com/beetbox/beets/pull/3810
388-
return list(_instances.values())
389-
390-
plugins = []
391-
for cls in _classes:
392-
# Only instantiate each plugin class once.
393-
if cls not in _instances:
394-
try:
395-
_instances[cls] = cls()
396-
except Exception:
397-
log.error(
398-
"failed to initialize plugin class {}.{}",
399-
cls.__module__,
400-
cls.__qualname__,
401-
)
402-
raise
403-
plugins.append(_instances[cls])
404-
return plugins
402+
yield from _instances.values()
405403

406404

407405
# Communication with plugins.

beets/test/helper.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,6 @@ def unload_plugins(self) -> None:
487487
for plugin_class in beets.plugins._instances:
488488
plugin_class.listeners = None
489489
self.config["plugins"] = []
490-
beets.plugins._classes = set()
491490
beets.plugins._instances = {}
492491
Item._types = self.original_item_types
493492
Album._types = self.original_album_types

test/test_plugins.py

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -37,30 +37,22 @@
3737

3838

3939
class PluginLoaderTestCase(BasePluginTestCase):
40-
def setup_plugin_loader(self):
41-
# FIXME the mocking code is horrific, but this is the lowest and
42-
# earliest level of the plugin mechanism we can hook into.
43-
self._plugin_loader_patch = patch("beets.plugins.load_plugins")
44-
self._plugin_classes = set()
45-
load_plugins = self._plugin_loader_patch.start()
40+
"""A test case that adds an additional plugin to beets' plugin method
41+
resolution mechanism.
4642
47-
def myload(names=()):
48-
plugins._classes.update(self._plugin_classes)
49-
50-
load_plugins.side_effect = myload
51-
52-
def teardown_plugin_loader(self):
53-
self._plugin_loader_patch.stop()
43+
It does _not_ go through the full `plugins.load_plugins` code, which would
44+
require that the plugin_class is part of an importable module.
45+
"""
5446

5547
def register_plugin(self, plugin_class):
56-
self._plugin_classes.add(plugin_class)
48+
plugins._register_plugin(plugin_class)
5749

5850
def setUp(self):
59-
self.setup_plugin_loader()
6051
super().setUp()
52+
self._prev_plugin_instances = plugins._instances.copy()
6153

6254
def tearDown(self):
63-
self.teardown_plugin_loader()
55+
plugins._instances = self._prev_plugin_instances
6456
super().tearDown()
6557

6658

test/test_ui.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1059,7 +1059,7 @@ def test_cli_config_file_loads_plugin_commands(self):
10591059
file.write("plugins: test")
10601060

10611061
self.run_command("--config", self.cli_config_path, "plugin", lib=None)
1062-
assert plugins.find_plugins()[0].is_test_plugin
1062+
assert next(iter(plugins.find_plugins())).is_test_plugin
10631063
self.unload_plugins()
10641064

10651065
def test_beetsdir_config(self):

0 commit comments

Comments
 (0)