如何输入注释可选参数化装饰器,其中使用第三方装饰器

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

在我的项目中使用

mypy
时遇到问题。首先,我使用
backoff
包对某些函数/方法进行一些重试。然后我意识到,大多数选项只是重复的,因此我创建了每个项目的装饰器,并为
backoff
装饰器填充了所有常见值。但是,我不知道如何注释这样的“装饰器中的装饰器”。更重要的是,这应该与同步/异步函数/方法矩阵一起使用。这是重现我的痛苦的代码:

import asyncio
from collections.abc import Awaitable, Callable
from functools import wraps
from typing import Any, Literal, ParamSpec, TypeVar

T = TypeVar("T")
P = ParamSpec("P")
AnyCallable = Callable[P, T | Awaitable[T]]
Decorator = Callable[[AnyCallable[P, T]], AnyCallable[P, T]]


def third_party_decorator(
        a: int | None = None,
        b: str | None = None,
        c: Literal[None] = None,
        d: bool | None = None,
        e: str | None = None,
    ) -> Decorator[P, T]:
    def decorator(f: AnyCallable[P, T]) -> AnyCallable[P, T]:
        @wraps(f)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> T | Awaitable[T]:
            print(f"third_party_decorator {f = } {a = } {b = } {c = } {d = } {e = }")
            return f(*args, **kwargs)
        return wrapper
    return decorator


def parametrized_decorator(f: AnyCallable[P, T] | None = None, **kwargs: Any) -> Decorator[P, T] | AnyCallable[P, T]:
    def decorator(f: AnyCallable[P, T]) -> AnyCallable[P, T]:
        defaults = {"a": 1, "b": "b", "c": None, "d": True}
        defaults.update(kwargs)
        print(f"parametrized_decorator {f = } {defaults = }")
        decorator: Decorator[P, T] = third_party_decorator(**defaults)
        wrapped = decorator(f)
        return wrapped

    if f is None:
        return decorator
    else:
        return decorator(f)


@parametrized_decorator
def sync_straight_function(x: int = 0) -> None:
    print(f"sync_straight_function {x = }")


@parametrized_decorator(b="B", e="e")
def sync_parametrized_function(x: str = "abc", y: bool = False) -> None:
    print(f"sync_parametrized_function {x = } {y = }")


@parametrized_decorator
async def async_straight_function(x: int = 0) -> None:
    print(f"sync_straight_function {x = }")


@parametrized_decorator(b="B", e="e")
async def async_parametrized_function(x: str = "abc", y: bool = False) -> None:
    print(f"sync_parametrized_function {x = } {y = }")


class Foo:
    @parametrized_decorator
    def sync_straight_method(self, x: int = 0) -> None:
        print(f"sync_straight_function {x = }")

    @parametrized_decorator(b="B", e="e")
    def sync_parametrized_method(self, x: str = "abc", y: bool = False) -> None:
        print("sync_parametrized_method", x, y)

    @parametrized_decorator
    async def async_straight_method(self, x: int = 0) -> None:
        print(f"sync_straight_function {x = }")

    @parametrized_decorator(b="B", e="e")
    async def async_parametrized_method(self, x: str = "abc", y: bool = False) -> None:
        print(f"sync_parametrized_function {x = } {y = }")


def main_sync_functions() -> None:
    sync_straight_function()
    sync_straight_function(1)

    sync_parametrized_function()
    sync_parametrized_function("xyz", True)


async def main_async_functions() -> None:
    await async_straight_function()
    await async_straight_function(1)

    await async_parametrized_function()
    await async_parametrized_function("xyz", True)


def main_sync_methods() -> None:
    foo = Foo()
    foo.sync_straight_method()
    foo.sync_straight_method(1)

    foo.sync_parametrized_method()
    foo.sync_parametrized_method("xyz", True)


async def main_async_methods() -> None:
    foo = Foo()
    await foo.async_straight_method()
    await foo.async_straight_method(1)

    await foo.async_parametrized_method()
    await foo.async_parametrized_method("xyz", True)


