I was reading PEP-612 and it makes typing a decorator fairly tractable. Also, the example provided in the PEP makes it look pretty easy. This example is copied directly from the PEP:
from typing import ParamSpec, TypeVar
from collections.abc import Callable, Awaitable
P = ParamSpec("P")
R = TypeVar("R")
def add_logging(f: Callable[P, R]) -> Callable[P, Awaitable[R]]:
async def inner(*args: P.args, **kwargs: P.kwargs) -> R:
await log_to_database()
return f(*args, **kwargs)
return inner
@add_logging
def takes_int_str(x: int, y: str) -> int:
return x + 7
await takes_int_str(1, "A") # Accepted
await takes_int_str("B", 2) # Correctly rejected by the type checker
However, I found it non-trivial to properly type annotate a parametrizable decorator. Check the following MRE:
import functools
import inspect
from collections.abc import Callable, Coroutine
from typing import ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
def tag(*names: str) -> ??:
"""This decorator just tags an async function with the provided name(s)."""
for name in names:
if not isinstance(name, str):
raise TypeError("tag name must be a string")
def outer(func: Callable[P, R]) -> Callable[P, Coroutine[R]]:
func.tags = names
if not inspect.iscoroutinefunction(func):
raise TypeError("tagged function must be an async function")
@functools.wraps(func)
async def inner(*args: P.args, **kwargs: P.kwargs) -> R:
result = await func(*args, **kwargs)
return result
return inner
return outer
I'm struggling with figuring out the return type of the tag
function. Also, I'm not 100% confident with the correctness of the typing of outer
and inner
nested functions. How do I type this properly?
P.S. I know, as of today, mypy 0.902 doesn't support this feature fully yet.
First thing to note is that your parametrized decorator example isn't just the PEP's decorator example plus parametrization. Instead, your second example's decorator (after parametrization) takes a asynchronous function, whereas the PEP's example takes a synchronous function.
Because you are await
ing the func
's result directly, unlike the PEP's example which await
ed a separate logger followed by calling f
normally, your outer
needs to take a Callable[[P], Awaitable[R]]
instead of Callable[[P], R]
.
Second thing to note is that regarding tag
s return type, you can figure it out by adding reveal_type(outer)
, which would in turn be the return type of tag
. I haven't run this (because of mypy
not actually supporting your example yet), but it should say Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[R]]]
. In other words, tag
returns a decorator that itself takes an asynchronous function and returns an asynchronous function.
Third thing to note is that you probably want Awaitable[T]
in all your examples (which is why I've been using it myself throughout the above) instead of Coroutine[T]
. This is because a) Coroutine
takes three type parameters instead of one (so you'd have to use Coroutine[Any, Any, T]
instead of Coroutine[T]
, where the first two type parameters are for send) and b) Coroutine
is a subtype of Awaitable
with the added support of send
ing, which you don't utilize anyway.