Skip to content

Commit a8d99fe

Browse files
committed
fix functools.cache and operator.attrgetter
1 parent 8cd497f commit a8d99fe

File tree

10 files changed

+101
-60
lines changed

10 files changed

+101
-60
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## [Unreleased]
44

5+
## [2.9.1]
6+
### Fixed
7+
- definition of `functools.cache` and `operator.attrgetter`
8+
59
## [2.9.0]
610
### Added
711
- `collections.User*` should have `__repr__`

docs/source/based_features.rst

+20
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,26 @@ The defined type of a variable will be shown in the message for `reveal_type`:
325325
reveal_type(a) # Revealed type is "int" (narrowed from "object")
326326
327327
328+
Typed ``functools.Cache``
329+
-------------------------
330+
331+
In mypy, ``functools.cache`` is unsafe:
332+
333+
.. code-block:: python
334+
335+
@cache
336+
def f(): ...
337+
f(1, 2, 3) # no error
338+
339+
This is resolved:
340+
341+
.. code-block:: python
342+
343+
@cache
344+
def f(): ...
345+
f(1, 2, 3) # error: expected no args
346+
347+
328348
Checked f-strings
329349
-----------------
330350

mypy/test/typetest/__init__.py

Whitespace-only changes.

mypy/test/typetest/functools.py

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from __future__ import annotations
2+
3+
from functools import lru_cache
4+
5+
# use `basedtyping` when we drop python 3.8
6+
from types import MethodType
7+
from typing import TYPE_CHECKING, Callable
8+
from typing_extensions import assert_type
9+
10+
from mypy_extensions import Arg
11+
12+
# use `functools.cache` when we drop python 3.8
13+
cache = lru_cache(None)
14+
15+
16+
class A:
17+
@cache
18+
def m(self, a: list[int]): ...
19+
20+
@classmethod
21+
@cache
22+
def c(cls, a: list[int]): ...
23+
24+
@staticmethod
25+
@cache
26+
def s(a: list[int]): ...
27+
28+
29+
@cache
30+
def f(a: list[int]): ...
31+
32+
33+
if TYPE_CHECKING:
34+
from functools import _HashCallable, _LruCacheWrapperBase, _LruCacheWrapperMethod
35+
36+
ExpectedFunction = _LruCacheWrapperBase[Callable[[Arg(list[int], "a")], None]]
37+
ExpectedMethod = _LruCacheWrapperMethod[Callable[[Arg(list[int], "a")], None]]
38+
ExpectedMethodNone = _LruCacheWrapperMethod["() -> None"]
39+
a = A()
40+
a.m([1]) # type: ignore[arg-type]
41+
assert_type(a.m, ExpectedMethod)
42+
assert_type(a.c, ExpectedMethod)
43+
# this is wrong, it shouldn't eat the `a` argument, but this is because of mypy `staticmethod` special casing
44+
assert_type(a.s, ExpectedMethodNone)
45+
assert_type(a.s, MethodType & (_LruCacheWrapperBase[Callable[[Arg(list[int], "a")], None]] | _HashCallable)) # type: ignore[assert-type]
46+
assert_type(f.__get__(1), ExpectedMethodNone)
47+
f([1]) # type: ignore[arg-type]

mypy/test/typetest/operator.py

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from operator import attrgetter
2+
from typing_extensions import assert_type
3+
4+
5+
def check_attrgetter():
6+
assert_type(attrgetter("name"), attrgetter[object])

mypy/test/typetest_functools.py

-43
This file was deleted.

mypy/typeshed/stdlib/builtins.pyi

+1
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ class staticmethod(Generic[_P, _R_co]):
140140
@property
141141
def __isabstractmethod__(self) -> bool: ...
142142
def __init__(self, f: Callable[_P, _R_co], /) -> None: ...
143+
# TODO: doesn't actually return `_NamedCallable`, it returns the callable that was passed to the constructor
143144
@overload
144145
def __get__(self, instance: None, owner: type, /) -> _NamedCallable[_P, _R_co]: ...
145146
@overload

mypy/typeshed/stdlib/functools.pyi

+16-12
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,9 @@ class _HashCallable(Protocol):
5858
def __call__(self, /, *args: Hashable, **kwargs: Hashable) -> Never: ...
5959

6060
@type_check_only
61-
class _LruCacheWrapperBase(Protocol[_out_TCallable]):
62-
__wrapped__: Final[_out_TCallable] = ... # type: ignore[misc]
63-
__call__: Final[_out_TCallable | _HashCallable] = ... # type: ignore[misc]
64-
61+
class _LruCacheWrapperBase(Generic[_out_TCallable]):
62+
__wrapped__: Final[_out_TCallable] # type: ignore[misc]
63+
__call__: Final[_out_TCallable | _HashCallable] # type: ignore[misc]
6564