if __name__ == "__main__":
    print("\nSYNC FUNCTIONS:")
    main_sync_functions()
    print("\nASYNC FUNCTIONS:")
    asyncio.run(main_async_functions())
    print("\nSYNC METHODS:")
    main_sync_methods()
    print("\nASYNC METHODS:")
    asyncio.run(main_async_methods())

mypy 的输出有 44 个错误:

parametrized-decorator-typing.py:33: error: Argument 1 to "third_party_decorator" has incompatible type "**dict[str, object]"; expected "int | None"  [arg-type]
            decorator: Decorator[P, T] = third_party_decorator(**defaults)
                                                                 ^~~~~~~~
parametrized-decorator-typing.py:33: error: Argument 1 to "third_party_decorator" has incompatible type "**dict[str, object]"; expected "str | None"  [arg-type]
            decorator: Decorator[P, T] = third_party_decorator(**defaults)
                                                                 ^~~~~~~~
parametrized-decorator-typing.py:33: error: Argument 1 to "third_party_decorator" has incompatible type "**dict[str, object]"; expected "None"  [arg-type]
            decorator: Decorator[P, T] = third_party_decorator(**defaults)
                                                                 ^~~~~~~~
parametrized-decorator-typing.py:33: error: Argument 1 to "third_party_decorator" has incompatible type "**dict[str, object]"; expected "bool | None"  [arg-type]
            decorator: Decorator[P, T] = third_party_decorator(**defaults)
                                                                 ^~~~~~~~
parametrized-decorator-typing.py:48: error: Argument 1 has incompatible type "Callable[[str, bool], None]"; expected
"Callable[[VarArg(<nothing>), KwArg(<nothing>)], <nothing> | Awaitable[<nothing>]]"  [arg-type]
    @parametrized_decorator(b="B", e="e")
     ^
parametrized-decorator-typing.py:48: error: Argument 1 has incompatible type "Callable[[str, bool], None]"; expected <nothing>  [arg-type]
    @parametrized_decorator(b="B", e="e")
     ^
parametrized-decorator-typing.py:53: error: Argument 1 to "parametrized_decorator" has incompatible type "Callable[[int], Coroutine[Any, Any, None]]"; expected
"Callable[[int], <nothing> | Awaitable[<nothing>]] | None"  [arg-type]
    @parametrized_decorator
     ^
parametrized-decorator-typing.py:58: error: Argument 1 has incompatible type "Callable[[str, bool], Coroutine[Any, Any, None]]"; expected
"Callable[[VarArg(<nothing>), KwArg(<nothing>)], <nothing> | Awaitable[<nothing>]]"  [arg-type]
    @parametrized_decorator(b="B", e="e")
     ^
parametrized-decorator-typing.py:58: error: Argument 1 has incompatible type "Callable[[str, bool], Coroutine[Any, Any, None]]"; expected <nothing>  [arg-type]
    @parametrized_decorator(b="B", e="e")
     ^
parametrized-decorator-typing.py:68: error: Argument 1 has incompatible type "Callable[[Foo, str, bool], None]"; expected
"Callable[[VarArg(<nothing>), KwArg(<nothing>)], <nothing> | Awaitable[<nothing>]]"  [arg-type]
        @parametrized_decorator(b="B", e="e")
         ^
parametrized-decorator-typing.py:68: error: Argument 1 has incompatible type "Callable[[Foo, str, bool], None]"; expected <nothing>  [arg-type]
        @parametrized_decorator(b="B", e="e")
         ^
parametrized-decorator-typing.py:72: error: Argument 1 to "parametrized_decorator" has incompatible type "Callable[[Foo, int], Coroutine[Any, Any, None]]"; expected
"Callable[[Foo, int], <nothing> | Awaitable[<nothing>]] | None"  [arg-type]
        @parametrized_decorator
         ^
parametrized-decorator-typing.py:76: error: Argument 1 has incompatible type "Callable[[Foo, str, bool], Coroutine[Any, Any, None]]"; expected
"Callable[[VarArg(<nothing>), KwArg(<nothing>)], <nothing> | Awaitable[<nothing>]]"  [arg-type]
        @parametrized_decorator(b="B", e="e")
         ^
