如何键入注释多级装饰器

问题描述 投票:0回答:1

我正在尝试注释一个注入器装饰器,该装饰器在调用函数时将全局字典中的值作为关键字参数注入到装饰函数中。

任何有使用参数注释装饰器经验的人都可以帮助我吗? 尝试注释但陷入以下错误:

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]

感谢您的宝贵时间。

python mypy python-decorators python-typing
1个回答
3
投票

第一个

[attr-defined]
错误在我看来是不可避免的,应该简单地明确忽略。重新发明轮子并使用该特殊属性定义您自己的特殊可调用协议是没有意义的。

代码的第二个和第三个错误

[no-any-return]
/
misc
我稍后会回来。

出现

[return-value]
代码的第四个错误是因为包装器的返回注释应该是
T
,而不是
Callable[P, T]
。它应该返回装饰函数返回的任何内容。

第五个错误(也是

[return-value]
)告诉您包装器可能是一个可以等待产生
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
。从静态类型检查器的角度来看,
handler
返回some object。并且不能安全地认为这是可以等待的。因此,您无法安全地将其与
await
一起使用(
[misc]
错误)。

由于类型检查器甚至不允许该行中存在

await
表达式,因此它显然无法验证返回值是否确实与
wrapper
的注释匹配(应该是
T
,就像前面提到的错误一样,但在这种情况下,无论如何都没关系)。

不过我并不能 100% 确定这两个错误的根本原因。


如果我是你,我什至不会首先检查装饰功能,从而让我的生活变得更加轻松。装饰器的行为不会改变。唯一的区别是,一次调用后需要跟一个

await
才能获取值。您可以让装饰器不知道
fn
是否返回可等待的对象,并将其留给调用者来处理。

这是我的建议:

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]"

这正是我们所期望的。

© www.soinside.com 2019 - 2024. All rights reserved.