diff --git a/beets/plugins.py b/beets/plugins.py index a8e803efd..678d653b4 100644 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -22,7 +22,7 @@ import re import sys import warnings from collections import defaultdict -from functools import wraps +from functools import cached_property, wraps from importlib import import_module from pathlib import Path from types import GenericAlias @@ -192,12 +192,23 @@ class BeetsPlugin(metaclass=abc.ABCMeta): stacklevel=3, ) + method: property | cached_property[Any] | Callable[..., Any] 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__) + predicate=lambda f: ( # type: ignore[arg-type] + ( + isinstance(f, (property, cached_property)) + and not hasattr( + BeetsPlugin, + getattr(f, "attrname", None) or f.fget.__name__, # type: ignore[union-attr] + ) + ) + or ( + inspect.isfunction(f) + and f.__name__ + and not getattr(f, "__isabstractmethod__", False) + and not hasattr(BeetsPlugin, f.__name__) + ) ), ): setattr(cls, name, method) diff --git a/docs/changelog.rst b/docs/changelog.rst index 6d08d6bdb..88c157ec5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -22,6 +22,8 @@ For packagers: - Fixed dynamic versioning install not disabled for source distribution builds. :bug:`6089` +- Fixed issue with legacy metadata plugins not copying properties from the base + class. Other changes: diff --git a/test/autotag/test_distance.py b/test/autotag/test_distance.py index b327bbe44..213d32956 100644 --- a/test/autotag/test_distance.py +++ b/test/autotag/test_distance.py @@ -11,6 +11,7 @@ from beets.autotag.distance import ( ) from beets.library import Item from beets.metadata_plugins import MetadataSourcePlugin, get_penalty +from beets.plugins import BeetsPlugin from beets.test.helper import ConfigMixin _p = pytest.param @@ -310,8 +311,13 @@ class TestDataSourceDistance: def candidates(self, *args, **kwargs): ... def item_candidates(self, *args, **kwargs): ... - class OriginalPlugin(TestMetadataSourcePlugin): - pass + # We use BeetsPlugin here to check if our compatibility layer + # for pre 2.4.0 MetadataPlugins is working as expected + # TODO: Replace BeetsPlugin with TestMetadataSourcePlugin in v3.0.0 + with pytest.deprecated_call(): + + class OriginalPlugin(BeetsPlugin): + data_source = "Original" class OtherPlugin(TestMetadataSourcePlugin): @property @@ -332,6 +338,7 @@ class TestDataSourceDistance: [ _p("Original", "Original", 0.5, 1.0, True, MATCH, id="match"), _p("Original", "Other", 0.5, 1.0, True, MISMATCH, id="mismatch"), + _p("Other", "Original", 0.5, 1.0, True, MISMATCH, id="mismatch"), _p("Original", "unknown", 0.5, 1.0, True, MISMATCH, id="mismatch-unknown"), # noqa: E501 _p("Original", None, 0.5, 1.0, True, MISMATCH, id="mismatch-no-info"), # noqa: E501 _p(None, "Other", 0.5, 1.0, True, MISMATCH, id="mismatch-no-original-multiple-sources"), # noqa: E501 diff --git a/test/test_plugins.py b/test/test_plugins.py index df338f924..07bbf0966 100644 --- a/test/test_plugins.py +++ b/test/test_plugins.py @@ -523,3 +523,23 @@ class TestImportPlugin(PluginMixin): assert "PluginImportError" not in caplog.text, ( f"Plugin '{plugin_name}' has issues during import." ) + + +class TestDeprecationCopy: + # TODO: remove this test in Beets 3.0.0 + def test_legacy_metadata_plugin_deprecation(self): + """Test that a MetadataSourcePlugin with 'legacy' data_source + raises a deprecation warning and all function and properties are + copied from the base class. + """ + with pytest.warns(DeprecationWarning, match="LegacyMetadataPlugin"): + + class LegacyMetadataPlugin(plugins.BeetsPlugin): + data_source = "legacy" + + # Assert all methods are present + assert hasattr(LegacyMetadataPlugin, "albums_for_ids") + assert hasattr(LegacyMetadataPlugin, "tracks_for_ids") + assert hasattr(LegacyMetadataPlugin, "data_source_mismatch_penalty") + assert hasattr(LegacyMetadataPlugin, "_extract_id") + assert hasattr(LegacyMetadataPlugin, "get_artist")