mirror of
https://github.com/beetbox/beets.git
synced 2026-07-22 02:06:50 -04:00
Move AttrDict under beets.util
This commit is contained in:
@@ -8,7 +8,7 @@ from importlib import import_module
|
||||
from beets.util.deprecation import deprecate_for_maintainers, deprecate_imports
|
||||
|
||||
from .distance import Distance, distance, string_dist, track_distance
|
||||
from .hooks import AlbumInfo, AttrDict, Info, TrackInfo, correct_list_fields
|
||||
from .hooks import AlbumInfo, Info, TrackInfo, correct_list_fields
|
||||
from .match import (
|
||||
AlbumMatch,
|
||||
Match,
|
||||
@@ -34,7 +34,6 @@ def __getattr__(name: str):
|
||||
__all__ = [
|
||||
"AlbumInfo",
|
||||
"AlbumMatch",
|
||||
"AttrDict",
|
||||
"Distance",
|
||||
"Info",
|
||||
"Match",
|
||||
|
||||
@@ -4,20 +4,16 @@ from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from functools import cached_property
|
||||
from typing import Any, ClassVar, TypeVar
|
||||
|
||||
from typing_extensions import Self
|
||||
from typing import Any, ClassVar
|
||||
|
||||
from beets import config, logging
|
||||
from beets.util import cached_classproperty, unique_list
|
||||
from beets.util import AttrDict, cached_classproperty, unique_list
|
||||
from beets.util.deprecation import (
|
||||
ALBUM_LEGACY_TO_LIST_FIELD,
|
||||
ITEM_LEGACY_TO_LIST_FIELD,
|
||||
deprecate_for_maintainers,
|
||||
)
|
||||
|
||||
V = TypeVar("V")
|
||||
|
||||
JSONDict = dict[str, Any]
|
||||
|
||||
log = logging.getLogger("beets")
|
||||
@@ -87,51 +83,6 @@ def correct_list_fields(input_data: JSONDict) -> JSONDict:
|
||||
|
||||
|
||||
# Classes used to represent candidate options.
|
||||
class AttrDict(dict[str, V]):
|
||||
"""Mapping enabling attribute-style access to stored metadata values."""
|
||||
|
||||
def copy(self) -> Self:
|
||||
"""Return a detached copy preserving subclass-specific behavior."""
|
||||
return deepcopy(self)
|
||||
|
||||
def __getattribute__(self, attr: str) -> V:
|
||||
# Intercept cached_property failures so an AttributeError raised
|
||||
# inside the property body is not masked by __getattr__ fallback.
|
||||
# Reuse the original traceback so the wrapped RuntimeError still
|
||||
# points at the real failing line, but suppress the printed cause
|
||||
# block to keep CLI tracebacks readable. See #6558 (and #6503 /
|
||||
# #6506 for the same masking pattern with different metadata
|
||||
# providers).
|
||||
try:
|
||||
return super().__getattribute__(attr)
|
||||
except AttributeError as exc:
|
||||
if not attr.startswith("__"):
|
||||
for klass in type(self).__mro__:
|
||||
descr = klass.__dict__.get(attr)
|
||||
if descr is None:
|
||||
continue
|
||||
if isinstance(descr, cached_property):
|
||||
raise RuntimeError(
|
||||
f"{type(self).__name__}.{attr} failed: {exc}"
|
||||
).with_traceback(exc.__traceback__) from None
|
||||
break
|
||||
raise
|
||||
|
||||
def __getattr__(self, attr: str) -> V:
|
||||
if attr in self:
|
||||
return self[attr]
|
||||
|
||||
raise AttributeError(
|
||||
f"'{self.__class__.__name__}' object has no attribute '{attr}'"
|
||||
)
|
||||
|
||||
def __setattr__(self, key: str, value: V) -> None:
|
||||
self.__setitem__(key, value)
|
||||
|
||||
def __hash__(self) -> int: # type: ignore[override]
|
||||
return id(self)
|
||||
|
||||
|
||||
class Info(AttrDict[Any]):
|
||||
"""Container for metadata about a musical entity."""
|
||||
|
||||
|
||||
@@ -1199,3 +1199,48 @@ def chunks(lst: Sequence[T], n: int) -> Iterator[list[T]]:
|
||||
"""Yield successive n-sized chunks from lst."""
|
||||
for i in range(0, len(lst), n):
|
||||
yield list(lst[i : i + n])
|
||||
|
||||
|
||||
class AttrDict(dict[str, T]):
|
||||
"""Mapping enabling attribute-style access to stored metadata values."""
|
||||
|
||||
def copy(self) -> Self:
|
||||
"""Return a detached copy preserving subclass-specific behavior."""
|
||||
return deepcopy(self)
|
||||
|
||||
def __getattribute__(self, attr: str) -> T:
|
||||
# Intercept cached_property failures so an AttributeError raised
|
||||
# inside the property body is not masked by __getattr__ fallback.
|
||||
# Reuse the original traceback so the wrapped RuntimeError still
|
||||
# points at the real failing line, but suppress the printed cause
|
||||
# block to keep CLI tracebacks readable. See #6558 (and #6503 /
|
||||
# #6506 for the same masking pattern with different metadata
|
||||
# providers).
|
||||
try:
|
||||
return super().__getattribute__(attr)
|
||||
except AttributeError as exc:
|
||||
if not attr.startswith("__"):
|
||||
for klass in type(self).__mro__:
|
||||
descr = klass.__dict__.get(attr)
|
||||
if descr is None:
|
||||
continue
|
||||
if isinstance(descr, cached_property):
|
||||
raise RuntimeError(
|
||||
f"{type(self).__name__}.{attr} failed: {exc}"
|
||||
).with_traceback(exc.__traceback__) from None
|
||||
break
|
||||
raise
|
||||
|
||||
def __getattr__(self, attr: str) -> T:
|
||||
if attr in self:
|
||||
return self[attr]
|
||||
|
||||
raise AttributeError(
|
||||
f"'{self.__class__.__name__}' object has no attribute '{attr}'"
|
||||
)
|
||||
|
||||
def __setattr__(self, key: str, value: T) -> None:
|
||||
self.__setitem__(key, value)
|
||||
|
||||
def __hash__(self) -> int: # type: ignore[override]
|
||||
return id(self)
|
||||
|
||||
@@ -9,7 +9,6 @@ import pytest
|
||||
from beets.autotag import (
|
||||
AlbumInfo,
|
||||
AlbumMatch,
|
||||
AttrDict,
|
||||
Distance,
|
||||
TrackInfo,
|
||||
TrackMatch,
|
||||
@@ -17,6 +16,7 @@ from beets.autotag import (
|
||||
)
|
||||
from beets.library import Item
|
||||
from beets.test.helper import BeetsTestCase
|
||||
from beets.util import AttrDict
|
||||
|
||||
str_field_deprecation = pytest.warns(
|
||||
DeprecationWarning, match="The 'genre' field is deprecated"
|
||||
|
||||
Reference in New Issue
Block a user