parametrized-decorator-typing.py:76: error: Argument 1 has incompatible type "Callable[[Foo, str, bool], Coroutine[Any, Any, None]]"; expected <nothing>  [arg-type]
        @parametrized_decorator(b="B", e="e")
         ^
parametrized-decorator-typing.py:82: error: Too few arguments  [call-arg]
        sync_straight_function()
        ^~~~~~~~~~~~~~~~~~~~~~~~
parametrized-decorator-typing.py:83: error: Argument 1 has incompatible type "int"; expected "Callable[[int], Awaitable[None] | None]"  [arg-type]
        sync_straight_function(1)
                               ^
parametrized-decorator-typing.py:85: error: "Awaitable[<nothing>]" not callable  [operator]
        sync_parametrized_function()
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
parametrized-decorator-typing.py:86: error: "Awaitable[<nothing>]" not callable  [operator]
        sync_parametrized_function("xyz", True)
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
parametrized-decorator-typing.py:86: error: Argument 1 has incompatible type "str"; expected <nothing>  [arg-type]
        sync_parametrized_function("xyz", True)
                                   ^~~~~
parametrized-decorator-typing.py:86: error: Argument 2 has incompatible type "bool"; expected <nothing>  [arg-type]
        sync_parametrized_function("xyz", True)
                                          ^~~~
parametrized-decorator-typing.py:90: error: Incompatible types in "await" (actual type "Callable[[int], <nothing> | Awaitable[<nothing>]] | Awaitable[<nothing>]",
expected type "Awaitable[Any]")  [misc]
        await async_straight_function()
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
parametrized-decorator-typing.py:90: error: Too few arguments  [call-arg]
        await async_straight_function()
              ^~~~~~~~~~~~~~~~~~~~~~~~~
parametrized-decorator-typing.py:91: error: Incompatible types in "await" (actual type "Callable[[int], <nothing> | Awaitable[<nothing>]] | Awaitable[<nothing>]",
expected type "Awaitable[Any]")  [misc]
        await async_straight_function(1)
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
parametrized-decorator-typing.py:91: error: Argument 1 has incompatible type "int"; expected "Callable[[int], <nothing> | Awaitable[<nothing>]]"  [arg-type]
        await async_straight_function(1)
                                      ^
parametrized-decorator-typing.py:93: error: "Awaitable[<nothing>]" not callable  [operator]
        await async_parametrized_function()
              ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
parametrized-decorator-typing.py:94: error: "Awaitable[<nothing>]" not callable  [operator]
        await async_parametrized_function("xyz", True)
              ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
parametrized-decorator-typing.py:94: error: Argument 1 has incompatible type "str"; expected <nothing>  [arg-type]
        await async_parametrized_function("xyz", True)
                                          ^~~~~
parametrized-decorator-typing.py:94: error: Argument 2 has incompatible type "bool"; expected <nothing>  [arg-type]
        await async_parametrized_function("xyz", True)
                                                 ^~~~
parametrized-decorator-typing.py:99: error: Too few arguments  [call-arg]
        foo.sync_straight_method()
        ^~~~~~~~~~~~~~~~~~~~~~~~~~
parametrized-decorator-typing.py:100: error: Argument 1 has incompatible type "int"; expected "Callable[[Foo, int], Awaitable[None] | None]"  [arg-type]
        foo.sync_straight_method(1)
                                 ^
parametrized-decorator-typing.py:100: error: Argument 1 has incompatible type "int"; expected "Foo"  [arg-type]
        foo.sync_straight_method(1)
                                 ^
parametrized-decorator-typing.py:102: error: "Awaitable[<nothing>]" not callable  [operator]
        foo.sync_parametrized_method()
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
parametrized-decorator-typing.py:103: error: "Awaitable[<nothing>]" not callable  [operator]
        foo.sync_parametrized_method("xyz", True)
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
parametrized-decorator-typing.py:103: error: Argument 1 has incompatible type "str"; expected <nothing>  [arg-type]
        foo.sync_parametrized_method("xyz", True)
                                     ^~~~~
