From 4026bd5df50a9a90ee6dde096a773aeb06a00794 Mon Sep 17 00:00:00 2001 From: Konstantin <78656278+amogus07@users.noreply.github.com> Date: Mon, 2 Feb 2026 21:54:07 +0100 Subject: [PATCH 1/3] improve cached_classproperty type annotations --- beets/util/__init__.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/beets/util/__init__.py b/beets/util/__init__.py index fc05e4997..1fb6ac3fe 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -65,6 +65,7 @@ if TYPE_CHECKING: MAX_FILENAME_LENGTH = 200 WINDOWS_MAGIC_PREFIX = "\\\\?\\" T = TypeVar("T") +T2 = TypeVar("T2") PathLike = Union[str, bytes, Path] StrPath = Union[str, Path] Replacements = Sequence[tuple[Pattern[str], str]] @@ -1053,7 +1054,7 @@ 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 @@ -1061,7 +1062,7 @@ class cached_classproperty(Generic[T]): instance properties, this operates on the class rather than instances. """ - cache: ClassVar[dict[tuple[type[object], str], object]] = {} + cache: dict[tuple[type[T], str], T2] = {} name: str = "" @@ -1079,21 +1080,28 @@ 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 + owner = cast(type[T], owner) + key: tuple[type[T], str] = owner, self.name if key not in self.cache: self.cache[key] = self.getter(owner) - return cast(T, self.cache[key]) + return self.cache[key] class LazySharedInstance(Generic[T]): From 5f7cd484e620f212f12538415ae6d3f4a83ce074 Mon Sep 17 00:00:00 2001 From: Konstantin <78656278+amogus07@users.noreply.github.com> Date: Mon, 2 Feb 2026 21:55:24 +0100 Subject: [PATCH 2/3] make cache private and add clear_cache method to cached_classproperty --- beets/util/__init__.py | 12 ++++++++---- test/conftest.py | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 1fb6ac3fe..55ca8a8f7 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -1062,7 +1062,7 @@ class cached_classproperty(Generic[T, T2]): instance properties, this operates on the class rather than instances. """ - cache: dict[tuple[type[T], str], T2] = {} + _cache: dict[tuple[type[T], str], T2] = {} name: str = "" @@ -1098,10 +1098,14 @@ class cached_classproperty(Generic[T, T2]): """Compute and cache if needed, and return the property value.""" owner = cast(type[T], owner) key: tuple[type[T], str] = owner, self.name - if key not in self.cache: - self.cache[key] = self.getter(owner) + if key not in self._cache: + self._cache[key] = self.getter(owner) - return self.cache[key] + return self._cache[key] + + @classmethod + def clear_cache(cls) -> None: + cls._cache.clear() class LazySharedInstance(Generic[T]): diff --git a/test/conftest.py b/test/conftest.py index eb46b94b0..4918fd79e 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -52,4 +52,4 @@ def pytest_assertrepr_compare(op, left, right): @pytest.fixture(autouse=True) def clear_cached_classproperty(): - cached_classproperty.cache.clear() + cached_classproperty.clear_cache() From ed0f50d2aff5ce1caea3ac9091acc16469b07b5f Mon Sep 17 00:00:00 2001 From: Konstantin <78656278+amogus07@users.noreply.github.com> Date: Tue, 3 Feb 2026 12:56:01 +0100 Subject: [PATCH 3/3] use _cache as a ClassVar --- beets/util/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/beets/util/__init__.py b/beets/util/__init__.py index e109098b8..e6da8e96a 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -1069,7 +1069,7 @@ class cached_classproperty(Generic[T, T2]): instance properties, this operates on the class rather than instances. """ - _cache: dict[tuple[type[T], str], T2] = {} + _cache: ClassVar[dict[tuple[type[object], str], object]] = {} name: str = "" @@ -1106,9 +1106,9 @@ class cached_classproperty(Generic[T, T2]): owner = cast(type[T], owner) key: tuple[type[T], str] = owner, self.name if key not in self._cache: - self._cache[key] = self.getter(owner) + cached_classproperty._cache[key] = self.getter(owner) - return self._cache[key] + return cast(T2, cached_classproperty._cache[key]) @classmethod def clear_cache(cls) -> None: