类型检查动态添加的属性
Typechecking dynamically added attributes
在编写特定于项目的 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
class 中没有 fizz
属性,因此 运行 mypy
上面的代码片段会产生
conftest.py:5: error: "Config" has no attribute "fizz"
conftest.py:8: error: "Config" has no attribute "fizz"
(请注意 pytest
还没有带有类型提示的版本,所以如果你想在本地实际重现错误,请按照 this comment 中的步骤安装一个分支)。
有时重新定义 class 进行类型检查可以提供快速帮助:
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)
然而,除了使代码混乱之外,subclassing 解决方法非常有限:添加例如
from pytest import Session
def pytest_sessionstart(session: Session) -> None:
session.config.fizz = "buzz"
会迫使我也覆盖 Session
进行类型检查。
解决此问题的最佳方法是什么? Config
是一个例子,但我通常在每个项目中还有几个(针对测试 collection/invocation/reporting 等的项目特定调整)。我可以想象编写我自己的 pytest
存根版本,但是我需要为每个项目重复这个,这非常乏味。
这样做的一种方法是设法让您的 Config
对象定义 __getattr__
和 __setattr__
方法。如果这些方法在 class 中定义,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__
到 return 或接受 str
Any
分别获得更紧密的类型。
不幸的是,您仍然需要执行子类型化技巧或制作自己的存根 - 这给您带来的唯一好处是您至少不必列出每个自定义 属性 你想设置并使创建真正可重用的东西成为可能。这可能会让您更喜欢这个选项,不确定。
您可以探索的其他选项包括:
- 只需在使用临时 属性 的每一行添加一个
# type: ignore
注释。这将是一种抑制错误消息的精确方法,如果是侵入性的。
- 键入您的
pytest_configure
和 pytest_unconfigure
,以便它们接受 Any
类型的对象。这是抑制错误消息的一种侵入性较小的方法。如果你想最小化使用 Any
的爆炸半径,你可以将任何想要使用这些自定义属性的逻辑限制在它们自己的专用函数中,并在其他任何地方继续使用 Config
。
- 尝试改用转换。例如,在
pytest_configure
中你可以做 config = cast(MutableConfig, config)
其中 MutableConfig
是一个 class 你写了 subclasses _pytest.Config
并且定义了两个 __getattr__
和 __setattr__
。这可能是上述两种方法之间的中间立场。
- 如果将临时属性添加到
Config
和类似的 classes 是一种常见的事情,也许可以尝试说服 pytest 维护者包含仅输入 __getattr__
和 __setattr__
类型提示中的定义——或者其他一些更专门的方式让用户添加这些动态属性。
您可以通过一个 dict
的新属性扩展 Config
class 并存储所有自定义信息。例如:
def pytest_configure(config: Config) -> None:
config.data["fizz"] = "buzz" # `data` is the custom dict
这样一来,一个自定义存根文件就适合您的所有项目。当然,它不会立即帮助您的旧项目,因为您需要重写相关部分以使用 data['fizz']
而不是 fizz
。然而,使用 dict
的另一个好处是它可以防止现有属性和自定义属性之间可能发生的名称冲突。
如果将自定义数据附加到 Config
对象是常见的做法,也许值得尝试以数据字典的形式对其进行标准化,并在 pytest
项目中打开相应的问题。
如果您不喜欢重写代码但仍想使用静态类型检查器,您仍然可以使用从某个模板生成的自定义每个项目存根文件。您可以将所有自定义属性直接列为自定义 class 上的注释,然后让脚本从中生成相应的存根文件:
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.
在编写特定于项目的 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
class 中没有 fizz
属性,因此 运行 mypy
上面的代码片段会产生
conftest.py:5: error: "Config" has no attribute "fizz"
conftest.py:8: error: "Config" has no attribute "fizz"
(请注意 pytest
还没有带有类型提示的版本,所以如果你想在本地实际重现错误,请按照 this comment 中的步骤安装一个分支)。
有时重新定义 class 进行类型检查可以提供快速帮助:
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)
然而,除了使代码混乱之外,subclassing 解决方法非常有限:添加例如
from pytest import Session
def pytest_sessionstart(session: Session) -> None:
session.config.fizz = "buzz"
会迫使我也覆盖 Session
进行类型检查。
解决此问题的最佳方法是什么? Config
是一个例子,但我通常在每个项目中还有几个(针对测试 collection/invocation/reporting 等的项目特定调整)。我可以想象编写我自己的 pytest
存根版本,但是我需要为每个项目重复这个,这非常乏味。
这样做的一种方法是设法让您的 Config
对象定义 __getattr__
和 __setattr__
方法。如果这些方法在 class 中定义,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__
到 return 或接受 str
Any
分别获得更紧密的类型。
不幸的是,您仍然需要执行子类型化技巧或制作自己的存根 - 这给您带来的唯一好处是您至少不必列出每个自定义 属性 你想设置并使创建真正可重用的东西成为可能。这可能会让您更喜欢这个选项,不确定。
您可以探索的其他选项包括:
- 只需在使用临时 属性 的每一行添加一个
# type: ignore
注释。这将是一种抑制错误消息的精确方法,如果是侵入性的。 - 键入您的
pytest_configure
和pytest_unconfigure
,以便它们接受Any
类型的对象。这是抑制错误消息的一种侵入性较小的方法。如果你想最小化使用Any
的爆炸半径,你可以将任何想要使用这些自定义属性的逻辑限制在它们自己的专用函数中,并在其他任何地方继续使用Config
。 - 尝试改用转换。例如,在
pytest_configure
中你可以做config = cast(MutableConfig, config)
其中MutableConfig
是一个 class 你写了 subclasses_pytest.Config
并且定义了两个__getattr__
和__setattr__
。这可能是上述两种方法之间的中间立场。 - 如果将临时属性添加到
Config
和类似的 classes 是一种常见的事情,也许可以尝试说服 pytest 维护者包含仅输入__getattr__
和__setattr__
类型提示中的定义——或者其他一些更专门的方式让用户添加这些动态属性。
您可以通过一个 dict
的新属性扩展 Config
class 并存储所有自定义信息。例如:
def pytest_configure(config: Config) -> None:
config.data["fizz"] = "buzz" # `data` is the custom dict
这样一来,一个自定义存根文件就适合您的所有项目。当然,它不会立即帮助您的旧项目,因为您需要重写相关部分以使用 data['fizz']
而不是 fizz
。然而,使用 dict
的另一个好处是它可以防止现有属性和自定义属性之间可能发生的名称冲突。
如果将自定义数据附加到 Config
对象是常见的做法,也许值得尝试以数据字典的形式对其进行标准化,并在 pytest
项目中打开相应的问题。
如果您不喜欢重写代码但仍想使用静态类型检查器,您仍然可以使用从某个模板生成的自定义每个项目存根文件。您可以将所有自定义属性直接列为自定义 class 上的注释,然后让脚本从中生成相应的存根文件:
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.