From adb1fcc4178191b10099d537e9aef08698736890 Mon Sep 17 00:00:00 2001 From: KotlinIsland <65446343+kotlinisland@users.noreply.github.com> Date: Tue, 7 Jan 2025 23:48:57 +1000 Subject: [PATCH] fix `functools.cache` and `operator.attrgetter` --- CHANGELOG.md | 4 ++++ docs/source/based_features.rst | 14 ++++++++++++++ mypy/test/typetest/functools.py | 24 +++++++++++++----------- mypy/test/typetest/operator.py | 8 ++------ mypy/typeshed/stdlib/functools.pyi | 12 +++++++----- mypy/typeshed/stdlib/operator.pyi | 10 +++++----- 6 files changed, 45 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce6384b20..51fe10619 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Basedmypy Changelog +## [2.9.1] +### Fixed +- definition of `functools.cache` and `operator.attrgetter` + ## [2.9.0] ### Added - `collections.User*` should have `__repr__` diff --git a/docs/source/based_features.rst b/docs/source/based_features.rst index 4e385e44d..8d424836f 100644 --- a/docs/source/based_features.rst +++ b/docs/source/based_features.rst @@ -328,7 +328,21 @@ The defined type of a variable will be shown in the message for `reveal_type`: Typed ``functools.Cache`` ------------------------- +In mypy, ``functools.cache`` is unsafe: +.. code-block:: python + + @cache + def f(): ... + f(1, 2, 3) # no error + +This is resolved: + +.. code-block:: python + + @cache + def f(): ... + f(1, 2, 3) # error: expected no args Checked f-strings diff --git a/mypy/test/typetest/functools.py b/mypy/test/typetest/functools.py index 28c47c62d..9aa687adb 100644 --- a/mypy/test/typetest/functools.py +++ b/mypy/test/typetest/functools.py @@ -1,10 +1,10 @@ from __future__ import annotations -from functools import _lru_cache_wrapper, lru_cache +from functools import lru_cache # use `basedtyping` when we drop python 3.8 -from types import FunctionType, MethodType -from typing import TYPE_CHECKING, Callable, Protocol +from types import MethodType +from typing import TYPE_CHECKING, Callable from typing_extensions import assert_type from mypy_extensions import Arg @@ -15,31 +15,33 @@ class A: @cache - def m(self, a: int): ... + def m(self, a: list[int]): ... @classmethod @cache - def c(cls, a: int): ... + def c(cls, a: list[int]): ... @staticmethod @cache - def s(a: int): ... + def s(a: list[int]): ... @cache -def f(a: int): ... +def f(a: list[int]): ... if TYPE_CHECKING: from functools import _HashCallable, _LruCacheWrapperBase, _LruCacheWrapperMethod - a = A() - ExpectedFunction = _LruCacheWrapperBase[Callable[[Arg(int, "a")], None]] - ExpectedMethod = _LruCacheWrapperMethod[Callable[[Arg(int, "a")], None]] + ExpectedFunction = _LruCacheWrapperBase[Callable[[Arg(list[int], "a")], None]] + ExpectedMethod = _LruCacheWrapperMethod[Callable[[Arg(list[int], "a")], None]] ExpectedMethodNone = _LruCacheWrapperMethod["() -> None"] + a = A() + a.m([1]) # type: ignore[arg-type] assert_type(a.m, ExpectedMethod) assert_type(a.c, ExpectedMethod) # this is wrong, it shouldn't eat the `a` argument, but this is because of mypy `staticmethod` special casing assert_type(a.s, ExpectedMethodNone) - assert_type(a.s, MethodType & (_LruCacheWrapperBase[Callable[[Arg(int, "a")], None]] | _HashCallable)) # type: ignore[assert-type] + assert_type(a.s, MethodType & (_LruCacheWrapperBase[Callable[[Arg(list[int], "a")], None]] | _HashCallable)) # type: ignore[assert-type] assert_type(f.__get__(1), ExpectedMethodNone) + f([1]) # type: ignore[arg-type] diff --git a/mypy/test/typetest/operator.py b/mypy/test/typetest/operator.py index 416181539..fd1cec88d 100644 --- a/mypy/test/typetest/operator.py +++ b/mypy/test/typetest/operator.py @@ -1,10 +1,6 @@ -from operator import attrgetter, itemgetter -from typing import assert_type +from operator import attrgetter +from typing_extensions import assert_type def check_attrgetter(): assert_type(attrgetter("name"), attrgetter[object]) - - -def check_itemgetter(): - assert_type(itemgetter("name"), itemgetter[object]) diff --git a/mypy/typeshed/stdlib/functools.pyi b/mypy/typeshed/stdlib/functools.pyi index 5b4fc0593..8e5b6c6a1 100644 --- a/mypy/typeshed/stdlib/functools.pyi +++ b/mypy/typeshed/stdlib/functools.pyi @@ -58,9 +58,9 @@ class _HashCallable(Protocol): def __call__(self, /, *args: Hashable, **kwargs: Hashable) -> Never: ... @type_check_only -class _LruCacheWrapperBase(Protocol[_out_TCallable]): - __wrapped__: Final[_out_TCallable] = ... # type: ignore[misc] - __call__: Final[_out_TCallable | _HashCallable] = ... # type: ignore[misc] +class _LruCacheWrapperBase(Generic[_out_TCallable]): + __wrapped__: Final[_out_TCallable] # type: ignore[misc] + __call__: Final[_out_TCallable | _HashCallable] # type: ignore[misc] def cache_info(self) -> _CacheInfo: ... def cache_clear(self) -> None: ... @@ -70,10 +70,12 @@ class _LruCacheWrapperBase(Protocol[_out_TCallable]): def __copy__(self) -> Self: ... def __deepcopy__(self, memo: Any, /) -> Self: ... + # replace with `Method & X` once #856 is resolved @type_check_only -class _LruCacheWrapperMethod(MethodType, _LruCacheWrapperBase[_out_TCallable]): - pass +class _LruCacheWrapperMethod(MethodType, _LruCacheWrapperBase[_out_TCallable]): # type: ignore[misc] + __call__: Final[_out_TCallable | _HashCallable] # type: ignore[misc, assignment] + # actually defined in `_functools` @final diff --git a/mypy/typeshed/stdlib/operator.pyi b/mypy/typeshed/stdlib/operator.pyi index b73e037f3..6c104b362 100644 --- a/mypy/typeshed/stdlib/operator.pyi +++ b/mypy/typeshed/stdlib/operator.pyi @@ -183,15 +183,15 @@ if sys.version_info >= (3, 11): @final class attrgetter(Generic[_T_co]): @overload - def __new__(cls, attr: str, /) -> attrgetter[Any]: ... + def __new__(cls, attr: str, /) -> attrgetter[object]: ... @overload - def __new__(cls, attr: str, attr2: str, /) -> attrgetter[tuple[Any, Any]]: ... + def __new__(cls, attr: str, attr2: str, /) -> attrgetter[tuple[object, object]]: ... @overload - def __new__(cls, attr: str, attr2: str, attr3: str, /) -> attrgetter[tuple[Any, Any, Any]]: ... + def __new__(cls, attr: str, attr2: str, attr3: str, /) -> attrgetter[tuple[object, object, object]]: ... @overload - def __new__(cls, attr: str, attr2: str, attr3: str, attr4: str, /) -> attrgetter[tuple[Any, Any, Any, Any]]: ... + def __new__(cls, attr: str, attr2: str, attr3: str, attr4: str, /) -> attrgetter[tuple[object, object, object, object]]: ... @overload - def __new__(cls, attr: str, /, *attrs: str) -> attrgetter[tuple[Any, ...]]: ... + def __new__(cls, attr: str, /, *attrs: str) -> attrgetter[tuple[object, ...]]: ... def __call__(self, obj: Any, /) -> _T_co: ... @final