Проверка типов динамически добавляемых атрибутов
При написании для конкретного проекта 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
заглушки, но тогда мне придется повторять это для каждого проекта, что очень утомительно.
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"
Если вы точно знаете, что ваши специальные свойства всегда будут 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__
определения в их подсказках типа - или какой-либо другой более специализированный способ позволить пользователям добавлять эти динамические свойства.
Вы можете продлить Config
класс одним новым атрибутом, который является dict
и хранит всю пользовательскую информацию. Например:
def pytest_configure(config: Config) -> None:
config.data["fizz"] = "buzz" # `data` is the custom dict
Таким образом, один настраиваемый файл-заглушка подходит для всех ваших проектов. Конечно, это не поможет вашим старым проектам сразу, так как вам нужно будет переписать соответствующие части, чтобы использоватьdata['fizz']
вместо того fizz
. Однако дополнительное преимущество использованияdict
заключается в том, что он предотвращает возможные конфликты имен между уже существующими и настраиваемыми атрибутами.
Если прикрепить пользовательские данные к Config
объекты - обычная практика, возможно, стоит попытаться стандартизировать это в форме data dict и открыть соответствующую проблему в pytest
проект.
Если вам не нравится переписывать код, но, тем не менее, вы хотите использовать средство проверки статического типа, вы все равно можете использовать собственные файлы-заглушки для каждого проекта, созданные на основе какого-либо шаблона. Вы можете перечислить все настраиваемые атрибуты непосредственно в виде аннотаций к настраиваемому классу, а затем заставить сценарий сгенерировать из него соответствующий файл-заглушку:
from _pytest.config import Configas _Config
class Config(_Config):
fizz: str
# The above code can be used by a script to generate custom stub files.