Considérez le code suivant:
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
Ce morceau de code fonctionne comme prévu. Imaginez maintenant que je veux étendre cela, et au lieu d'exécuter simplement func(*args, **kwargs)
je veux injecter le nom d'utilisateur dans les arguments. Par conséquent, je modifie la signature de la fonction.
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
Je ne peux pas trouver une bonne façon de taper ceci. Je sais que sur cet exemple, la fonction décorée et la fonction retournée devraient techniquement avoir la même signature (mais même cela n'est pas détecté).
Vous ne pouvez pas utiliser Callable
pour dire quoi que ce soit sur des arguments supplémentaires; ils ne sont pas génériques. Votre seule option est de dire que votre décorateur prend un Callable
et qu'un Callable
différent est retourné.
Dans votre cas, vous pouvez clouer le type de retour avec une police de caractères:
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:
# ...
Même alors, la fonction foo()
décorée résultante a une signature de frappe de def (*Any, **Any) -> builtins.bool*
lorsque vous utilisez reveal_type()
.
Diverses propositions sont actuellement en cours de discussion pour rendre Callable
plus flexible, mais celles-ci ne sont pas encore concrétisées. Voir
Callable
pour pouvoir spécifier les noms et les types d'argumentspour quelques exemples. Le dernier de cette liste est un ticket parapluie qui comprend votre cas d'utilisation spécifique, le décorateur qui modifie la signature appelable:
Mess avec le type de retour ou avec des arguments
Pour une fonction arbitraire, vous ne pouvez pas encore le faire du tout - il n'y a même pas de syntaxe. Voici moi en train de créer une syntaxe.