Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 2 additions & 8 deletions beets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

import confuse

from .util import deprecate_imports
from .util.deprecation import deprecate_imports

__version__ = "2.5.1"
__author__ = "Adrian Sampson <[email protected]>"
Expand All @@ -26,13 +26,7 @@
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
Copy link
Contributor

@semohr semohr Oct 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you try to keep the trailing commas here? I recon we might add more imports here in the future.

See COM812:
The presence of a trailing comma can reduce diff size when parameters or elements are added or removed from function calls, function definitions, literals, etc.

)


Expand Down
14 changes: 4 additions & 10 deletions beets/autotag/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,15 @@

from __future__ import annotations

import warnings
from importlib import import_module
from typing import TYPE_CHECKING, Union

from beets import config, logging

# Parts of external interface.
from beets.util import unique_list
from beets.util.deprecation import deprecate_for_maintainers, deprecate_imports

from ..util import deprecate_imports
from .hooks import AlbumInfo, AlbumMatch, TrackInfo, TrackMatch
from .match import Proposal, Recommendation, tag_album, tag_item

Expand All @@ -37,18 +36,13 @@

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
)


Expand Down
4 changes: 2 additions & 2 deletions beets/library/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from beets.util import deprecate_imports
from beets.util.deprecation import deprecate_imports

from .exceptions import FileOperationError, ReadError, WriteError
from .library import Library
Expand All @@ -13,7 +13,7 @@


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__ = [
Expand Down
14 changes: 4 additions & 10 deletions beets/mediafile.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,16 @@
# 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():
if key not in ["__name__"]:
globals()[key] = value

# Cleanup namespace.
del key, value, warnings, mediafile
del key, value, mediafile
51 changes: 30 additions & 21 deletions beets/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import inspect
import re
import sys
import warnings
from collections import defaultdict
from functools import cached_property, wraps
from importlib import import_module
Expand All @@ -34,6 +33,7 @@
import beets
from beets import logging
from beets.util import unique_list
from beets.util.deprecation import deprecate_for_maintainers, deprecate_for_user

if TYPE_CHECKING:
from collections.abc import Callable, Iterable, Sequence
Expand Down Expand Up @@ -184,11 +184,12 @@ def __init_subclass__(cls) -> None:
):
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,
)

Expand Down Expand Up @@ -256,16 +257,19 @@ def _verify_config(self, *_, **__) -> None:
):
return

message = (
"'source_weight' configuration option is deprecated and will be"
" removed in v3.0.0. Use 'data_source_mismatch_penalty' instead"
)
for source in self.config.root().sources:
if "source_weight" in (source.get(self.name) or {}):
if source.filename: # user config
self._log.warning(message)
deprecate_for_user(
self._log,
f"'{self.name}.source_weight' configuration option",
f"'{self.name}.data_source_mismatch_penalty'",
)
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
Expand Down Expand Up @@ -414,16 +418,21 @@ def get_plugin_names() -> list[str]:
# *contain* a `beetsplug` package.
sys.path += paths
plugins = unique_list(beets.config["plugins"].as_str_seq())
# TODO: Remove in v3.0.0
if (
"musicbrainz" not in plugins
and "musicbrainz" in beets.config
and beets.config["musicbrainz"].get().get("enabled")
):
plugins.append("musicbrainz")

beets.config.add({"disabled_plugins": []})
disabled_plugins = set(beets.config["disabled_plugins"].as_str_seq())
# TODO: Remove in v3.0.0
mb_enabled = beets.config["musicbrainz"].flatten().get("enabled")
if mb_enabled:
deprecate_for_user(
log,
"'musicbrainz.enabled' configuration option",
"'plugins' configuration to explicitly add 'musicbrainz'",
)
if "musicbrainz" not in plugins:
plugins.append("musicbrainz")
elif mb_enabled is False:
deprecate_for_user(log, "'musicbrainz.enabled' configuration option")
disabled_plugins.add("musicbrainz")

return [p for p in plugins if p not in disabled_plugins]


