类型检查动态添加的属性

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

在编写特定于项目的

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)

显然,

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

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

(请注意,

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

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

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
存根,但随后我需要为每个项目重复此操作,这非常乏味。

python python-typing mypy
2个回答
7
投票

实现此目的的一种方法是设法让您的

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"

如果您确定您的临时属性将始终是 strs 或其他内容,您可以输入

__getattr__
__setattr__
分别返回或接受
str
而不是
Any
以获得更严格的类型。

不幸的是,您仍然需要执行子类型技巧或制作自己的存根 - 这给您带来的唯一好处是您至少不必列出您想要设置和制作的每个自定义属性创造出真正可重复使用的东西是可能的。这可能会让你更喜欢这个选项,但不确定。

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

  • 只需在使用临时属性的每一行添加
    # type: ignore
    注释即可。这将是一种抑制错误消息的有点精确(尽管有侵入性)的方法。
  • 输入您的
    pytest_configure
    pytest_unconfigure
    ,以便它们接受
    Any
    类型的对象。这将是一种抑制错误消息的侵入性较小的方法。如果您想最小化使用
    Any
    的影响范围,您可以将任何想要使用这些自定义属性的逻辑限制在其自己的专用函数中,并在其他地方继续使用
    Config
  • 尝试使用强制转换。例如,在
    pytest_configure
    中,您可以执行
    config = cast(MutableConfig, config)
    ,其中
    MutableConfig
    是您编写的类,它是
    _pytest.Config
    的子类,并定义
    __getattr__
    __setattr__
    。这可能是上述两种方法之间的中间立场。
  • 如果向
    Config
    和类似的类添加临时属性是一种常见的事情,也许可以尝试说服 pytest 维护人员在其类型提示中包含仅键入
    __getattr__
    __setattr__
    定义——或者一些让用户添加这些动态属性的其他更专用的方式。

3
投票

您可以通过一个新属性扩展

Config
类,该新属性是
dict
并存储所有自定义信息。例如:

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

这样,一个自定义存根文件就适合您的所有项目。当然,它不会立即帮助您的旧项目,因为您需要重写相关部分以使用

data['fizz']
而不是
fizz
。然而,使用
dict
的另一个优点是,它可以防止现有属性和自定义属性之间可能发生名称冲突。

如果将自定义数据附加到

Config
对象是常见做法,也许值得尝试以数据字典的形式对其进行标准化,并在
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.
© www.soinside.com 2019 - 2024. All rights reserved.