parametrized-decorator-typing.py:103: error: Argument 2 has incompatible type "bool"; expected <nothing>  [arg-type]
        foo.sync_parametrized_method("xyz", True)
                                            ^~~~
parametrized-decorator-typing.py:108: error: Incompatible types in "await" (actual type "Callable[[Foo, int], <nothing> | Awaitable[<nothing>]] | Awaitable[<nothing>]",
expected type "Awaitable[Any]")  [misc]
        await foo.async_straight_method()
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
parametrized-decorator-typing.py:108: error: Too few arguments  [call-arg]
        await foo.async_straight_method()
              ^~~~~~~~~~~~~~~~~~~~~~~~~~~
parametrized-decorator-typing.py:109: error: Incompatible types in "await" (actual type "Callable[[Foo, int], <nothing> | Awaitable[<nothing>]] | Awaitable[<nothing>]",
expected type "Awaitable[Any]")  [misc]
        await foo.async_straight_method(1)
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
parametrized-decorator-typing.py:109: error: Argument 1 has incompatible type "int"; expected "Callable[[Foo, int], <nothing> | Awaitable[<nothing>]]"  [arg-type]
        await foo.async_straight_method(1)
                                        ^
parametrized-decorator-typing.py:109: error: Argument 1 has incompatible type "int"; expected "Foo"  [arg-type]
        await foo.async_straight_method(1)
                                        ^
parametrized-decorator-typing.py:111: error: "Awaitable[<nothing>]" not callable  [operator]
        await foo.async_parametrized_method()
              ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
parametrized-decorator-typing.py:112: error: "Awaitable[<nothing>]" not callable  [operator]
        await foo.async_parametrized_method("xyz", True)
              ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
parametrized-decorator-typing.py:112: error: Argument 1 has incompatible type "str"; expected <nothing>  [arg-type]
        await foo.async_parametrized_method("xyz", True)
                                            ^~~~~
parametrized-decorator-typing.py:112: error: Argument 2 has incompatible type "bool"; expected <nothing>  [arg-type]
        await foo.async_parametrized_method("xyz", True)
                                                   ^~~~
Found 44 errors in 1 file (checked 1 source file)
python decorator mypy
2个回答
0
投票

我最终得到了下面的代码。不要介意

type: ignore
,因为这是第三方装饰器。

AnyCallable = Callable[..., Any]
F = TypeVar("F", bound=AnyCallable)
Decorator = Callable[[F], F]


def third_party_decorator(
        a: int | None = None,
        b: str | None = None,
        c: Literal[None] = None,
        d: bool | None = None,
        e: str | None = None,
    ) -> Decorator[F]:
    def decorator(f: F) -> F:
        @wraps(f)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            print(f"third_party_decorator {f = } {a = } {b = } {c = } {d = } {e = }")
            return f(*args, **kwargs)
        return wrapper  # type: ignore
    return decorator


def parametrized_decorator(f: AnyCallable | None = None, **kwargs: Any) -> AnyCallable:
    def decorator(f: F) -> F:
        defaults: dict[str, Any] = {"a": 1, "b": "b", "c": None, "d": True}
        defaults.update(kwargs)
        print(f"parametrized_decorator {f = } {defaults = }")
        decorator: Decorator[F] = third_party_decorator(**defaults)
        wrapped = decorator(f)
        return wrapped

    if f is None:
        return decorator
    else:
        return decorator(f)

0
投票

您提出的解决方案有一个主要缺陷:您的

parametrized_decorator
破坏了函数签名。您仍然需要一个 typevar 来保存它。

首先,你的

T
没有任何界限,所以
Awaitable[Something]
也可以只是
T
。在你的
third_party_decorator
中,
wrapper
返回
T | Awaitable[T]
,这很奇怪 - 它总是返回给定的
T
,它不能变得更加等待。所以,我们可以在这里完全忘记
Awaitable
,这大大简化了打字。

然后,你的装饰器可以有两种不同的行为方式:当 func 未传递时,要么是二阶装饰器(返回装饰器),要么返回一个函数。这可以表示为

