Проверка типов динамически добавляемых атрибутов

При написании для конкретного проекта 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.
Другие вопросы по тегам