类型检查动态添加属性

2024-05-15 06:49:25 发布

您现在位置:Python中文网/ 问答频道 /正文

在编写特定于项目的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)

显然,在_pytest.config.Config类中没有fizz属性,因此在上述代码段上运行mypy会产生

conftest.py:5: error: "Config" has no attribute "fizz"
conftest.py:8: error: "Config" has no attribute "fizz"

(请注意,pytest还没有带有类型提示的发行版,因此如果您想在本地实际重现错误,请按照this comment中的步骤安装一个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)

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

from pytest import Session


def pytest_sessionstart(session: Session) -> None:
    session.config.fizz = "buzz"

将迫使我也重写Session以进行类型检查

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


Tags: 项目fromimportnoneconfig类型属性pytest
2条回答

一种方法是让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"

如果您确实知道您的特殊属性将始终是STR或其他内容,则可以键入__getattr____setattr__以返回或接受str,而不是分别键入Any以获得更紧凑的类型

不幸的是,您仍然需要使用子类型技巧,否则就需要自己制作存根。这给您带来的唯一好处是,您至少不必列出要设置的每个自定义属性,从而可以创建真正可重用的内容。这可能会让你更喜欢这个选择,不确定

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

  • 只需在使用特殊属性的每一行添加# type: ignore注释。这将是一种有点精确(如果是侵入性的)抑制错误消息的方法
  • 键入pytest_configurepytest_unconfigure,以便它们接受类型为Any的对象。这将是一种抑制错误消息的干扰性较小的方法。如果您想最小化使用Any的爆炸半径,那么您可以将希望使用这些自定义属性的任何逻辑限制为它们自己的专用函数,并继续在其他任何地方使用Config
  • 尝试使用铸造代替。例如,在pytest_configure内部,您可以执行config = cast(MutableConfig, config),其中MutableConfig是您编写的一个类,它将_pytest.Config子类化,并定义__getattr____setattr__。这可能是上述两种方法之间的一个中间点
  • 如果向Config和类似类添加特殊属性是一种常见的做法,那么可以尝试说服pytest维护人员在其类型提示中只键入__getattr____setattr__定义,或者以其他更专用的方式让用户添加这些动态属性

您可以通过一个新属性来扩展Config类,该属性是dict并存储所有自定义信息。例如:

def pytest_configure(config: Config) -> None:
    config.data["fizz"] = "buzz"  # `data` is the custom dict

这样一来,一个自定义存根文件适合所有项目。当然,它不会立即帮助您的旧项目,因为您需要重写相关部分以使用data['fizz']而不是fizz。但是,使用dict的另一个优点是,它可以防止现有属性和自定义属性之间可能出现的名称冲突

如果将自定义数据附加到Config对象是常见的做法,那么可能值得尝试以数据dict的形式对此进行标准化,并在pytest项目中打开相应的问题

如果您不喜欢重写代码,但仍然希望使用静态类型检查器,那么仍然可以使用从某个模板生成的自定义每个项目存根文件。您可以将所有自定义属性直接列为自定义类上的注释,然后让脚本从中生成相应的存根文件:

from _pytest.config import Config as _Config

class Config(_Config):
    fizz: str

# The above code can be used by a script to generate custom stub files.

相关问题 更多 >