mirror of
https://github.com/beetbox/beets.git
synced 2025-12-09 18:12:19 +01:00
Fix plugin loading (#6039)
Fixes #6033 This PR addresses a bug where plugin loading failed when plugins imported other `BeetsPlugin` classes, namely `chroma` and `bpsync`. - Add module path filtering to ensure only classes from the target plugin module are considered, preventing conflicts when plugins import other `BeetsPlugin` classes
This commit is contained in:
commit
689ec1022f
3 changed files with 49 additions and 29 deletions
|
|
@ -22,6 +22,7 @@ import re
|
||||||
import sys
|
import sys
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
from importlib import import_module
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import GenericAlias
|
from types import GenericAlias
|
||||||
from typing import TYPE_CHECKING, Any, ClassVar, Literal, TypeVar
|
from typing import TYPE_CHECKING, Any, ClassVar, Literal, TypeVar
|
||||||
|
|
@ -365,11 +366,11 @@ def _get_plugin(name: str) -> BeetsPlugin | None:
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
namespace = __import__(f"{PLUGIN_NAMESPACE}.{name}", None, None)
|
namespace = import_module(f"{PLUGIN_NAMESPACE}.{name}")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise PluginImportError(name) from exc
|
raise PluginImportError(name) from exc
|
||||||
|
|
||||||
for obj in getattr(namespace, name).__dict__.values():
|
for obj in namespace.__dict__.values():
|
||||||
if (
|
if (
|
||||||
inspect.isclass(obj)
|
inspect.isclass(obj)
|
||||||
and not isinstance(
|
and not isinstance(
|
||||||
|
|
@ -378,6 +379,12 @@ def _get_plugin(name: str) -> BeetsPlugin | None:
|
||||||
and issubclass(obj, BeetsPlugin)
|
and issubclass(obj, BeetsPlugin)
|
||||||
and obj != BeetsPlugin
|
and obj != BeetsPlugin
|
||||||
and not inspect.isabstract(obj)
|
and not inspect.isabstract(obj)
|
||||||
|
# Only consider this plugin's module or submodules to avoid
|
||||||
|
# conflicts when plugins import other BeetsPlugin classes
|
||||||
|
and (
|
||||||
|
obj.__module__ == namespace.__name__
|
||||||
|
or obj.__module__.startswith(f"{namespace.__name__}.")
|
||||||
|
)
|
||||||
):
|
):
|
||||||
return obj()
|
return obj()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ Bug fixes:
|
||||||
extraartists field.
|
extraartists field.
|
||||||
- :doc:`plugins/spotify` Fixed an issue where candidate lookup would not find
|
- :doc:`plugins/spotify` Fixed an issue where candidate lookup would not find
|
||||||
matches due to query escaping (single vs double quotes).
|
matches due to query escaping (single vs double quotes).
|
||||||
|
- :doc:`plugins/chroma` :doc:`plugins/bpsync` Fix plugin loading issue caused by
|
||||||
|
an import of another :class:`beets.plugins.BeetsPlugin` class. :bug:`6033`
|
||||||
|
|
||||||
For packagers:
|
For packagers:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,10 @@ import sys
|
||||||
import threading
|
import threading
|
||||||
import unittest
|
import unittest
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
from types import ModuleType
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
import beets.logging as blog
|
import beets.logging as blog
|
||||||
import beetsplug
|
|
||||||
from beets import plugins, ui
|
from beets import plugins, ui
|
||||||
from beets.test import _common, helper
|
from beets.test import _common, helper
|
||||||
from beets.test.helper import AsIsImporterMixin, ImportTestCase, PluginMixin
|
from beets.test.helper import AsIsImporterMixin, ImportTestCase, PluginMixin
|
||||||
|
|
@ -47,36 +48,46 @@ class LoggingTest(unittest.TestCase):
|
||||||
assert stream.getvalue(), "foo oof baz"
|
assert stream.getvalue(), "foo oof baz"
|
||||||
|
|
||||||
|
|
||||||
|
class DummyModule(ModuleType):
|
||||||
|
class DummyPlugin(plugins.BeetsPlugin):
|
||||||
|
def __init__(self):
|
||||||
|
plugins.BeetsPlugin.__init__(self, "dummy")
|
||||||
|
self.import_stages = [self.import_stage]
|
||||||
|
self.register_listener("dummy_event", self.listener)
|
||||||
|
|
||||||
|
def log_all(self, name):
|
||||||
|
self._log.debug("debug {}", name)
|
||||||
|
self._log.info("info {}", name)
|
||||||
|
self._log.warning("warning {}", name)
|
||||||
|
|
||||||
|
def commands(self):
|
||||||
|
cmd = ui.Subcommand("dummy")
|
||||||
|
cmd.func = lambda _, __, ___: self.log_all("cmd")
|
||||||
|
return (cmd,)
|
||||||
|
|
||||||
|
def import_stage(self, session, task):
|
||||||
|
self.log_all("import_stage")
|
||||||
|
|
||||||
|
def listener(self):
|
||||||
|
self.log_all("listener")
|
||||||
|
|
||||||
|
def __init__(self, *_, **__):
|
||||||
|
module_name = "beetsplug.dummy"
|
||||||
|
super().__init__(module_name)
|
||||||
|
self.DummyPlugin.__module__ = module_name
|
||||||
|
self.DummyPlugin = self.DummyPlugin
|
||||||
|
|
||||||
|
|
||||||
class LoggingLevelTest(AsIsImporterMixin, PluginMixin, ImportTestCase):
|
class LoggingLevelTest(AsIsImporterMixin, PluginMixin, ImportTestCase):
|
||||||
plugin = "dummy"
|
plugin = "dummy"
|
||||||
|
|
||||||
class DummyModule:
|
@classmethod
|
||||||
class DummyPlugin(plugins.BeetsPlugin):
|
def setUpClass(cls):
|
||||||
def __init__(self):
|
patcher = patch.dict(sys.modules, {"beetsplug.dummy": DummyModule()})
|
||||||
plugins.BeetsPlugin.__init__(self, "dummy")
|
patcher.start()
|
||||||
self.import_stages = [self.import_stage]
|
cls.addClassCleanup(patcher.stop)
|
||||||
self.register_listener("dummy_event", self.listener)
|
|
||||||
|
|
||||||
def log_all(self, name):
|
super().setUpClass()
|
||||||
self._log.debug("debug {}", name)
|
|
||||||
self._log.info("info {}", name)
|
|
||||||
self._log.warning("warning {}", name)
|
|
||||||
|
|
||||||
def commands(self):
|
|
||||||
cmd = ui.Subcommand("dummy")
|
|
||||||
cmd.func = lambda _, __, ___: self.log_all("cmd")
|
|
||||||
return (cmd,)
|
|
||||||
|
|
||||||
def import_stage(self, session, task):
|
|
||||||
self.log_all("import_stage")
|
|
||||||
|
|
||||||
def listener(self):
|
|
||||||
self.log_all("listener")
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
sys.modules["beetsplug.dummy"] = self.DummyModule
|
|
||||||
beetsplug.dummy = self.DummyModule
|
|
||||||
super().setUp()
|
|
||||||
|
|
||||||
def test_command_level0(self):
|
def test_command_level0(self):
|
||||||
self.config["verbose"] = 0
|
self.config["verbose"] = 0
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue