diff --git a/beets/__init__.py b/beets/__init__.py index 4891010a5..2c6069b29 100644 --- a/beets/__init__.py +++ b/beets/__init__.py @@ -26,13 +26,9 @@ __author__ = "Adrian Sampson " def __getattr__(name: str): """Handle deprecated imports.""" return deprecate_imports( - old_module=__name__, - new_module_by_name={ - "art": "beetsplug._utils", - "vfs": "beetsplug._utils", - }, - name=name, - version="3.0.0", + __name__, + {"art": "beetsplug._utils", "vfs": "beetsplug._utils"}, + name, ) diff --git a/beets/autotag/__init__.py b/beets/autotag/__init__.py index f79b193fd..beaf4341c 100644 --- a/beets/autotag/__init__.py +++ b/beets/autotag/__init__.py @@ -16,7 +16,6 @@ from __future__ import annotations -import warnings from importlib import import_module from typing import TYPE_CHECKING @@ -24,7 +23,7 @@ from beets import config, logging # Parts of external interface. from beets.util import unique_list -from beets.util.deprecation import deprecate_imports +from beets.util.deprecation import deprecate_for_maintainers, deprecate_imports from .hooks import AlbumInfo, AlbumMatch, TrackInfo, TrackMatch from .match import Proposal, Recommendation, tag_album, tag_item @@ -37,18 +36,13 @@ if TYPE_CHECKING: def __getattr__(name: str): if name == "current_metadata": - warnings.warn( - ( - f"'beets.autotag.{name}' is deprecated and will be removed in" - " 3.0.0. Use 'beets.util.get_most_common_tags' instead." - ), - DeprecationWarning, - stacklevel=2, + deprecate_for_maintainers( + f"'beets.autotag.{name}'", "'beets.util.get_most_common_tags'" ) return import_module("beets.util").get_most_common_tags return deprecate_imports( - __name__, {"Distance": "beets.autotag.distance"}, name, "3.0.0" + __name__, {"Distance": "beets.autotag.distance"}, name ) diff --git a/beets/library/__init__.py b/beets/library/__init__.py index afde96e0c..22416ecb5 100644 --- a/beets/library/__init__.py +++ b/beets/library/__init__.py @@ -13,7 +13,7 @@ NEW_MODULE_BY_NAME = dict.fromkeys( def __getattr__(name: str): - return deprecate_imports(__name__, NEW_MODULE_BY_NAME, name, "3.0.0") + return deprecate_imports(__name__, NEW_MODULE_BY_NAME, name) __all__ = [ diff --git a/beets/mediafile.py b/beets/mediafile.py index 8bde9274c..df735afff 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -13,17 +13,11 @@ # included in all copies or substantial portions of the Software. -import warnings - import mediafile -warnings.warn( - "beets.mediafile is deprecated; use mediafile instead", - # Show the location of the `import mediafile` statement as the warning's - # source, rather than this file, such that the offending module can be - # identified easily. - stacklevel=2, -) +from .util.deprecation import deprecate_for_maintainers + +deprecate_for_maintainers("'beets.mediafile'", "'mediafile'", stacklevel=2) # Import everything from the mediafile module into this module. for key, value in mediafile.__dict__.items(): @@ -31,4 +25,4 @@ for key, value in mediafile.__dict__.items(): globals()[key] = value # Cleanup namespace. -del key, value, warnings, mediafile +del key, value, mediafile diff --git a/beets/plugins.py b/beets/plugins.py index 0c7bae234..458c2351c 100644 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -20,7 +20,6 @@ import abc import inspect import re import sys -import warnings from collections import defaultdict from functools import cached_property, wraps from importlib import import_module @@ -33,6 +32,7 @@ from typing_extensions import ParamSpec import beets from beets import logging from beets.util import unique_list +from beets.util.deprecation import deprecate_for_maintainers if TYPE_CHECKING: from collections.abc import Callable, Iterable, Sequence @@ -184,11 +184,12 @@ class BeetsPlugin(metaclass=abc.ABCMeta): ): 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, + deprecate_for_maintainers( + ( + f"'{cls.__name__}' is used as a legacy metadata source since it" + " inherits 'beets.plugins.BeetsPlugin'. Support for this" + ), + "'beets.metadata_plugins.MetadataSourcePlugin'", stacklevel=3, ) @@ -265,7 +266,10 @@ class BeetsPlugin(metaclass=abc.ABCMeta): if source.filename: # user config self._log.warning(message) else: # 3rd-party plugin config - warnings.warn(message, DeprecationWarning, stacklevel=0) + deprecate_for_maintainers( + "'source_weight' configuration option", + "'data_source_mismatch_penalty'", + ) def commands(self) -> Sequence[Subcommand]: """Should return a list of beets.ui.Subcommand objects for diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index 12eb6d005..cfd8b6bd7 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -28,7 +28,6 @@ import sqlite3 import sys import textwrap import traceback -import warnings from difflib import SequenceMatcher from functools import cache from itertools import chain @@ -40,6 +39,7 @@ from beets import config, library, logging, plugins, util from beets.dbcore import db from beets.dbcore import query as db_query from beets.util import as_string +from beets.util.deprecation import deprecate_for_maintainers from beets.util.functemplate import template if TYPE_CHECKING: @@ -114,11 +114,7 @@ def decargs(arglist): .. deprecated:: 2.4.0 This function will be removed in 3.0.0. """ - warnings.warn( - "decargs() is deprecated and will be removed in version 3.0.0.", - DeprecationWarning, - stacklevel=2, - ) + deprecate_for_maintainers("'beets.ui.decargs'") return arglist diff --git a/beets/ui/commands/__init__.py b/beets/ui/commands/__init__.py index 214bcfbd0..d88d397ec 100644 --- a/beets/ui/commands/__init__.py +++ b/beets/ui/commands/__init__.py @@ -16,7 +16,7 @@ interface. """ -from beets.util import deprecate_imports +from beets.util.deprecation import deprecate_imports from .completion import completion_cmd from .config import config_cmd @@ -36,14 +36,12 @@ from .write import write_cmd def __getattr__(name: str): """Handle deprecated imports.""" return deprecate_imports( - old_module=__name__, - new_module_by_name={ + __name__, + { "TerminalImportSession": "beets.ui.commands.import_.session", "PromptChoice": "beets.ui.commands.import_.session", - # TODO: We might want to add more deprecated imports here }, - name=name, - version="3.0.0", + name, ) diff --git a/beets/util/deprecation.py b/beets/util/deprecation.py index 4bc939cb4..832408060 100644 --- a/beets/util/deprecation.py +++ b/beets/util/deprecation.py @@ -1,10 +1,39 @@ +from __future__ import annotations + import warnings from importlib import import_module from typing import Any +from packaging.version import Version + +import beets + + +def _format_message(old: str, new: str | None = None) -> str: + next_major = f"{Version(beets.__version__).major + 1}.0.0" + msg = f"{old} is deprecated and will be removed in version {next_major}." + if new: + msg += f" Use {new} instead." + + return msg + + +def deprecate_for_maintainers( + old: str, new: str | None = None, stacklevel: int = 1 +) -> None: + """Issue a deprecation warning visible to maintainers during development. + + Emits a DeprecationWarning that alerts developers about deprecated code + patterns. Unlike user-facing warnings, these are primarily for internal + code maintenance and appear during test runs or with warnings enabled. + """ + warnings.warn( + _format_message(old, new), DeprecationWarning, stacklevel=stacklevel + 1 + ) + def deprecate_imports( - old_module: str, new_module_by_name: dict[str, str], name: str, version: str + old_module: str, new_module_by_name: dict[str, str], name: str ) -> Any: """Handle deprecated module imports by redirecting to new locations. @@ -14,13 +43,9 @@ def deprecate_imports( existing code to continue working during transition periods. """ if new_module := new_module_by_name.get(name): - warnings.warn( - ( - f"'{old_module}.{name}' is deprecated and will be removed" - f" in {version}. Use '{new_module}.{name}' instead." - ), - DeprecationWarning, - stacklevel=2, + deprecate_for_maintainers( + f"'{old_module}.{name}'", f"'{new_module}.{name}'", stacklevel=2 ) + return getattr(import_module(new_module), name) raise AttributeError(f"module '{old_module}' has no attribute '{name}'")