python-3.x 如何对多级装饰器进行类型注解

q0qdq0h2  于 2023-01-03  发布在  Python
关注(0)|答案(1)|浏览(148)

我正在尝试注解一个注入器装饰器,当函数被调用时,它将全局字典中的一个值作为关键字参数注入到装饰函数中。
有没有人能帮我解释一下用参数注解装饰器的经验?尝试过注解,但是遇到了下面的错误:

import functools
import inspect
from typing import Any, Callable, TypeVar, ParamSpec

Type = TypeVar('Type')
Param = ParamSpec('Param')
_INSTANCES = {}

def make_injectable(instance_name: str, instance: object) -> None:
    _INSTANCES[instance_name] = instance

def inject(*instances: str) -> Callable[Param, Type]:

    def get_function_with_instances(fn: Callable[Param, Type]) -> Callable[Param, Type]:
        # This attribute is to easily access which arguments of fn are injectable
        fn._injectable_args = instances

        def handler(*args: Param.args, **kwargs: Param.kwargs) -> Type:
            new_kwargs: dict[str, Any] = dict(kwargs).copy()
            for instance in instances:
                if instance in new_kwargs:
                    continue
                if instance not in _INSTANCES:
                    raise ValueError(f"Instance {instance} was not initialized yet")
                new_kwargs[instance] = _INSTANCES[instance]
            return fn(*args, **new_kwargs)

        if inspect.iscoroutinefunction(fn):
            @functools.wraps(fn)
            async def wrapper(*args: Param.args, **kwargs: Param.kwargs) -> Callable[Param, Type]:
                return await handler(*args, **kwargs)

        else:
            @functools.wraps(fn)
            def wrapper(*args: Param.args, **kwargs: Param.kwargs) -> Callable[Param, Type]:
                return handler(*args, **kwargs)

        return wrapper

    return get_function_with_instances

如果我用这些注解运行mypy,我会得到这些错误,如果不创建新的错误,我就无法避免这些错误:

mypy injector.py --strict --warn-unreachable --allow-subclassing-any --ignore-missing-imports --show-error-codes --install-types --non-interactive

injector.py:33: error: "Callable[Param, Type]" has no attribute "_injectable_args"  [attr-defined]
injector.py:48: error: Returning Any from function declared to return "Callable[Param, Type]"  [no-any-return]
injector.py:48: error: Incompatible types in "await" (actual type "Type", expected type "Awaitable[Any]")  [misc]
injector.py:53: error: Incompatible return value type (got "Type", expected "Callable[Param, Type]")  [return-value]
injector.py:55: error: Incompatible return value type (got "Callable[Param, Coroutine[Any, Any, Callable[Param, Type]]]", expected "Callable[Param, Type]")  [return-value]
injector.py:57: error: Incompatible return value type (got "Callable[[Callable[Param, Type]], Callable[Param, Type]]", expected "Callable[Param, Type]")  [return-value]

谢谢你抽出时间。

vaqhlq81

vaqhlq811#

第一个[attr-defined]错误是不可避免的IMO,应该直接显式忽略。
第二个和第三个错误是关于代码[no-any-return]/misc的,我稍后会回来。
[return-value]代码出现的第四个错误是因为 Package 器的返回注解应该是T,而不是Callable[P, T],它应该返回修饰函数返回的任何内容。
第五个错误(也是[return-value])告诉您,wrapper可能是一个可以等待产生T的协程,但是您声明了get_function_with_instances以返回一个返回T的可调用对象(不是一个等待T的协程)。
最后一个[return-value]错误出现是因为inject返回了一个decorator,它接受了一个Callable[P, T]类型的参数,并再次返回了一个相同类型的对象,所以inject的返回注解确实应该是Callable[[Callable[P, T]], Callable[P, T]],就像mypy所说的那样。
现在来看看[no-any-return]/misc错误,这有点令人困惑,因为您的意图是涵盖fn作为协程函数的情况和它作为常规函数的情况。
您可以对handler进行注解,以返回T,就像fn一样。但是,T是什么,并没有进一步缩小范围。由iscoroutinefunction给出的类型保护适用于fn,并且不会自动扩展到handler。从静态类型检查器的Angular 来看,handler返回 * some object *。并且不能安全地假定该对象是可等待的。因此,不能安全地将其用于await[misc]错误)。
由于类型检查器甚至不允许在该行中使用await表达式,因此它显然无法验证返回值是否确实匹配wrapper的注解(应该是T,就像前面提到的错误一样,但在本例中,这两种方式都无关紧要)。
我不能100%确定这两个错误的根本原因。
如果我是你,如果一开始就不检查修饰函数,我的工作会轻松很多,修饰器的行为不会改变,唯一的区别是一个调用需要后面跟一个await来获取值。您可以让装饰器不知道fn是否返回一个awatable,并让调用者来处理它。
以下是我的建议:

from collections.abc import Callable
from functools import wraps
from typing import TypeVar, ParamSpec

T = TypeVar('T')
P = ParamSpec('P')
_INSTANCES = {}

def make_injectable(instance_name: str, instance: object) -> None:
    _INSTANCES[instance_name] = instance

def inject(*instances: str) -> Callable[[Callable[P, T]], Callable[P, T]]:
    def get_function_with_instances(fn: Callable[P, T]) -> Callable[P, T]:
        # This attribute is to easily access which arguments of fn are injectable
        fn._injectable_args = instances  # type: ignore[attr-defined]

        @wraps(fn)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
            for instance in instances:
                if instance in kwargs:
                    continue
                if instance not in _INSTANCES:
                    raise ValueError(f"Instance {instance} was not initialized yet")
                kwargs[instance] = _INSTANCES[instance]
            return fn(*args, **kwargs)
        return wrapper
    return get_function_with_instances

下面是一个快速测试,显示类型都被正确地推断出来了:

make_injectable("foo", object())

@inject("foo")
def f(**kwargs: object) -> int:
    print(kwargs)
    return 1

@inject("foo")
async def g(**kwargs: object) -> int:
    print(kwargs)
    return 2

async def main() -> tuple[int, int]:
    x = f()
    y = await g()
    return x, y

if __name__ == '__main__':
    from asyncio import run
    print(run(main()))

输出示例:

{'foo': <object object at 0x7fe39fea0b20>}
{'foo': <object object at 0x7fe39fea0b20>}
(1, 2)

mypy --strict没有任何抱怨,从main的编写方式来看,我们可以看到返回类型都被正确地推断出来了,但是如果我们想显式地检查,我们可以在脚本的末尾添加reveal_type(f)reveal_type(g),那么mypy会告诉我们:

Revealed type is "def (**kwargs: builtins.object) -> builtins.int"
Revealed type is "def (**kwargs: builtins.object) -> typing.Coroutine[Any, Any, builtins.int]"

这正是我们所期待的。

相关问题