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
32 changes: 22 additions & 10 deletions beets/util/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
MAX_FILENAME_LENGTH = 200
WINDOWS_MAGIC_PREFIX = "\\\\?\\"
T = TypeVar("T")
T2 = TypeVar("T2")
StrPath = str | Path
PathLike = StrPath | bytes
Replacements = Sequence[tuple[Pattern[str], str]]
Expand Down Expand Up @@ -1060,15 +1061,15 @@ def par_map(transform: Callable[[T], Any], items: Sequence[T]) -> None:
pool.join()


class cached_classproperty(Generic[T]):
class cached_classproperty(Generic[T, T2]):
"""Descriptor implementing cached class properties.

Provides class-level dynamic property behavior where the getter function is
called once per class and the result is cached for subsequent access. Unlike
instance properties, this operates on the class rather than instances.
"""

cache: ClassVar[dict[tuple[type[object], str], object]] = {}
_cache: ClassVar[dict[tuple[type[object], str], object]] = {}

name: str = ""

Expand All @@ -1086,21 +1087,32 @@ class cached_classproperty(Generic[T]):
# "Callable[[Album], ...]"; expected "Callable[[type[Album]], ...]"
#
# Therefore, we just use `Any` here, which is not ideal, but works.
def __init__(self, getter: Callable[..., T]) -> None:
def __init__(self, getter: Callable[..., T2]) -> None:
"""Initialize the descriptor with the property getter function."""
self.getter: Callable[..., T] = getter
self.getter: Callable[[type[T]], T2] = getter

def __set_name__(self, owner: object, name: str) -> None:
def __set_name__(self, owner: T, name: str) -> None:
"""Capture the attribute name this descriptor is assigned to."""
self.name = name

def __get__(self, instance: object, owner: type[object]) -> T:
# For some reason, if we use T instead of object here,
# mypy complains when accessing a cached_property, e. g.:
# error: Argument 1 to "__get__" of "cached_classproperty" has incompatible type
# "MetadataSourcePlugin"; expected "Never"
# error: Argument 2 to "__get__" of "cached_classproperty" has incompatible type
# "type[MetadataSourcePlugin]"; expected "type[Never]"
def __get__(self, instance: object, owner: type[object]) -> T2:
"""Compute and cache if needed, and return the property value."""
key: tuple[type[object], str] = owner, self.name
if key not in self.cache:
self.cache[key] = self.getter(owner)
owner = cast(type[T], owner)
key: tuple[type[T], str] = owner, self.name
if key not in self._cache:
cached_classproperty._cache[key] = self.getter(owner)

return cast(T, self.cache[key])
return cast(T2, cached_classproperty._cache[key])

@classmethod
def clear_cache(cls) -> None:
cls._cache.clear()


class LazySharedInstance(Generic[T]):
Expand Down
2 changes: 1 addition & 1 deletion test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def pytest_assertrepr_compare(op, left, right):

@pytest.fixture(autouse=True)
def clear_cached_classproperty():
cached_classproperty.cache.clear()
cached_classproperty.clear_cache()


@pytest.fixture(scope="module")
Expand Down
Loading