Expand Down
8 changes: 2 additions & 6 deletions beets/ui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
import sys
import textwrap
import traceback
import warnings
from difflib import SequenceMatcher
from functools import cache
from itertools import chain
Expand All @@ -40,6 +39,7 @@
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

# On Windows platforms, use colorama to support "ANSI" terminal colors.
Expand Down Expand Up @@ -111,11 +111,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


Expand Down
24 changes: 0 additions & 24 deletions beets/util/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
import sys
import tempfile
import traceback
import warnings
from collections import Counter
from collections.abc import Sequence
from contextlib import suppress
Expand Down Expand Up @@ -1192,26 +1191,3 @@ def get_temp_filename(
def unique_list(elements: Iterable[T]) -> list[T]:
"""Return a list with unique elements in the original order."""
return list(dict.fromkeys(elements))


def deprecate_imports(
old_module: str, new_module_by_name: dict[str, str], name: str, version: str
) -> Any:
"""Handle deprecated module imports by redirecting to new locations.

Facilitates gradual migration of module structure by intercepting import
attempts for relocated functionality. Issues deprecation warnings while
transparently providing access to the moved implementation, allowing
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,
)
return getattr(import_module(new_module), name)
raise AttributeError(f"module '{old_module}' has no attribute '{name}'")
60 changes: 60 additions & 0 deletions beets/util/deprecation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from __future__ import annotations

import warnings
from importlib import import_module
from typing import TYPE_CHECKING, Any

from packaging.version import Version

import beets

if TYPE_CHECKING:
from logging import Logger


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_user(
logger: Logger, old: str, new: str | None = None
) -> None:
logger.warning(_format_message(old, new))


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
) -> Any:
"""Handle deprecated module imports by redirecting to new locations.

Facilitates gradual migration of module structure by intercepting import
attempts for relocated functionality. Issues deprecation warnings while
transparently providing access to the moved implementation, allowing
existing code to continue working during transition periods.
"""
if new_module := new_module_by_name.get(name):
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}'")
8 changes: 5 additions & 3 deletions beetsplug/musicbrainz.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import beets.autotag.hooks
from beets import config, plugins, util
from beets.metadata_plugins import MetadataSourcePlugin
from beets.util.deprecation import deprecate_for_user
from beets.util.id_extractors import extract_release_id

if TYPE_CHECKING:
Expand Down Expand Up @@ -389,9 +390,10 @@ def __init__(self):
self.config["search_limit"] = self.config["match"][
"searchlimit"
].get()
self._log.warning(
"'musicbrainz.searchlimit' option is deprecated and will be "
"removed in 3.0.0. Use 'musicbrainz.search_limit' instead."
deprecate_for_user(
self._log,
"'musicbrainz.searchlimit' configuration option",
"'musicbrainz.search_limit'",
)
hostname = self.config["host"].as_str()
https = self.config["https"].get(bool)
Expand Down
36 changes: 36 additions & 0 deletions test/test_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -543,3 +543,39 @@ class LegacyMetadataPlugin(plugins.BeetsPlugin):
assert hasattr(LegacyMetadataPlugin, "data_source_mismatch_penalty")
assert hasattr(LegacyMetadataPlugin, "_extract_id")
assert hasattr(LegacyMetadataPlugin, "get_artist")


class TestMusicBrainzPluginLoading:
@pytest.fixture(autouse=True)
def config(self):
_config = config
_config.sources = []
_config.read(user=False, defaults=True)
return _config

def test_default(self):
assert "musicbrainz" in plugins.get_plugin_names()

def test_other_plugin_enabled(self, config):
config["plugins"] = ["anything"]

assert "musicbrainz" not in plugins.get_plugin_names()

def test_deprecated_enabled(self, config, caplog):
config["plugins"] = ["anything"]
config["musicbrainz"]["enabled"] = True

assert "musicbrainz" in plugins.get_plugin_names()
assert (
"musicbrainz.enabled' configuration option is deprecated"
in caplog.text
)

def test_deprecated_disabled(self, config, caplog):
config["musicbrainz"]["enabled"] = False

assert "musicbrainz" not in plugins.get_plugin_names()
assert (
"musicbrainz.enabled' configuration option is deprecated"
in caplog.text
)
Loading