我正在尝试在 Python 中设置一个类装饰器,它的作用类似于attr.frozen
但在创建之前添加了一个额外的字段(以及其他一些东西)。虽然代码运行良好,但我无法让 mypy 意识到新类具有新字段。我试图通过一个自定义 mypy 插件(完全如's documentation中attr
所述)和一个Protocol
定义新类具有给定字段的组合来做到这一点。总之,代码分解如下(都在一个文件中,尽管我在这里分解了它)。
应该注意我正在运行 Python 3.7,所以我typing_extensions
在需要的地方使用,但我相信无论版本如何,这个问题都会持续存在。
首先定义应该通知 mypy 新类具有新字段(added
在此处调用)的协议:
from typing_extensions import Protocol
class Proto(Protocol):
def __init__(self, added: float, *args, **kwargs):
...
@property
def added(self) -> float:
...
现在根据文档field_transformer
定义添加新字段的函数:attr
from typing import Type, List
import attr
def _field_transformer(cls: type, fields: List[attr.Attribute]) -> List[attr.Attribute]:
return [
# For some reason mypy has trouble with attr.Attribute's signature
# Bonus points if someone can point out a fix that doesn't use type: ignore
attr.Attribute ( # type: ignore
"added", # name
attr.NOTHING, # default
None, # validator
True, # repr
None, # cmp
None, # hash
True, # init
False, # inherited
type=float,
order=float,
),
*fields,
]
现在,最后,设置一个类装饰器来做我们想要的:
from functools import wraps
from typing import Callable, TypeVar
_T = TypeVar("_T", bound=Proto)
_C = TypeVar("_C", bound=type)
def transform(_cls: _C = None, **kwargs):
def transform_decorator(cls: _C) -> Callable[[], Type[_T]]:
@wraps(cls)
def wrapper() -> Type[_T]:
if "field_transformer" not in kwargs:
kwargs["field_transformer"] = _field_transformer
return attr.frozen(cls, **kwargs)
return wrapper()
if _cls is None:
return transform_decorator
return transform_decorator(_cls)
现在对于(失败的)mypy 测试:
@transform
class Test:
other_field: str
# E: Too many arguments for "Test"
# E: Argument 1 to "Test" has incompatible type "float"; expected "str"
t = Test(0.0, "hello, world")
print(t.added) # E: "Test" has no attribute "added"
理想情况下,我希望 mypy 消除所有这三个错误。坦率地说,我不确定这是否可能;可能是属性的动态添加是不可键入的,我们可能不得不强制我们库的用户在使用装饰器时编写自定义类型的存根。但是,由于我们总是向生成的类添加相同的属性,如果有解决方案会很棒,即使这意味着编写一个特别支持这个装饰器的自定义 mypy 插件(如果这甚至可能的话)。