web-dev-qa-db-fra.com

Python 3 conseils de type pour décorateur

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é).

22
FunkySayu

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

pour 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.

14
Martijn Pieters