[ty] propagate classmethod-ness through decorators returning Callables (#21958)
Fixes https://github.com/astral-sh/ty/issues/1787 ## Summary Allow method decorators returning Callables to presumptively propagate "classmethod-ness" in the same way that they already presumptively propagate "function-like-ness". We can't actually be sure that this is the case, based on the decorator's annotations, but (along with other type checkers) we heuristically assume it to be the case for decorators applied via decorator syntax. ## Test Plan Added mdtest.
This commit is contained in:
@@ -113,9 +113,7 @@ intention that it shouldn't influence the method's descriptor behavior. For exam
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
# TODO: this could use a generic signature, but we don't support
|
||||
# `ParamSpec` and solving of typevars inside `Callable` types yet.
|
||||
def memoize(f: Callable[[C1, int], str]) -> Callable[[C1, int], str]:
|
||||
def memoize[**P, R](f: Callable[P, R]) -> Callable[P, R]:
|
||||
raise NotImplementedError
|
||||
|
||||
class C1:
|
||||
@@ -259,4 +257,37 @@ reveal_type(MyClass().method) # revealed: (...) -> int
|
||||
reveal_type(MyClass().method.__name__) # revealed: str
|
||||
```
|
||||
|
||||
## classmethods passed through Callable-returning decorators
|
||||
|
||||
The behavior described above is also applied to classmethods. If a method is decorated with
|
||||
`@classmethod`, and also with another decorator which returns a Callable type, we make the
|
||||
assumption that the decorator returns a callable which still has the classmethod descriptor
|
||||
behavior.
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
def callable_identity[**P, R](func: Callable[P, R]) -> Callable[P, R]:
|
||||
return func
|
||||
|
||||
class C:
|
||||
@callable_identity
|
||||
@classmethod
|
||||
def f1(cls, x: int) -> str:
|
||||
return "a"
|
||||
|
||||
@classmethod
|
||||
@callable_identity
|
||||
def f2(cls, x: int) -> str:
|
||||
return "a"
|
||||
|
||||
# error: [too-many-positional-arguments]
|
||||
# error: [invalid-argument-type]
|
||||
C.f1(C, 1)
|
||||
C.f1(1)
|
||||
C().f1(1)
|
||||
C.f2(1)
|
||||
C().f2(1)
|
||||
```
|
||||
|
||||
[`tensorbase`]: https://github.com/pytorch/pytorch/blob/f3913ea641d871f04fa2b6588a77f63efeeb9f10/torch/_tensor.py#L1084-L1092
|
||||
|
||||
@@ -444,20 +444,18 @@ When a `@classmethod` is additionally decorated with another decorator, it is st
|
||||
class method:
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
def does_nothing[T](f: T) -> T:
|
||||
return f
|
||||
|
||||
class C:
|
||||
@classmethod
|
||||
@does_nothing
|
||||
def f1(cls: type[C], x: int) -> str:
|
||||
def f1(cls, x: int) -> str:
|
||||
return "a"
|
||||
|
||||
@does_nothing
|
||||
@classmethod
|
||||
def f2(cls: type[C], x: int) -> str:
|
||||
def f2(cls, x: int) -> str:
|
||||
return "a"
|
||||
|
||||
reveal_type(C.f1(1)) # revealed: str
|
||||
|
||||
Reference in New Issue
Block a user