From 9fb53ff311095fd82c1b4453a0732efe850a373f Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Tue, 23 Sep 2025 11:52:49 +0200 Subject: [PATCH 1/7] Use ParamSpec _lru_cache_wrapper Closes: #11280 --- stdlib/functools.pyi | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/stdlib/functools.pyi b/stdlib/functools.pyi index 47baf917294d..8929692a335d 100644 --- a/stdlib/functools.pyi +++ b/stdlib/functools.pyi @@ -25,7 +25,9 @@ __all__ = [ _T = TypeVar("_T") _T_co = TypeVar("_T_co", covariant=True) +_R = TypeVar("_R") _S = TypeVar("_S") +_P = ParamSpec("_P") _PWrapped = ParamSpec("_PWrapped") _RWrapped = TypeVar("_RWrapped") _PWrapper = ParamSpec("_PWrapper") @@ -54,19 +56,19 @@ class _CacheParameters(TypedDict): typed: bool @final -class _lru_cache_wrapper(Generic[_T]): - __wrapped__: Callable[..., _T] - def __call__(self, *args: Hashable, **kwargs: Hashable) -> _T: ... +class _lru_cache_wrapper(Generic[_P, _R]): + __wrapped__: Callable[_P, _R] + def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R: ... def cache_info(self) -> _CacheInfo: ... def cache_clear(self) -> None: ... def cache_parameters(self) -> _CacheParameters: ... - def __copy__(self) -> _lru_cache_wrapper[_T]: ... - def __deepcopy__(self, memo: Any, /) -> _lru_cache_wrapper[_T]: ... + def __copy__(self) -> _lru_cache_wrapper[_P, _R]: ... + def __deepcopy__(self, memo: Any, /) -> _lru_cache_wrapper[_P, _R]: ... @overload -def lru_cache(maxsize: int | None = 128, typed: bool = False) -> Callable[[Callable[..., _T]], _lru_cache_wrapper[_T]]: ... +def lru_cache(maxsize: int | None = 128, typed: bool = False) -> Callable[[Callable[_P, _R]], _lru_cache_wrapper[_P, _R]]: ... @overload -def lru_cache(maxsize: Callable[..., _T], typed: bool = False) -> _lru_cache_wrapper[_T]: ... +def lru_cache(maxsize: Callable[_P, _R], typed: bool = False) -> _lru_cache_wrapper[_P, _R]: ... if sys.version_info >= (3, 14): WRAPPER_ASSIGNMENTS: Final[ @@ -237,7 +239,7 @@ class cached_property(Generic[_T_co]): def __set__(self, instance: object, value: _T_co) -> None: ... # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... -def cache(user_function: Callable[..., _T], /) -> _lru_cache_wrapper[_T]: ... +def cache(user_function: Callable[_P, _R], /) -> _lru_cache_wrapper[_P, _R]: ... def _make_key( args: tuple[Hashable, ...], kwds: SupportsItems[Any, Any], From ea4ae219d1e99b32387a4e66f539d10378438501 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Tue, 23 Sep 2025 12:02:55 +0200 Subject: [PATCH 2/7] Revert functools changes --- stdlib/functools.pyi | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/stdlib/functools.pyi b/stdlib/functools.pyi index 8929692a335d..47baf917294d 100644 --- a/stdlib/functools.pyi +++ b/stdlib/functools.pyi @@ -25,9 +25,7 @@ __all__ = [ _T = TypeVar("_T") _T_co = TypeVar("_T_co", covariant=True) -_R = TypeVar("_R") _S = TypeVar("_S") -_P = ParamSpec("_P") _PWrapped = ParamSpec("_PWrapped") _RWrapped = TypeVar("_RWrapped") _PWrapper = ParamSpec("_PWrapper") @@ -56,19 +54,19 @@ class _CacheParameters(TypedDict): typed: bool @final -class _lru_cache_wrapper(Generic[_P, _R]): - __wrapped__: Callable[_P, _R] - def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R: ... +class _lru_cache_wrapper(Generic[_T]): + __wrapped__: Callable[..., _T] + def __call__(self, *args: Hashable, **kwargs: Hashable) -> _T: ... def cache_info(self) -> _CacheInfo: ... def cache_clear(self) -> None: ... def cache_parameters(self) -> _CacheParameters: ... - def __copy__(self) -> _lru_cache_wrapper[_P, _R]: ... - def __deepcopy__(self, memo: Any, /) -> _lru_cache_wrapper[_P, _R]: ... + def __copy__(self) -> _lru_cache_wrapper[_T]: ... + def __deepcopy__(self, memo: Any, /) -> _lru_cache_wrapper[_T]: ... @overload -def lru_cache(maxsize: int | None = 128, typed: bool = False) -> Callable[[Callable[_P, _R]], _lru_cache_wrapper[_P, _R]]: ... +def lru_cache(maxsize: int | None = 128, typed: bool = False) -> Callable[[Callable[..., _T]], _lru_cache_wrapper[_T]]: ... @overload -def lru_cache(maxsize: Callable[_P, _R], typed: bool = False) -> _lru_cache_wrapper[_P, _R]: ... +def lru_cache(maxsize: Callable[..., _T], typed: bool = False) -> _lru_cache_wrapper[_T]: ... if sys.version_info >= (3, 14): WRAPPER_ASSIGNMENTS: Final[ @@ -239,7 +237,7 @@ class cached_property(Generic[_T_co]): def __set__(self, instance: object, value: _T_co) -> None: ... # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... -def cache(user_function: Callable[_P, _R], /) -> _lru_cache_wrapper[_P, _R]: ... +def cache(user_function: Callable[..., _T], /) -> _lru_cache_wrapper[_T]: ... def _make_key( args: tuple[Hashable, ...], kwds: SupportsItems[Any, Any], From e3f2bee99a0f409db04d1addf3d416f9b24f4f36 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Tue, 23 Sep 2025 12:08:00 +0200 Subject: [PATCH 3/7] Add a few tests for @cached --- stdlib/@tests/test_cases/check_functools.py | 25 ++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/stdlib/@tests/test_cases/check_functools.py b/stdlib/@tests/test_cases/check_functools.py index 47b6bb77c25d..f7ad97c3a6dd 100644 --- a/stdlib/@tests/test_cases/check_functools.py +++ b/stdlib/@tests/test_cases/check_functools.py @@ -1,12 +1,15 @@ from __future__ import annotations -from functools import cached_property, wraps +from functools import cache, cached_property, wraps from typing import Callable, TypeVar from typing_extensions import ParamSpec, assert_type P = ParamSpec("P") T_co = TypeVar("T_co", covariant=True) +# +# Tests for @wraps +# def my_decorator(func: Callable[P, T_co]) -> Callable[P, T_co]: @wraps(func) @@ -53,6 +56,26 @@ def func_wrapper(x: int) -> None: ... # Wrapper().method(3) func_wrapper(3) +# +# Tests for @cache +# + +@cache +def check_cached(x: int) -> int: + return x * 2 + + +assert_type(check_cached(3), int) +# Type checkers should check the argument type, but this is currently not +# possible. See https://github.com/python/typeshed/issues/6347 and +# https://github.com/python/typeshed/issues/11280. +# check_cached("invalid") # type: ignore + +assert_type(check_cached.cache_info().misses, int) + +# +# Tests for @cached_property +# class A: def __init__(self, x: int): From 2b330751836c06289cd9fa1aa2d420ffbffea051 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 23 Sep 2025 10:10:36 +0000 Subject: [PATCH 4/7] [pre-commit.ci] auto fixes from pre-commit.com hooks --- stdlib/@tests/test_cases/check_functools.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/stdlib/@tests/test_cases/check_functools.py b/stdlib/@tests/test_cases/check_functools.py index f7ad97c3a6dd..f407aafa7b31 100644 --- a/stdlib/@tests/test_cases/check_functools.py +++ b/stdlib/@tests/test_cases/check_functools.py @@ -11,6 +11,7 @@ # Tests for @wraps # + def my_decorator(func: Callable[P, T_co]) -> Callable[P, T_co]: @wraps(func) def wrapper(*args: P.args, **kwargs: P.kwargs) -> T_co: @@ -56,10 +57,12 @@ def func_wrapper(x: int) -> None: ... # Wrapper().method(3) func_wrapper(3) + # # Tests for @cache # + @cache def check_cached(x: int) -> int: return x * 2 @@ -77,6 +80,7 @@ def check_cached(x: int) -> int: # Tests for @cached_property # + class A: def __init__(self, x: int): self.x = x From 72db8975db4997eb2b27179111ad0fbaac8299f9 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Tue, 23 Sep 2025 12:11:37 +0200 Subject: [PATCH 5/7] Disable "type ignore" in comment --- stdlib/@tests/test_cases/check_functools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stdlib/@tests/test_cases/check_functools.py b/stdlib/@tests/test_cases/check_functools.py index f407aafa7b31..1ee4a718bdc3 100644 --- a/stdlib/@tests/test_cases/check_functools.py +++ b/stdlib/@tests/test_cases/check_functools.py @@ -72,7 +72,7 @@ def check_cached(x: int) -> int: # Type checkers should check the argument type, but this is currently not # possible. See https://github.com/python/typeshed/issues/6347 and # https://github.com/python/typeshed/issues/11280. -# check_cached("invalid") # type: ignore +# check_cached("invalid") # xtype: ignore assert_type(check_cached.cache_info().misses, int) From cd60ff82218d941cab91c988fe966e1b4889caec Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Tue, 23 Sep 2025 12:14:56 +0200 Subject: [PATCH 6/7] Add @lru_cache tests --- stdlib/@tests/test_cases/check_functools.py | 34 ++++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/stdlib/@tests/test_cases/check_functools.py b/stdlib/@tests/test_cases/check_functools.py index 1ee4a718bdc3..7e36708954d6 100644 --- a/stdlib/@tests/test_cases/check_functools.py +++ b/stdlib/@tests/test_cases/check_functools.py @@ -1,6 +1,6 @@ from __future__ import annotations -from functools import cache, cached_property, wraps +from functools import cache, cached_property, lru_cache, wraps from typing import Callable, TypeVar from typing_extensions import ParamSpec, assert_type @@ -64,17 +64,43 @@ def func_wrapper(x: int) -> None: ... @cache -def check_cached(x: int) -> int: +def check_cache(x: int) -> int: return x * 2 -assert_type(check_cached(3), int) +assert_type(check_cache(3), int) # Type checkers should check the argument type, but this is currently not # possible. See https://github.com/python/typeshed/issues/6347 and # https://github.com/python/typeshed/issues/11280. # check_cached("invalid") # xtype: ignore -assert_type(check_cached.cache_info().misses, int) +assert_type(check_cache.cache_info().misses, int) + + +# +# Tests for @lru_cache +# + + +@lru_cache +def check_lru_cache(x: int) -> int: + return x * 2 + +@lru_cache(maxsize=32) +def check_lru_cache_with_maxsize(x: int) -> int: + return x * 2 + +assert_type(check_lru_cache(3), int) +assert_type(check_lru_cache_with_maxsize(3), int) +# Type checkers should check the argument type, but this is currently not +# possible. See https://github.com/python/typeshed/issues/6347 and +# https://github.com/python/typeshed/issues/11280. +# check_lru_cache("invalid") # xtype: ignore +# check_lru_cache_with_maxsize("invalid") # xtype: ignore + +assert_type(check_lru_cache.cache_info().misses, int) +assert_type(check_lru_cache_with_maxsize.cache_info().misses, int) + # # Tests for @cached_property From 319c3bb3e438af8d0dde920fca71e6dde3d6b4d3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 23 Sep 2025 10:16:46 +0000 Subject: [PATCH 7/7] [pre-commit.ci] auto fixes from pre-commit.com hooks --- stdlib/@tests/test_cases/check_functools.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/stdlib/@tests/test_cases/check_functools.py b/stdlib/@tests/test_cases/check_functools.py index 7e36708954d6..3e37802bd591 100644 --- a/stdlib/@tests/test_cases/check_functools.py +++ b/stdlib/@tests/test_cases/check_functools.py @@ -86,10 +86,12 @@ def check_cache(x: int) -> int: def check_lru_cache(x: int) -> int: return x * 2 + @lru_cache(maxsize=32) def check_lru_cache_with_maxsize(x: int) -> int: return x * 2 + assert_type(check_lru_cache(3), int) assert_type(check_lru_cache_with_maxsize(3), int) # Type checkers should check the argument type, but this is currently not