Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python 3 type hinting for decorator

Consider the following code:

from typing import Callable, Any  TFunc = Callable[..., Any]  def get_authenticated_user(): return "John"  def require_auth() -> Callable[TFunc, TFunc]:     def decorator(func: TFunc) -> TFunc:         def wrapper(*args, **kwargs) -> Any:             user = get_authenticated_user()             if user is None:                 raise Exception("Don't!")             return func(*args, **kwargs)         return wrapper     return decorator  @require_auth() def foo(a: int) -> bool:     return bool(a % 2)  foo(2)      # Type check OK foo("no!")  # Type check failing as intended 

This piece of code is working as intended. Now imagine I want to extend this, and instead of just executing func(*args, **kwargs) I want to inject the username in the arguments. Therefore, I modify the function signature.

from typing import Callable, Any  TFunc = Callable[..., Any]  def get_authenticated_user(): return "John"  def inject_user() -> Callable[TFunc, TFunc]:     def decorator(func: TFunc) -> TFunc:         def wrapper(*args, **kwargs) -> Any:             user = get_authenticated_user()             if user is None:                 raise Exception("Don't!")             return func(*args, user, **kwargs)  # <- call signature modified          return wrapper      return decorator   @inject_user() def foo(a: int, username: str) -> bool:     print(username)     return bool(a % 2)   foo(2)      # Type check OK foo("no!")  # Type check OK <---- UNEXPECTED 

I can't figure out a correct way to type this. I know that on this example, decorated function and returned function should technically have the same signature (but even that is not detected).

like image 707
FunkySayu Avatar asked Nov 01 '17 17:11

FunkySayu


People also ask

What is the return type of a decorator Python?

Decorators in Python are very powerful which modify the behavior of a function without modifying it permanently. It basically wraps another function and since both functions are callable, it returns a callable. In hindsight, a decorator wraps a function and modifies its behavior.

Should I use type hinting in Python?

Type hints work best in modern Pythons. Annotations were introduced in Python 3.0, and it's possible to use type comments in Python 2.7. Still, improvements like variable annotations and postponed evaluation of type hints mean that you'll have a better experience doing type checks using Python 3.6 or even Python 3.7.

How do you type hinting in Python?

Here's how you can add type hints to our function: Add a colon and a data type after each function parameter. Add an arrow ( -> ) and a data type after the function to specify the return data type.

Are decorators Pythonic?

Decorators are a very powerful and useful tool in Python since it allows programmers to modify the behaviour of a function or class. Decorators allow us to wrap another function in order to extend the behaviour of the wrapped function, without permanently modifying it.


2 Answers

You can't use Callable to say anything about additional arguments; they are not generic. Your only option is to say that your decorator takes a Callable and that a different Callable is returned.

In your case you can nail down the return type with a typevar:

RT = TypeVar('RT')  # return type  def inject_user() -> Callable[[Callable[..., RT]], Callable[..., RT]]:     def decorator(func: Callable[..., RT]) -> Callable[..., RT]:         def wrapper(*args, **kwargs) -> RT:             # ... 

Even then the resulting decorated foo() function has a typing signature of def (*Any, **Any) -> builtins.bool* when you use reveal_type().

Various proposals are currently being discussed to make Callable more flexible but those have not yet come to fruition. See

  • Allow variadic generics
  • Proposal: Generalize Callable to be able to specify argument names and kinds
  • TypeVar to represent a Callable's arguments
  • Support function decorators excellently

for some examples. The last one in that list is an umbrella ticket that includes your specific usecase, the decorator that alters the callable signature:

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.

like image 191
Martijn Pieters Avatar answered Oct 01 '22 23:10

Martijn Pieters


PEP 612 was accepted after the accepted answer, and we now have typing.ParamSpec and typing.Concatenate in Python 3.10. With these variables, we can correctly type some decorators that manipulate positional parameters.

Note that mypy's support for PEP 612 is still under way (tracking issue).

The code in question can be typed like this (though not tested on mypy for the reason above)

from typing import Callable, ParamSpec, Concatenate, TypeVar  Param = ParamSpec("Param") RetType = TypeVar("RetType") OriginalFunc = Callable[Param, RetType] DecoratedFunc = Callable[Concatenate[Param, str], RetType]  def get_authenticated_user(): return "John"  def inject_user() -> Callable[[OriginalFunc], DecoratedFunc]:     def decorator(func: OriginalFunc) -> DecoratedFunc:         def wrapper(*args, **kwargs) -> RetType:             user = get_authenticated_user()             if user is None:                 raise Exception("Don't!")             return func(*args, user, **kwargs)  # <- call signature modified          return wrapper      return decorator   @inject_user() def foo(a: int, username: str) -> bool:     print(username)     return bool(a % 2)   foo(2)      # Type check OK foo("no!")  # Type check should fail 
like image 35
legogo Avatar answered Oct 01 '22 23:10

legogo