The motivation for this is to type check event handlers, to ensure matching between the types that the registered events expect as arguments and those the handler is primed to give.
I am trying to track function signatures in type annotations for a class based function decorator. This is just a mypy stubs project: the actual implementation will get the same result in a different way.
So, we have a basic decorator skeleton like so
from typing import Any, Callable, Generic, TypeVar
FuncT = TypeVar("FuncT", bound=Callable)
class decorator(Generic[FuncT]):
def __init__(self, method: FuncT) -> None:
... # Allows mypy to infer the parameter type
__call__: FuncT
execute: FuncT
With the following stub example
class Widget:
def bar(self: Any, a: int) -> int:
...
@decorator
def foo(self: Any, a: int) -> int:
...
w = Widget()
reveal_type(Widget.bar)
reveal_type(w.bar)
reveal_type(Widget.foo.__call__)
reveal_type(w.foo.__call__)
The revealed types are as follows:
Widget.bar (undecorated class method): 'def (self: demo.Widget, a: builtins.int) -> builtins.int'
w.bar (undecorated instance method): 'def (a: builtins.int) -> builtins.int'
Widget.foo.__call__ (decorated class method): 'def (self: demo.Widget, a: builtins.int) -> builtins.int'
w.foo.__call__ (decorated instance method): 'def (self: demo.Widget, a: builtins.int) -> builtins.int'
The implication of this is that if I call w.bar(2) it passes the type checker, but if I call w.foo(2) or w.foo.execute(2) then mypy complains that there aren't enough parameters. Meanwhile all of Widget.bar(w, 2) Widget.foo(w, 2), and Widget.foo.execute(w, 2) pass fine.
What I'm after is a way to annotate this to persuade w.foo.__call__ and w.foo.execute to give the same signature as w.bar.
This is now possible using ParamSpec from PEP 612. It also requires an intermediate class overloading __get__ to distinguish class from instance access.
FuncT = TypeVar("FuncT", bound=Callable)
FuncT2 = TypeVar("FuncT2", bound=Callable)
SelfT = TypeVar("SelfT")
ParamTs = ParamSpec("ParamTs")
R = TypeVar("R")
class DecoratorCallable(Generic[FuncT]):
__call__ : FuncT
# FuncT and FuncT2 refer to the method signature with and without self
class DecoratorBase(Generic[FuncT, FuncT2]):
@overload
def __get__(self, instance: None, owner: object) -> DecoratorCallable[FuncT]:
# when a method is accessed directly, instance will be None
...
@overload
def __get__(self, instance: object, owner: object) -> DecoratorCallable[FuncT2]:
# when a method is accessed through an instance, instance will be that object
...
def __get__(self, instance: Optional[object], owner: object) -> DecoratorCallable:
...
def decorator(f: Callable[Concatenate[SelfT, ParamTs], R]) -> DecoratorBase[Callable[Concatenate[SelfT, ParamTs], R], Callable[ParamTs, R]] :
...
class Widget:
def bar(self: Any, a: int) -> int:
...
@decorator
def foo(self: Any, a: int) -> int:
...
With the same class as before, the revealed types are now
Widget.bar (undecorated class method): 'def (self: Any, a: builtins.int) -> builtins.int'
w.bar (undecorated instance method): 'def (a: builtins.int) -> builtins.int'
Widget.foo.__call__ (decorated class method): 'def (Any, a: builtins.int) -> builtins.int'
w.foo.__call__ (decorated instance method): 'def (a: builtins.int) -> builtins.int'
This means that MyPy will correctly allow Widget.foo(w, 2) and w.foo(2), and will correctly disallow Widget.foo(w, "A"), w.foo("A"), w.foo(2, 5), and x: dict = w.foo(2). It also allows for keyword arguments; w.foo(a=2) passes.
One corner case that it fails on is that it forgets about the name of self and so Widget.foo(self=w, a = 2) fails with Unexpected keyword argument "self".
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With