类型检查动态添加的属性

hoe*_*ing 8 python typechecking mypy

在编写特定于项目的pytest插件时,我经常发现该Config对象对于附加我自己的属性很有用。例子:

from _pytest.config import Config


def pytest_configure(config: Config) -> None:
    config.fizz = "buzz"

def pytest_unconfigure(config: Config) -> None:
    print(config.fizz)
Run Code Online (Sandbox Code Playgroud)

显然,类中没有fizz属性_pytest.config.Config,所以运行mypy上面的代码片段会产生

conftest.py:5: error: "Config" has no attribute "fizz"
conftest.py:8: error: "Config" has no attribute "fizz"
Run Code Online (Sandbox Code Playgroud)

(请注意,pytest目前还没有带有类型提示的版本,因此如果您想在本地实际重现错误,请按照此评论中的步骤安装一个 fork )。

有时重新定义用于类型检查的类可以提供快速帮助:

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from _pytest.config import Config as _Config

    class Config(_Config):
        fizz: str

else:
    from _pytest.config import Config



def pytest_configure(config: Config) -> None:
    config.fizz = "buzz"

def pytest_unconfigure(config: Config) -> None:
    print(config.fizz)
Run Code Online (Sandbox Code Playgroud)

然而,除了混乱的代码之外,子类化的解决方法非常有限:添加例如

from pytest import Session


def pytest_sessionstart(session: Session) -> None:
    session.config.fizz = "buzz"
Run Code Online (Sandbox Code Playgroud)

会迫使我也覆盖类型检查Session

解决此问题的最佳方法是什么?Config是一个例子,但我通常在每个项目中都有更多(针对测试收集/调用/报告等的项目特定调整)。我可以想象编写我自己的pytest存根版本,但是我需要为每个项目重复这个,这非常乏味。

Mic*_*x2a 5

这样做的一种方法是设法让您的Config对象定义__getattr____setattr__方法。如果这些方法是在类中定义的,mypy 将使用它们来键入检查您正在访问或设置某些未定义属性的位置。

例如:

from typing import Any

class Config:
    def __init__(self) -> None:
        self.always_available = 1

    def __getattr__(self, name: str) -> Any: pass

    def __setattr__(self, name: str, value: Any) -> None: pass

c = Config()

# Revealed types are 'int' and 'Any' respectively
reveal_type(c.always_available)
reveal_type(c.missing_attr)

# The first assignment type checks, but the second doesn't: since
# 'already_available' is a predefined attr, mypy won't try using
# `__setattr__`.
c.dummy = "foo"
c.always_available = "foo"
Run Code Online (Sandbox Code Playgroud)

如果您确定您的临时属性将始终是 strs 或其他内容,您可以输入__getattr____setattr__返回或接受str而不是Any分别获得更严格的类型。

不幸的是,您仍然需要使用子类型技巧或制作自己的存根——这给您带来的唯一优势是您至少不必列出您想要设置的每个自定义属性并制作它可能创造出真正可重用的东西。这可能会使该选项对您来说更可口,但不确定。

您可以探索的其他选项包括:

  • 只需# type: ignore在使用临时属性的每一行添加注释即可。如果是侵入性的,这将是一种有点精确的抑制错误消息的方式。
  • 输入你的pytest_configurepytest_unconfigure所以他们接受类型的对象Any。这将是一种抑制错误消息的侵入性较小的方式。如果您想最小化 using 的爆炸半径Any,您可以将任何想要使用这些自定义属性的逻辑限制在它们自己的专用函数中,并继续Config在其他任何地方使用。
  • 尝试使用强制转换。例如,在里面pytest_configure你可以做config = cast(MutableConfig, config)where MutableConfigis a class 你写的子类_pytest.Config并定义了__getattr____setattr__。这可能是上述两种方法之间的中间地带。
  • 如果向Config和类似的类添加临时属性是一种常见的事情,也许可以尝试说服 pytest 维护者在他们的类型提示中包含仅输入__getattr____setattr__定义——或者一些其他更专门的方式让用户添加这些动态特性。