I raised this as a bug on the pyright bug tracker, but they said this is working as designed and closed the ticked… but I don’t understand why.
If I write a class that has a method that is a decorator, and I use functools.wraps
, then because of the generic type signature for functools.wraps, the decorated functions signature is “passed through”.
In other words, this works:
import functools
import typing as t
class WorkingStatefulDecorator:
def __init__(self, some_state):
self.some_state = some_state
def decorate(self, fn):
@functools.wraps(fn)
async def wrapper(*args, **kwargs):
return await fn(*args, **kwargs)
return wrapper
working_decorator = WorkingStatefulDecorator("some state")
# This interface technically not needed to show the problem, but it makes
# type checking fail in the broken example in the next code block
class MyInterface(t.Protocol):
async def foo(self, x: str) -> str:
...
class MyWorkingImpl(MyInterface):
# This is the bit I'm confused by:
# *this* produces a `foo` method with the correct signature
@working_decorator.decorate
async def foo(self, x: str) -> str:
return "bar"
print('MyWorkingImpl.foo')
t.reveal_type(MyWorkingImpl.foo)
Type checking with pyright passes, and it prints out that the type signature for the final foo
method is:
$ uv run pyright hmm.py
<snip>
Type of "MyWorkingImpl.foo" is "(self: MyWorkingImpl, x: str) -> Coroutine[Any, Any, str]"
My question then. If I change this code in one single way - I use __call__
as the decorator method instead of decorate
… then the type signature is lost, and type checking fails:
class BrokenStatefulDecorator:
def __init__(self, some_state):
self.some_state = some_state
def __call__(self, fn):
@functools.wraps(fn)
async def wrapper(*args, **kwargs):
return await fn(*args, **kwargs)
return wrapper
broken_decorator = BrokenStatefulDecorator("some state")
class MyBrokenImpl(MyInterface):
# This is the bit I'm confused by:
# *this* produces a `foo` method that doesn't have the expected signature
@broken_decorator
async def foo(self, x: str) -> str:
return "bar"
t.reveal_type(MyBrokenImpl.foo)
Now type checking fails, and the reveal_type call shows __call__
hasn’t had the wrapped type signature propagated the way decorate
did:
hmm.py:49:15 - error: "foo" overrides method of same name in class "MyInterface" with incompatible type "_Wrapped[..., Unknown, ..., Coroutine[Any, Any, Unknown]]" (reportIncompatibleMethodOverride)
hmm.py:54:15 - information: Type of "MyBrokenImpl.foo" is "_Wrapped[..., Unknown, ..., Coroutine[Any, Any, Unknown]]"
The pyright devs said this is because I need to set correct generic type signatures on the __call__
method… so I’m posting to ask why that is? Why does the type signature for functools.wraps work for “normal” methods but not for the call magic method?