In mypy the decorators are literally removed. EG in mypy:
elif refers_to_fullname(d, "builtins.classmethod"):
removed.append(i)
and later
for i in reversed(removed):
del dec.decorators[i]
Then special handling is applied elsewhere for accessing those classmethods/etc. This is fine in most cases. Until it’s combined with another decorator that doesn’t directly return the function type, but instead returns a class with a __call__
method. In that case the special handling in mypy breaks and a callable with the full original call signature is returned, even though at runtime the first cls
parameter has been removed. To demonstrate expected behavior -
If the classmethod
definition in typeshed is changed to:
_Class_co = TypeVar("_Class_co", bound=Callable[Concatenate[type[Any], ...], Any], covariant=True)
class classmethod(Generic[_Class_co]):
@property
def __func__(self) -> _Class_co: ...
@property
def __isabstractmethod__(self) -> bool: ...
def __init__(self, f: _Class_co, /) -> None: ...
@overload
def __get__(self: classmethod[Callable[Concatenate[type[_T], _P], _R_co]], instance: None, owner: type[_T], /) -> types.MethodType[_P, _R_co, classmethod[_Class_co]]: ...
@overload
def __get__(self: classmethod[Callable[Concatenate[type[_T], _P], _R_co]], instance: _T, owner: type[_T] | None = None, /) -> types.MethodType[_P, _R_co, classmethod[_Class_co]]: ...
if sys.version_info >= (3, 10):
__name__: str
__qualname__: str
@property
def __wrapped__(self) -> _Class_co: ...
Then typing.MethodType
:
_W = TypeVar("_W")
@final
class MethodType(Generic[_P, _R_co, _W]):
@property
def __closure__(self) -> tuple[CellType, ...] | None: ... # inherited from the added function
@property
def __code__(self) -> CodeType: ... # inherited from the added function
@property
def __defaults__(self) -> tuple[Any, ...] | None: ... # inherited from the added function
@property
def __func__(self) -> _W: ...
@property
def __self__(self) -> object: ...
@property
def __name__(self) -> str: ... # inherited from the added function
@property
def __qualname__(self) -> str: ... # inherited from the added function
def __new__(cls, func: Callable[..., Any], obj: object, /) -> Self: ...
def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R_co: ...
def __eq__(self, value: object, /) -> bool: ...
def __hash__(self) -> int: ...
def __get__(self, instance: object, owner: type[Any] | None = None,/) -> Self:...
Then what that says is that when a classmethod
is in the type dictionary wrapping a callable (any callable - not just a function defined with def
), when accessed via either the class or an instance, it returns a MethodType
which has a call signature with one less parameter than the wrapped callable.
If functools._lru_cache_wrapper
is changed to:
_C = TypeVar("_C", bound=Callable[..., Any])
_P = ParamSpec("_P")
_R_co = TypeVar("_R_co", covariant=True)
_Cache: TypeAlias = _lru_cache_wrapper[Callable[_P, _R_co]]
_MCache: TypeAlias = _lru_cache_wrapper[Callable[Concatenate[_T, _P], _R_co]]
@final
class _lru_cache_wrapper(Generic[_C]):
__wrapped__: _C
def __call__(self: _Cache[_P, _R_co], *args: _P.args, **kwargs: _P.kwargs) -> _R_co: ...
def cache_info(self) -> _CacheInfo: ...
def cache_clear(self) -> None: ...
if sys.version_info >= (3, 9):
def cache_parameters(self) -> _CacheParameters: ...
def __copy__(self) -> Self: ...
def __deepcopy__(self, memo: Any, /) -> Self: ...
@overload
def __get__(self, instance: None, owner: type[Any], /) -> Self: ...
@overload
def __get__(self: _MCache[_T, _P, _R_co], instance: _T, owner: type[_T] | None = None, /) -> types.MethodType[_P, _R_co, _lru_cache_wrapper[_C]]: ...
Then if it wraps a standard function it implements the same descriptor protocol as a function
. It’s also callable, however and so if combined with classmethod
:
class MyClass:
@classmethod
@cache
def fn(cls, arg: int, arg2: str) -> str: ...
When the above fn
is accessed through either an instance of the class or the type of the class it should return a Callable
that takes an int
and a str
. Currently, mypy
doesn’t implement this and I don’t think other type checkers do either (I’ve tried using pyright
with the above annotations and didn’t get expected results).
If the above behavior was implemented this would mean that functools.cache
wrapped functions would have correct call signatures (something that people keep asking for) as well as having no false positives for access to _lru_cache_wrapper.cache_clear
, a strong requirement of the typeshed repo devs.
I may be wrong in some way (or many ways), please correct me if so, I have a very good understanding of the c++ type system (including variadic templates), but most of my understanding of the python type system is based on guesses that it works similarly to the c++ type system combined with reading documentation.
(edits - using the right class defs, apologies)