Python 3装饰器的类型提示

ubbxdtey  于 4个月前  发布在  Python
关注(0)|答案(4)|浏览(73)

下面是一个例子:

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

字符串
这段代码按预期工作。现在假设我想扩展它,而不仅仅是执行func(*args, **kwargs),我想在参数中注入用户名。因此,我修改了函数签名。

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


我想不出正确的输入方法,我知道在这个例子中,修饰函数和返回函数在技术上应该有相同的签名(但即使这样也没有被检测到)。

fkaflof6

fkaflof61#

PEP 612在接受的答案之后被接受,现在我们在Python 3.10中有了typing.ParamSpectyping.Concatenate。有了这些变量,我们可以正确地输入一些操纵位置参数的装饰器。
代码可以这样输入:

from typing import Callable, ParamSpec, Concatenate, TypeVar

Param = ParamSpec("Param")
RetType = TypeVar("RetType")

def get_authenticated_user()->str:
    return "John"

def inject_user() -> Callable[[Callable[Param, RetType]], Callable[Concatenate[str, Param], RetType]]:
    def decorator(func: Callable[Param, RetType]) -> Callable[Concatenate[str, Param], RetType]:
        def wrapper(user: str, *args:Param.args, **kwargs:Param.kwargs) -> RetType:
            user = get_authenticated_user()
            if user is None:
                raise Exception("Don't!")
            return func(*args, **kwargs)

        return wrapper

    return decorator

@inject_user()
def foo(a: int) -> bool:
    return bool(a % 2)

reveal_type(foo)  #  # I: Revealed type is "def (builtins.str, a: builtins.int) -> builtins.bool"

foo("user", 2)  # Type check OK
foo("no!")  # E: Missing positional argument "a" in call to "foo"  [call-arg]
foo(3)  # # E: Missing positional argument "a" in call to "foo"  [call-arg] # E: Argument 1 to "foo" has incompatible type "int"; expected "str"  [arg-type]

字符串

arknldoa

arknldoa2#

你不能用Callable来描述额外的参数,它们不是泛型的,你只能说你的装饰器接受一个Callable,然后返回一个不同的Callable
在你的例子中,你可以用一个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:
            # ...

字符串
即使这样,当您使用reveal_type()时,最终的修饰foo()函数也具有def (*Any, **Any) -> builtins.bool*的类型签名。
目前正在讨论各种提案,以使Callable更加灵活,但这些提案尚未实现。

列表中的最后一个是一个包含特定用例的伞票,它是修改可调用签名的装饰器:

修改返回类型或参数

对于一个任意的函数,你还不能这样做--甚至没有语法,这是我为它编的一些语法。

apeeds0o

apeeds0o3#

我在Pyright测试了这个。

from typing import Any, Callable, Type, TypeVar

T = TypeVar('T')

def typing_decorator(rtype: Type[T]) -> Callable[..., Callable[..., T]]:
    """
    Useful function to typing a previously decorated func.
    ```
    @typing_decorator(rtype = int)
    @my_decorator()
    def my_func(a, b, *c, **d):
        ...
    ```
    In Pyright the return typing of my_func will be int.
    """
    def decorator(function: Any) -> Any:
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            return function(*args, **kwargs)
        return wrapper
    return decorator  # type: ignore

字符串

bvpmtnay

bvpmtnay4#

使用decohints库可以解决这个问题:

pip install decohints

字符串
以下是它如何与您的代码一起工作:

from decohints import decohints

def get_authenticated_user():
    return "John"

@decohints
def inject_user():
    def decorator(func):
        def wrapper(*args, **kwargs):
            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)


如果在PyCharm中输入foo()下面的内容并等待,它将显示foo函数参数提示(a: int, username: str)
这里有一个decohints源代码的链接,还有其他解决这个问题的方法:https://github.com/gri-gus/decohints

相关问题