From 293b140528c75bcd1a8c7f12438ca01b0558e613 Mon Sep 17 00:00:00 2001 From: Shamil Date: Tue, 23 Sep 2025 14:03:46 +0300 Subject: [PATCH 1/5] Update func.pyi with precise cache decorator type hints --- stubs/cachetools/cachetools/func.pyi | 55 +++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/stubs/cachetools/cachetools/func.pyi b/stubs/cachetools/cachetools/func.pyi index 8608a1060e43..0480334ffe19 100644 --- a/stubs/cachetools/cachetools/func.pyi +++ b/stubs/cachetools/cachetools/func.pyi @@ -1,16 +1,51 @@ -from _typeshed import IdentityFunction from collections.abc import Callable, Sequence -from typing import TypeVar +from typing import Any, Final, Generic, NamedTuple, TypeVar, overload + +__all__: Final = ("fifo_cache", "lfu_cache", "lru_cache", "rr_cache", "ttl_cache") -__all__ = ("fifo_cache", "lfu_cache", "lru_cache", "rr_cache", "ttl_cache") _T = TypeVar("_T") +_R = TypeVar("_R") + +class _CacheInfo(NamedTuple): + hits: int + misses: int + maxsize: int | None + currsize: int + +class _cachetools_cache_wrapper(Generic[_R]): + __wrapped__: Callable[..., _R] + def __call__(self, /, *args: Any, **kwargs: Any) -> _R: ... + def cache_info(self) -> _CacheInfo: ... + def cache_clear(self) -> None: ... + def cache_parameters(self) -> dict[str, Any]: ... -def fifo_cache(maxsize: float | None = 128, typed: bool = False) -> IdentityFunction: ... -def lfu_cache(maxsize: float | None = 128, typed: bool = False) -> IdentityFunction: ... -def lru_cache(maxsize: float | None = 128, typed: bool = False) -> IdentityFunction: ... +@overload +def fifo_cache( + maxsize: int | None = 128, typed: bool = False +) -> Callable[[Callable[..., _R]], _cachetools_cache_wrapper[_R]]: ... +@overload +def fifo_cache(maxsize: Callable[..., _R], typed: bool = False) -> _cachetools_cache_wrapper[_R]: ... +@overload +def lfu_cache(maxsize: int | None = 128, typed: bool = False) -> Callable[[Callable[..., _R]], _cachetools_cache_wrapper[_R]]: ... +@overload +def lfu_cache(maxsize: Callable[..., _R], typed: bool = False) -> _cachetools_cache_wrapper[_R]: ... +@overload +def lru_cache(maxsize: int | None = 128, typed: bool = False) -> Callable[[Callable[..., _R]], _cachetools_cache_wrapper[_R]]: ... +@overload +def lru_cache(maxsize: Callable[..., _R], typed: bool = False) -> _cachetools_cache_wrapper[_R]: ... +@overload def rr_cache( - maxsize: float | None = 128, choice: Callable[[Sequence[_T]], _T] | None = ..., typed: bool = False -) -> IdentityFunction: ... + maxsize: int | None = 128, choice: Callable[[Sequence[_T]], _T] | None = ..., typed: bool = False +) -> Callable[[Callable[..., _R]], _cachetools_cache_wrapper[_R]]: ... +@overload +def rr_cache( + maxsize: Callable[..., _R], choice: Callable[[Sequence[_T]], _T] | None = ..., typed: bool = False +) -> _cachetools_cache_wrapper[_R]: ... +@overload +def ttl_cache( + maxsize: int | None = 128, ttl: float = 600, timer: Callable[[], float] = ..., typed: bool = False +) -> Callable[[Callable[..., _R]], _cachetools_cache_wrapper[_R]]: ... +@overload def ttl_cache( - maxsize: float | None = 128, ttl: float = 600, timer: Callable[[], float] = ..., typed: bool = False -) -> IdentityFunction: ... + maxsize: Callable[..., _R], ttl: float = 600, timer: Callable[[], float] = ..., typed: bool = False +) -> _cachetools_cache_wrapper[_R]: ... From 8dda4ea3b527cbba392af4b96bc3bfb73b2ba000 Mon Sep 17 00:00:00 2001 From: Shamil Date: Tue, 23 Sep 2025 14:04:17 +0300 Subject: [PATCH 2/5] Add positional-only marker to methodkey and typedmethodkey --- stubs/cachetools/cachetools/keys.pyi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stubs/cachetools/cachetools/keys.pyi b/stubs/cachetools/cachetools/keys.pyi index 2f16e47edbf8..be1c7903c9c0 100644 --- a/stubs/cachetools/cachetools/keys.pyi +++ b/stubs/cachetools/cachetools/keys.pyi @@ -4,6 +4,6 @@ from collections.abc import Hashable __all__ = ("hashkey", "methodkey", "typedkey", "typedmethodkey") def hashkey(*args: Hashable, **kwargs: Hashable) -> tuple[Hashable, ...]: ... -def methodkey(self: Unused, *args: Hashable, **kwargs: Hashable) -> tuple[Hashable, ...]: ... +def methodkey(self: Unused, /, *args: Hashable, **kwargs: Hashable) -> tuple[Hashable, ...]: ... def typedkey(*args: Hashable, **kwargs: Hashable) -> tuple[Hashable, ...]: ... -def typedmethodkey(self: Unused, *args: Hashable, **kwargs: Hashable) -> tuple[Hashable, ...]: ... +def typedmethodkey(self: Unused, /, *args: Hashable, **kwargs: Hashable) -> tuple[Hashable, ...]: ... From ea350e1aebe3753bd6b3ff2eafe75dd3604fad0e Mon Sep 17 00:00:00 2001 From: Shamil Date: Tue, 23 Sep 2025 14:10:06 +0300 Subject: [PATCH 3/5] Add precise type annotations for cached decorator and helpers --- stubs/cachetools/cachetools/__init__.pyi | 51 +++++++++++++++++++++--- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/stubs/cachetools/cachetools/__init__.pyi b/stubs/cachetools/cachetools/__init__.pyi index 47db5e279956..bd3e14d39abc 100644 --- a/stubs/cachetools/cachetools/__init__.pyi +++ b/stubs/cachetools/cachetools/__init__.pyi @@ -2,15 +2,17 @@ from _typeshed import IdentityFunction, Unused from collections.abc import Callable, Iterator, MutableMapping, Sequence from contextlib import AbstractContextManager from threading import Condition -from typing import Any, TypeVar, overload +from typing import Any, Generic, Literal, NamedTuple, TypeVar, overload from typing_extensions import Self, deprecated __all__ = ("Cache", "FIFOCache", "LFUCache", "LRUCache", "RRCache", "TLRUCache", "TTLCache", "cached", "cachedmethod") + __version__: str _KT = TypeVar("_KT") _VT = TypeVar("_VT") _T = TypeVar("_T") +_R = TypeVar("_R") class Cache(MutableMapping[_KT, _VT]): @overload @@ -99,22 +101,61 @@ class TLRUCache(_TimedCache[_KT, _VT]): def ttu(self) -> Callable[[_KT, _VT, float], float]: ... def expire(self, time: float | None = None) -> list[tuple[_KT, _VT]]: ... +class _CacheInfo(NamedTuple): + hits: int + misses: int + maxsize: int | None + currsize: int + +class _cached_wrapper(Generic[_R]): + __wrapped__: Callable[..., _R] + def __call__(self, /, *args: Any, **kwargs: Any) -> _R: ... + +class _cached_wrapper_info(_cached_wrapper[_R]): + def cache_info(self) -> _CacheInfo: ... + def cache_clear(self) -> None: ... + @overload def cached( cache: MutableMapping[_KT, Any] | None, key: Callable[..., _KT] = ..., lock: AbstractContextManager[Any] | None = None, condition: Condition | None = None, - info: bool = False, -) -> IdentityFunction: ... + *, + info: Literal[True], +) -> Callable[[Callable[..., _R]], _cached_wrapper_info[_R]]: ... +@overload +def cached( + cache: MutableMapping[_KT, Any] | None, + key: Callable[..., _KT] = ..., + lock: AbstractContextManager[Any] | None = None, + condition: Condition | None = None, + *, + info: Literal[False] = False, +) -> Callable[[Callable[..., _R]], _cached_wrapper[_R]]: ... +@overload +def cached( # без параметра info (по умолчанию False) + cache: MutableMapping[_KT, Any] | None, + key: Callable[..., _KT] = ..., + lock: AbstractContextManager[Any] | None = None, + condition: Condition | None = None, +) -> Callable[[Callable[..., _R]], _cached_wrapper[_R]]: ... @overload @deprecated("Passing `info` as positional parameter is deprecated.") def cached( cache: MutableMapping[_KT, Any] | None, key: Callable[..., _KT] = ..., lock: AbstractContextManager[Any] | None = None, - condition: bool | None = None, -) -> IdentityFunction: ... + condition: Literal[True] = ..., +) -> Callable[[Callable[..., _R]], _cached_wrapper_info[_R]]: ... +@overload +@deprecated("Passing `info` as positional parameter is deprecated.") +def cached( + cache: MutableMapping[_KT, Any] | None, + key: Callable[..., _KT] = ..., + lock: AbstractContextManager[Any] | None = None, + condition: Literal[False] | None = ..., +) -> Callable[[Callable[..., _R]], _cached_wrapper[_R]]: ... def cachedmethod( cache: Callable[[Any], MutableMapping[_KT, Any] | None], key: Callable[..., _KT] = ..., From 80cf6f8ea6a6e8d5822d2e4a6e3c269e210cd4f8 Mon Sep 17 00:00:00 2001 From: Shamil Date: Tue, 23 Sep 2025 14:30:14 +0300 Subject: [PATCH 4/5] Make info parameter in cached overloads keyword-optional --- stubs/cachetools/cachetools/__init__.pyi | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/stubs/cachetools/cachetools/__init__.pyi b/stubs/cachetools/cachetools/__init__.pyi index bd3e14d39abc..8adfb544c640 100644 --- a/stubs/cachetools/cachetools/__init__.pyi +++ b/stubs/cachetools/cachetools/__init__.pyi @@ -6,7 +6,6 @@ from typing import Any, Generic, Literal, NamedTuple, TypeVar, overload from typing_extensions import Self, deprecated __all__ = ("Cache", "FIFOCache", "LFUCache", "LRUCache", "RRCache", "TLRUCache", "TTLCache", "cached", "cachedmethod") - __version__: str _KT = TypeVar("_KT") @@ -121,8 +120,7 @@ def cached( key: Callable[..., _KT] = ..., lock: AbstractContextManager[Any] | None = None, condition: Condition | None = None, - *, - info: Literal[True], + info: Literal[True] = ..., ) -> Callable[[Callable[..., _R]], _cached_wrapper_info[_R]]: ... @overload def cached( @@ -130,15 +128,7 @@ def cached( key: Callable[..., _KT] = ..., lock: AbstractContextManager[Any] | None = None, condition: Condition | None = None, - *, - info: Literal[False] = False, -) -> Callable[[Callable[..., _R]], _cached_wrapper[_R]]: ... -@overload -def cached( # без параметра info (по умолчанию False) - cache: MutableMapping[_KT, Any] | None, - key: Callable[..., _KT] = ..., - lock: AbstractContextManager[Any] | None = None, - condition: Condition | None = None, + info: Literal[False] = ..., ) -> Callable[[Callable[..., _R]], _cached_wrapper[_R]]: ... @overload @deprecated("Passing `info` as positional parameter is deprecated.") From 445d9dd1c71f297b2d3c721e35eaafaa3fa784f1 Mon Sep 17 00:00:00 2001 From: Shamil Date: Mon, 13 Oct 2025 14:14:43 +0300 Subject: [PATCH 5/5] Add test cases for cachetools stubs --- .../@tests/test_cases/check_cachetools.py | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 stubs/cachetools/@tests/test_cases/check_cachetools.py diff --git a/stubs/cachetools/@tests/test_cases/check_cachetools.py b/stubs/cachetools/@tests/test_cases/check_cachetools.py new file mode 100644 index 000000000000..9df9492bbf5b --- /dev/null +++ b/stubs/cachetools/@tests/test_cases/check_cachetools.py @@ -0,0 +1,104 @@ +from __future__ import annotations + +from collections.abc import Hashable +from typing import Any +from typing_extensions import assert_type + +from cachetools import LRUCache, cached, keys as cachekeys +from cachetools.func import fifo_cache, lfu_cache, lru_cache, rr_cache, ttl_cache + +# Tests for cachetools.cached + +# Explicitly parameterize the cache to avoid Unknown types +cache_inst: LRUCache[int, int] = LRUCache(maxsize=128) + + +@cached(cache_inst) +def check_cached(x: int) -> int: + return x * 2 + + +assert_type(check_cached(3), int) +# Methods cache_info/cache_clear are only present when info=True; do not access them here. + + +@cached(cache_inst, info=True) +def check_cached_with_info(x: int) -> int: + return x + 1 + + +assert_type(check_cached_with_info(4), int) +assert_type(check_cached_with_info.cache_info().misses, int) +check_cached_with_info.cache_clear() + + +# Tests for cachetools.func decorators + + +@lru_cache +def lru_noparens(x: int) -> int: + return x * 2 + + +@lru_cache(maxsize=32) +def lru_with_maxsize(x: int) -> int: + return x * 3 + + +assert_type(lru_noparens(3), int) +assert_type(lru_with_maxsize(3), int) +assert_type(lru_noparens.cache_info().hits, int) +assert_type(lru_with_maxsize.cache_info().misses, int) +assert_type(lru_with_maxsize.cache_parameters(), dict[str, Any]) +lru_with_maxsize.cache_clear() + + +@fifo_cache +def fifo_func(x: int) -> int: + return x + + +@lfu_cache +def lfu_func(x: int) -> int: + return x + + +@rr_cache +def rr_func(x: int) -> int: + return x + + +@ttl_cache +def ttl_func(x: int) -> int: + return x + + +assert_type(fifo_func(1), int) +assert_type(lfu_func(1), int) +assert_type(rr_func(1), int) +assert_type(ttl_func(1), int) +assert_type(fifo_func.cache_info().currsize, int) +assert_type(lfu_func.cache_parameters(), dict[str, Any]) + + +# Tests for cachetools.keys + +k1 = cachekeys.hashkey(1, "a") +assert_type(k1, tuple[Hashable, ...]) + + +class C: + def method(self, a: int) -> int: + return a + + +inst = C() + +k2 = cachekeys.methodkey(inst, 5) +assert_type(k2, tuple[Hashable, ...]) + +k3 = cachekeys.typedkey(1, "x") +assert_type(k3, tuple[Hashable, ...]) + +k4 = cachekeys.typedmethodkey(inst, 2) +assert_type(k4, tuple[Hashable, ...])