Description
Decorators currently stress mypy's support for functional programming to the point that many decorators are impossible to type. I'm intending this issue as a project plan and exploration of the issue of decorators and how to type them. It's a collection point for problems or incompletenesses that keep decorators from being fully supported, plans for solutions to those problems, and fully-implemented solutions to those problems.
Decorators that can only decorate a function of a fixed signature
You can define your decorator's signature explicitly, and it's completely supported:
from typing import Callable, Tuple
def with_strlen(f: Callable[[int], str]) -> Callable[[int], Tuple[str, int]]:
def ret(__i: int) -> Tuple[str, int]:
r = f(__i)
return r, len(r)
return ret
@with_strlen
def lol(x: int) -> str:
return "lol"*x
reveal_type(lol) # E: Revealed type is 'def (builtins.int) -> Tuple[builtins.str, builtins.int]
Notes:
- You can even use type variables to get some flexibility in argument and return types, but this falls over as soon as you don't know the exact number of arguments to expect.
- Your decorated function's arguments can't be called by name.
Decorators that do not change the signature of the function
Nearly fully supported. Here's how you do it:
from typing import TypeVar, Callable, cast
T = TypeVar('T')
def print_callcount(f: T) -> T:
x = 0
def ret(*args, **kwargs):
nonlocal x
x += 1
print("%d calls so far" % x)
return f(*args, **kwargs)
return cast(T, ret)
@print_callcount
def lol(x: int) -> str:
return "lol"*x
reveal_type(lol) # E: Revealed type is 'def (x: builtins.int) -> builtins.str'
Notes:
- Mypy trusts that you can call
f
. You can set a bound onT
to beCallable
, but you don't need to for it to typecheck. - This business doesn't typecheck without the cast. That's a symptom of the fact that mypy doesn't understand the function nature of the argument to
print_callcount
, and there's no way to declare that argument as aCallable
explicitly without losing the argument type oflol
later. We'd like to minimize the places where casts are required. Doing so requires something along the lines of "variadic argument variables", discussed below.
Decorators that take arguments ("second-order"?)
Plenty of decorators "take arguments" by actually being functions that return a decorator. For example, we'd like to be able to do this:
from typing import Any, TypeVar, Callable, cast
T = TypeVar('T')
def callback_callcount(cb: Callable[[int], None]) -> Callable[[T], T]:
def outer(f: T) -> T:
x = 0
def inner(*args, **kwargs):
nonlocal x
x += 1
cb(x)
return f(*args, **kwargs)
return cast(T, inner)
return outer
def print_int(x: int) -> None:
print(x)
@callback_callcount(print_int)
def lol(x: int) -> str:
return "lol"*x
reveal_type(lol) # E: Revealed type is 'def (x: builtins.int) -> builtins.str'
Notes:
- This does not typecheck yet -- errors on calling the decorator, and
lol
ends up typed asNone
- Relevant issue: Function returning a generic function #1551
- Still has a non-ideal cast...
Mess with the return type or with arguments
For an arbitrary function you can't do this at all yet -- there isn't even a syntax. Here's me making up some syntax for it.
Messing with the return type
from typing import Any, Dict, Callable
from mypy_extensions import SomeArguments
def reprify(f: Callable[[SomeArguments], Any]) -> Callable[[SomeArguments], str]:
def ret(*args: SomeArguments.positional, **kwargs: SomeArguments.keyword):
return repr(f(*args, **kwargs))
return ret
@reprify
def lol(x: int) -> Dict[str, int]:
return {"lol": x}
reveal_type(lol) # E: Revealed type is 'def (x: builtins.int) -> builtins.str'
Messing with the arguments
from typing import Any, Callable, TypeVar
from mypy_extensions import SomeArguments
R = TypeVar('R')
def supply_zero(f: Callable[[int, SomeArguments], R]) -> Callable[[SomeArguments], R]:
def ret(*args: SomeArguments.positional, **kwargs: SomeArguments.keyword):
return f(0, *args, **kwargs)
return ret
@supply_zero
def lol(x: int, y: str) -> str:
return "%d and %s" % (x, y)
reveal_type(lol) # E: Revealed type is 'def (y: builtins.str) -> builtins.str'
The syntax here is fungible, but we would need a way to do approximately this thing -- capture the types and kinds of all a function's arguments in some kind of variation on a type variable.
Relevant issues and discussions:
- Making a decorator which preserves function signature #1927
- TypeVar to represent a Callable's arguments #3028
- Make Callable more flexible typing#239
- Allow variadic generics typing#193
Variadic type variables alone (python/typing#193) get you some of the way there, but lose all keyword arguments of the decorated function.
Things to do:
- Implement variadic type variables (fill in PR when I have it)
- Write up detailed proposal for semantics of argument variables
- ... and how they interact with
*args
and**kwargs
- ... and their relationship to variadic type variables and the expand operation
- ... and the semantics of an easy-to-use
SomeArguments
-style alias, so nobody has to actually engage with the details of the above when writing normal decorators.
- ... and how they interact with
- Come to some kind of mypy-community consensus or near-consensus on that proposal. It'll be in
mypy_extensions
nottyping
at first -- this can be fodder for the future of PEP484, but while we're playing in such experimental land, not yet. - PR to implment the
SomeArguments
thing.