overload
。这是我的建议(游乐场):

import asyncio
from collections.abc import Awaitable, Callable, Coroutine
from functools import wraps
from typing import Any, Literal, ParamSpec, TypeVar, overload

T = TypeVar("T")
_T1 = TypeVar("_T1")
P = ParamSpec("P")
_P1 = ParamSpec("_P1")



def third_party_decorator(
        a: int | None = None,
        b: str | None = None,
        c: Literal[None] = None,
        d: bool | None = None,
        e: str | None = None,
    ) -> Callable[[Callable[P, T]], Callable[P, T]]:
    def decorator(f: Callable[P, T]) -> Callable[P, T]:
        @wraps(f)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
            print(f"third_party_decorator {f = } {a = } {b = } {c = } {d = } {e = }")
            return f(*args, **kwargs)
        return wrapper
    return decorator


@overload
def parametrized_decorator(f: None = ..., /, **kwargs: Any) -> Callable[[Callable[P, T]], Callable[P, T]]: ...
@overload
def parametrized_decorator(f: Callable[P, T], /, **kwargs: Any) -> Callable[P, T]: ...
def parametrized_decorator(f: Callable[P, T] | None = None, /, **kwargs: Any) -> Callable[[Callable[_P1, _T1]], Callable[_P1, _T1]] | Callable[P, T]:
    def decorator(f: Callable[P, T]) -> Callable[P, T]:
        defaults = {"a": 1, "b": "b", "c": None, "d": True}
        defaults.update(kwargs)
        print(f"parametrized_decorator {f = } {defaults = }")
        decorator = third_party_decorator(**defaults)  # type: ignore[arg-type]
        wrapped = decorator(f)
        return wrapped

    if f is None:
        # You can avoid type-ignore here, but this will require an insane if
        # clause with `decorator` body effectively repeated twice
        return decorator  # type: ignore[return-value]
    else:
        return decorator(f)

注意到上面丑陋的

_T1
_P1
了吗?这是因为可调用本身是通用的,并且绑定得太早(
P
T
在新可调用的左侧和两侧应该相同)。让我们通过创建“替代可调用”来解决这个问题,它不是通用的,但仅提供通用的
__call__
playground):

import asyncio
from collections.abc import Awaitable, Callable, Coroutine
from functools import wraps
from typing import Any, Literal, ParamSpec, TypeVar, overload, Protocol

T = TypeVar("T")
P = ParamSpec("P")


class Decorator(Protocol):
    def __call__(self, func: Callable[P, T], /) -> Callable[P, T]: ...


def third_party_decorator(
        a: int | None = None,
        b: str | None = None,
        c: Literal[None] = None,
        d: bool | None = None,
        e: str | None = None,
    ) -> Decorator:
    def decorator(f: Callable[P, T]) -> Callable[P, T]:
        @wraps(f)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
            print(f"third_party_decorator {f = } {a = } {b = } {c = } {d = } {e = }")
            return f(*args, **kwargs)
        return wrapper
    return decorator


@overload
def parametrized_decorator(f: None = ..., /, **kwargs: Any) -> Decorator: ...
@overload
def parametrized_decorator(f: Callable[P, T], /, **kwargs: Any) -> Callable[P, T]: ...
def parametrized_decorator(f: Callable[P, T] | None = None, /, **kwargs: Any) -> Decorator | Callable[P, T]:
    def decorator(f: Callable[P, T]) -> Callable[P, T]:
        defaults = {"a": 1, "b": "b", "c": None, "d": True}
        defaults.update(kwargs)
        print(f"parametrized_decorator {f = } {defaults = }")
        decorator = third_party_decorator(**defaults)  # type: ignore[arg-type]
        wrapped = decorator(f)
        return wrapped

    if f is None:
        return decorator
    else:
        return decorator(f)

现在您没有任何忽略注释并且具有正确的返回类型。

为了解释我的“破坏签名”,

reveal_type(sync_straight_function)
或只是尝试
sync_straight_function('this', 'is', 'still', 'allowed', 'why?') + 1
与您的答案中的实现并观察没有mypy错误。

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