6665
def cache_info(self) -> _CacheInfo: ...
6766
def cache_clear(self) -> None: ...
@@ -71,23 +70,28 @@ class _LruCacheWrapperBase(Protocol[_out_TCallable]):
7170
def __copy__(self) -> Self: ...
7271
def __deepcopy__(self, memo: Any, /) -> Self: ...
7372

74-
@final
73+
74+
# replace with `Method & X` once #856 is resolved
75+
@type_check_only
76+
class _LruCacheWrapperMethod(MethodType, _LruCacheWrapperBase[_out_TCallable]): # type: ignore[misc]
77+
__call__: Final[_out_TCallable | _HashCallable] # type: ignore[misc, assignment]
78+
79+
7580
# actually defined in `_functools`
81+
@final
7682
class _lru_cache_wrapper(_LruCacheWrapperBase[_out_TCallable]):
7783
def __init__(self, user_function: Never, maxsize: Never, typed: Never, cache_info_type: Never): ...
7884

79-
# TODO: reintroduce this once mypy 1.14 fork is merged
80-
# @overload
81-
# def __get__(self, instance: None, owner: object) -> Self: ...
82-
# @overload
85+
@overload
86+
def __get__(self, instance: None, owner: object) -> Self: ...
87+
@overload
8388
def __get__(
8489
self: _lru_cache_wrapper[Callable[Concatenate[Never, _PWrapped], _RWrapped]],
8590
instance: object,
8691
owner: type[object] | None = None,
8792
/,
88-
# ideally, we would capture the Callable here, and intersect with `MethodType`
89-
) -> _LruCacheWrapperBase[Callable[_PWrapped, _RWrapped]]: ...
90-
93+
# ideally we could capture the `Callable` to account for subtypes and intersect with `MethodType`
94+
) -> _LruCacheWrapperMethod[Callable[_PWrapped, _RWrapped]]: ...
9195

9296
@overload
9397
def lru_cache(maxsize: int | None = 128, typed: bool = False) -> FunctionType[[_TCallable], _lru_cache_wrapper[_TCallable]]: ...

mypy/typeshed/stdlib/operator.pyi

+5-5
Original file line numberDiff line numberDiff line change
@@ -183,15 +183,15 @@ if sys.version_info >= (3, 11):
183183
@final
184184
class attrgetter(Generic[_T_co]):
185185
@overload
186-
def __new__(cls, attr: str, /) -> attrgetter[Any]: ...
186+
def __new__(cls, attr: str, /) -> attrgetter[object]: ...
187187
@overload
188-
def __new__(cls, attr: str, attr2: str, /) -> attrgetter[tuple[Any, Any]]: ...
188+
def __new__(cls, attr: str, attr2: str, /) -> attrgetter[tuple[object, object]]: ...
189189
@overload
190-
def __new__(cls, attr: str, attr2: str, attr3: str, /) -> attrgetter[tuple[Any, Any, Any]]: ...
190+
def __new__(cls, attr: str, attr2: str, attr3: str, /) -> attrgetter[tuple[object, object, object]]: ...
191191
@overload
192-
def __new__(cls, attr: str, attr2: str, attr3: str, attr4: str, /) -> attrgetter[tuple[Any, Any, Any, Any]]: ...
192+
def __new__(cls, attr: str, attr2: str, attr3: str, attr4: str, /) -> attrgetter[tuple[object, object, object, object]]: ...
193193
@overload
194-
def __new__(cls, attr: str, /, *attrs: str) -> attrgetter[tuple[Any, ...]]: ...
194+
def __new__(cls, attr: str, /, *attrs: str) -> attrgetter[tuple[object, ...]]: ...
195195
def __call__(self, obj: Any, /) -> _T_co: ...
196196

197197
@final

mypy/typeshed/stdlib/types.pyi

+2
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ _P = ParamSpec("_P")
7373

7474
# Make sure this class definition stays roughly in line with `builtins.function`
7575
# This class is special-cased
76+
# is actually `builtins.function`
7677
@final
7778
class FunctionType:
7879
@property
@@ -430,6 +431,7 @@ class CoroutineType(Coroutine[_YieldT_co, _SendT_contra, _ReturnT_co]):
430431
if sys.version_info >= (3, 13):
431432
def __class_getitem__(cls, item: Any, /) -> Any: ...
432433

434+
# is actually `builtins.method`
433435
@final
434436
class MethodType:
435437
@property

0 commit comments

Comments
 (0)