如何告诉 mypy 类装饰器向装饰类添加方法

zer*_*dge 11 python mypy

Python 库pure_protobuf强制其用户使用数据类,用另一个装饰器装饰它们:

# to be clear: these two decorators are library code (external)
@message
@dataclass
class SearchRequest:
    query: str = field(1, default='')
    page_number: int32 = field(2, default=int32(0))
    result_per_page: int32 = field(3, default=int32(0))
Run Code Online (Sandbox Code Playgroud)

@message装饰器为实例分配SearchRequest一个名为的方法dumps

SearchRequest(
    query='hello',
    page_number=int32(1),
    result_per_page=int32(10),
).dumps() == b'\x0A\x05hello\x10\x01\x18\x0A'
Run Code Online (Sandbox Code Playgroud)

在我的应用程序代码中,我有一个特定的用例,我需要传递一个具有该dumps()方法的对象。它可以是pure_protobuf Message像上面这样的实例,也可以是任何其他类型,只要它实现dumps().

对于我自己定义并实现dumps()“接口”的类,它工作得很好,但对于pure_protobuf数据类,它一直抱怨它们没有 attribute dumps()

使这更具挑战性的是我pure_protobuf自己没有定义这些数据类,这些数据类将由我的库的客户定义,所以我不能简单地做一些(愚蠢的)事情,比如:

@message
@dataclass
class SearchRequest:
    query: str = field(1, default='')
    page_number: int32 = field(2, default=int32(0))
    result_per_page: int32 = field(3, default=int32(0))
    
    def dumps(self):
       self.dumps() # that is Message.dumps from the decorator
Run Code Online (Sandbox Code Playgroud)

我没有选择了吗?

STe*_*kov 9

不幸的是,你在这里真的没有解决方案,因为你需要(无论它是外部的,它并不是很重要)message装饰器来返回Intersection(或Meet根据类型理论)输入类和协议的 4 个方法(dumpdumpsloadloads) 。它尚未包含在 python 类型系统中,并且未作为类型检查器扩展实现。Intersection 请参阅有关此 python/typing 问题的讨论。

最有趣的是,根据本教程,您可以改用 pytype 并且不加注释。如果您可以选择使用其他类型检查器,您可以声明自己的版本:messagemessage

from typing import IO, Protocol, TYPE_CHECKING
from pure_protobuf.dataclasses_ import message as _message

class MessageMixin(Protocol):
    def dumps(self) -> bytes: ...
    def dump(self, io: IO) -> None: ...
    # Other definitions can go here

def message(cls):
    if TYPE_CHECKING:  # tweak
        return type(cls.__name__, (MessageMixin, cls), {})
    else:  # actually run on runtime
        return _message(cls)
Run Code Online (Sandbox Code Playgroud)

然后您的库的用户可以安全地使用此message实现,因为实际上它只是包装现有方法以进行类型检查,而不会影响运行时。因此,如果他们不这样做,他们只会有mypy错误(或者如果他们不使用类型检查器,则不会),但运行时不会受到影响。

然而,它再次不适用于mypy.

如果您有兴趣使用Intersection像 之类的工具mypy,请考虑切换到basedmypyfork,该工具除其他扩展外,Intersection还尽最大努力提供支持。

  • 我想,这里的完整解决方案是编写一个 mypy 插件。我知道 Django 这样做是为了支持许多本身不适合 mypy 的复杂习惯用法,但我认为这本身就是一项艰巨的任务。 (2认同)