diff --git a/beets/metadata_plugins.py b/beets/metadata_plugins.py index 7e84167ff..e765e4cbf 100644 --- a/beets/metadata_plugins.py +++ b/beets/metadata_plugins.py @@ -9,7 +9,7 @@ from __future__ import annotations import abc import re -import warnings +from functools import cache from typing import TYPE_CHECKING, Generic, Literal, Sequence, TypedDict, TypeVar import unidecode @@ -29,30 +29,11 @@ if TYPE_CHECKING: from .autotag.hooks import AlbumInfo, Item, TrackInfo +@cache def find_metadata_source_plugins() -> list[MetadataSourcePlugin]: - """Returns a list of MetadataSourcePlugin subclass instances - - Resolved from all currently loaded beets plugins. - """ - - all_plugins = find_plugins() - metadata_plugins: list[MetadataSourcePlugin | BeetsPlugin] = [] - for plugin in all_plugins: - if isinstance(plugin, MetadataSourcePlugin): - metadata_plugins.append(plugin) - elif hasattr(plugin, "data_source"): - # TODO: Remove this in the future major release, v3.0.0 - warnings.warn( - f"{plugin.__class__.__name__} is used as a legacy metadata source. " - "It should extend MetadataSourcePlugin instead of BeetsPlugin. " - "Support for this will be removed in the v3.0.0 release!", - DeprecationWarning, - stacklevel=2, - ) - metadata_plugins.append(plugin) - - # typeignore: BeetsPlugin is not a MetadataSourcePlugin (legacy support) - return metadata_plugins # type: ignore[return-value] + """Return a list of all loaded metadata source plugins.""" + # TODO: Make this an isinstance(MetadataSourcePlugin, ...) check in v3.0.0 + return [p for p in find_plugins() if hasattr(p, "data_source")] # type: ignore[misc] @notify_info_yielded("albuminfo_received") diff --git a/beets/plugins.py b/beets/plugins.py index c0dd12e5b..5d3e39cc7 100644 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -20,6 +20,7 @@ import abc import inspect import re import sys +import warnings from collections import defaultdict from functools import wraps from importlib import import_module @@ -160,19 +161,46 @@ class BeetsPlugin(metaclass=abc.ABCMeta): import_stages: list[ImportStageFunc] def __init_subclass__(cls) -> None: - # Dynamically copy methods to BeetsPlugin for legacy support - # TODO: Remove this in the future major release, v3.0.0 + """Enable legacy metadata‐source plugins to work with the new interface. + + When a plugin subclass of BeetsPlugin defines a `data_source` attribute + but does not inherit from MetadataSourcePlugin, this hook: + + 1. Skips abstract classes. + 2. Warns that the class should extend MetadataSourcePlugin (deprecation). + 3. Copies any nonabstract methods from MetadataSourcePlugin onto the + subclass to provide the full plugin API. + + This compatibility layer will be removed in the v3.0.0 release. + """ + # TODO: Remove in v3.0.0 if inspect.isabstract(cls): return from beets.metadata_plugins import MetadataSourcePlugin - abstractmethods = MetadataSourcePlugin.__abstractmethods__ - for name, method in inspect.getmembers( - MetadataSourcePlugin, predicate=inspect.isfunction + if issubclass(cls, MetadataSourcePlugin) or not hasattr( + cls, "data_source" ): - if name not in abstractmethods and not hasattr(cls, name): - setattr(cls, name, method) + return + + warnings.warn( + f"{cls.__name__} is used as a legacy metadata source. " + "It should extend MetadataSourcePlugin instead of BeetsPlugin. " + "Support for this will be removed in the v3.0.0 release!", + DeprecationWarning, + stacklevel=3, + ) + + for name, method in inspect.getmembers( + MetadataSourcePlugin, + predicate=lambda f: ( + inspect.isfunction(f) + and f.__name__ not in MetadataSourcePlugin.__abstractmethods__ + and not hasattr(cls, f.__name__) + ), + ): + setattr(cls, name, method) def __init__(self, name: str | None = None): """Perform one-time plugin setup."""