Проверка типов динамически добавленных атрибутов - PullRequest
5 голосов
/ 14 апреля 2020

Когда я пишу специфичные для проекта плагины c 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 пока нет выпуска с подсказками типов, поэтому, если вы действительно хотите воспроизвести ошибку локально, установите форк, следуя инструкциям в в этом комментарии ).

Иногда переопределение класса для Проверка типов может предложить быструю помощь:

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 является одним из примеров, но у меня обычно есть еще несколько в каждом проекте (настройки проекта c для сбора тестов / вызова / отчетности и т. Д. c). Я мог бы представить свою собственную версию заглушек pytest, но тогда мне нужно было бы повторять это для каждого проекта, что очень утомительно.

Ответы [ 2 ]

1 голос
/ 15 апреля 2020

Вы можете расширить класс Config с помощью одного нового атрибута, который является dict и хранит всю пользовательскую информацию. Например:

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

Таким образом, один специальный файл-заглушка подходит для всех ваших проектов. Конечно, это не поможет вашим старым проектам сразу, потому что вам нужно будет переписать соответствующие части, чтобы использовать data['fizz'] вместо fizz. Однако дополнительное преимущество использования dict состоит в том, что он предотвращает возможные конфликты имен между уже существующими и настраиваемыми атрибутами.

Если присоединение пользовательских данных к Config объектам является обычной практикой, возможно, стоит попытаться стандартизировать это в форме данных и откройте соответствующую проблему в проекте pytest.

Если вам не нравится переписывать код, но, тем не менее, вы хотите использовать средство проверки типа stati c, вы все равно можете использовать пользовательский Заглушки для каждого проекта, сгенерированные из некоторого шаблона. Вы можете перечислить все пользовательские атрибуты непосредственно в виде аннотаций для пользовательского класса, а затем создать сценарий для создания из него соответствующего файла-заглушки:

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.
1 голос
/ 15 апреля 2020

Один из способов сделать это - придумать, чтобы ваш 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"

Если вы знаете, для Если ваши ad-ho c свойства всегда будут strs или чем-то еще, вы можете набрать __getattr__ и __setattr__, чтобы вернуть или принять str вместо Any соответственно, чтобы получить более узкие типы.

К сожалению, вам все равно придется выполнить трюк с подтипами или возиться с созданием своих собственных заглушек - единственное преимущество, которое это дает вам, заключается в том, что вам, по крайней мере, не придется перечислять каждое отдельное свойство, которое вы хотите установить, и делать его Можно создать что-то действительно многоразовое. Возможно, это может сделать этот вариант более приемлемым для вас, не уверен.

Другие варианты, которые вы можете изучить, включают:

  • Просто добавьте комментарий # type: ignore к каждой строке, где вы используете ad-ho c собственность. Это был бы несколько точный, но навязчивый способ подавления сообщений об ошибках.
  • Введите ваши pytest_configure и pytest_unconfigure, чтобы они принимали объекты типа Any. Это было бы несколько менее навязчивым способом подавления сообщений об ошибках. Если вы хотите минимизировать радиус взрыва при использовании Any, вы можете ограничить любой лог c, который хочет использовать эти пользовательские свойства, своими собственными выделенными функциями и продолжать использовать Config везде.
  • Попробуйте вместо этого использовать кастинг. Например, внутри pytest_configure вы можете сделать config = cast(MutableConfig, config), где MutableConfig - это класс, который вы написали, который подклассов _pytest.Config и определяет как __getattr__, так и __setattr__. Возможно, это среднее между двумя вышеупомянутыми подходами.
  • Если добавление атрибутов ad-ho c к Config и аналогичным классам является обычным делом, возможно, попробуйте убедить сопровождающих pytest в включите определения __getattr__ и __setattr__ только для ввода в свои подсказки типов - или какой-то другой, более специализированный способ позволить пользователям добавлять эти динамические c свойства.
...