Skip to content

Commit

Permalink
fix functools.cache and operator.attrgetter
Browse files Browse the repository at this point in the history
  • Loading branch information
KotlinIsland committed Jan 8, 2025
1 parent 8cd497f commit a8d99fe
Show file tree
Hide file tree
Showing 10 changed files with 101 additions and 60 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

## [2.9.1]
### Fixed
- definition of `functools.cache` and `operator.attrgetter`

## [2.9.0]
### Added
- `collections.User*` should have `__repr__`
Expand Down
20 changes: 20 additions & 0 deletions docs/source/based_features.rst
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,26 @@ The defined type of a variable will be shown in the message for `reveal_type`:
reveal_type(a) # Revealed type is "int" (narrowed from "object")
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
-----------------

Expand Down
Empty file added mypy/test/typetest/__init__.py
Empty file.
47 changes: 47 additions & 0 deletions mypy/test/typetest/functools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from __future__ import annotations

from functools import lru_cache

# use `basedtyping` when we drop python 3.8
from types import MethodType
from typing import TYPE_CHECKING, Callable
from typing_extensions import assert_type

from mypy_extensions import Arg

# use `functools.cache` when we drop python 3.8
cache = lru_cache(None)


class A:
@cache
def m(self, a: list[int]): ...

@classmethod
@cache
def c(cls, a: list[int]): ...

@staticmethod
@cache
def s(a: list[int]): ...


@cache
def f(a: list[int]): ...


if TYPE_CHECKING:
from functools import _HashCallable, _LruCacheWrapperBase, _LruCacheWrapperMethod

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(list[int], "a")], None]] | _HashCallable)) # type: ignore[assert-type]
assert_type(f.__get__(1), ExpectedMethodNone)
f([1]) # type: ignore[arg-type]
6 changes: 6 additions & 0 deletions mypy/test/typetest/operator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from operator import attrgetter
from typing_extensions import assert_type


def check_attrgetter():
assert_type(attrgetter("name"), attrgetter[object])
43 changes: 0 additions & 43 deletions mypy/test/typetest_functools.py

This file was deleted.

1 change: 1 addition & 0 deletions mypy/typeshed/stdlib/builtins.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ class staticmethod(Generic[_P, _R_co]):
@property
def __isabstractmethod__(self) -> bool: ...
def __init__(self, f: Callable[_P, _R_co], /) -> None: ...
# TODO: doesn't actually return `_NamedCallable`, it returns the callable that was passed to the constructor
@overload
def __get__(self, instance: None, owner: type, /) -> _NamedCallable[_P, _R_co]: ...
@overload
Expand Down
28 changes: 16 additions & 12 deletions mypy/typeshed/stdlib/functools.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +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: ...
Expand All @@ -71,23 +70,28 @@ class _LruCacheWrapperBase(Protocol[_out_TCallable]):
def __copy__(self) -> Self: ...
def __deepcopy__(self, memo: Any, /) -> Self: ...

@final

# replace with `Method & X` once #856 is resolved
@type_check_only
class _LruCacheWrapperMethod(MethodType, _LruCacheWrapperBase[_out_TCallable]): # type: ignore[misc]
__call__: Final[_out_TCallable | _HashCallable] # type: ignore[misc, assignment]


# actually defined in `_functools`
@final
class _lru_cache_wrapper(_LruCacheWrapperBase[_out_TCallable]):
def __init__(self, user_function: Never, maxsize: Never, typed: Never, cache_info_type: Never): ...

# TODO: reintroduce this once mypy 1.14 fork is merged
# @overload
# def __get__(self, instance: None, owner: object) -> Self: ...
# @overload
@overload
def __get__(self, instance: None, owner: object) -> Self: ...
@overload
def __get__(
self: _lru_cache_wrapper[Callable[Concatenate[Never, _PWrapped], _RWrapped]],
instance: object,
owner: type[object] | None = None,
/,
# ideally, we would capture the Callable here, and intersect with `MethodType`
) -> _LruCacheWrapperBase[Callable[_PWrapped, _RWrapped]]: ...

# ideally we could capture the `Callable` to account for subtypes and intersect with `MethodType`
) -> _LruCacheWrapperMethod[Callable[_PWrapped, _RWrapped]]: ...

@overload
def lru_cache(maxsize: int | None = 128, typed: bool = False) -> FunctionType[[_TCallable], _lru_cache_wrapper[_TCallable]]: ...
Expand Down
10 changes: 5 additions & 5 deletions mypy/typeshed/stdlib/operator.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions mypy/typeshed/stdlib/types.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ _P = ParamSpec("_P")

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

# is actually `builtins.method`
@final
class MethodType:
@property
Expand Down

0 comments on commit a8d99fe

Please sign in to comment.