Обойти mypy «Модуль не имеет атрибута» при установке атрибута Dynami c - PullRequest
0 голосов
/ 18 марта 2020

У меня есть следующий код на a.py:

class Tags(enum.Flag):
    NONE = 0

    A = enum.auto()
    B = enum.auto()
    C = enum.auto()

# Allow using tags.A instead of tags.Tags.A
globals().update(Tags.__members__)

Но когда я использую его в других файлах, mypy (по праву) не может определить атрибуты:

import tags
print(tags.A)  # Module has no attribute "A"

Есть ли возможный способ обойти это в Python 3,6?

Известные решения (которые не достаточно хороши для моего случая):

  • Используйте # type: ignore каждый раз, когда вы используете tags.A
  • Использование tags.Tags.A
  • Использование __getitem__ на уровне модуля (работает только с Python 3.7)

1 Ответ

1 голос
/ 18 марта 2020

Лично я бы изменил свой импорт следующим образом:

from tags import Tags

.. что позволило бы мне ссылаться на элементы перечисления, просто набрав Tags.A везде с минимальным суетой.


Но если вы действительно хотите продолжать ссылаться на эти элементы из пространства имен модуля, один из подходов состоит в определении __getattr__ функции в tags.py:

class Tags(enum.Flag):
    NONE = 0

    A = enum.auto()
    B = enum.auto()
    C = enum.auto()

def __getattr__(name: str) -> Tags:
    return Tags[name]

Во время выполнения Python попытается вызвать эту функцию, если вы попытаетесь получить доступ к некоторому атрибуту, который не был определен напрямую в объекте модуля tags. Mypy понимает это соглашение и предполагает, что если определено __getattr__, модуль является «неполным». Таким образом, он будет использовать возвращаемый тип в качестве типа для любых отсутствующих атрибутов, к которым вы пытаетесь получить доступ.

Возможно, вы все равно захотите сделать globals().update(Tags.__members__) в основном для оптимизации производительности, чтобы пропустить необходимость фактического вызова __getattr__ функция во время выполнения.

Эта стратегия действительно работает только в том случае, если tags.py содержит только одно перечисление - в противном случае вам нужно сделать тип возвращаемого значения что-то вроде Union[Tags, MyOtherEnum] (что громоздко) или даже просто Any (что теряет преимущества использования средства проверки типов).

Эта стратегия также означает, что mypy не сможет делать более сложные выводы и сужения типов, принимая во внимание фактическое значение перечисления. Это, в основном, уместно, только если вы используете буквальные перечисления.


Если это проблемы, вам, возможно, придется прибегнуть к более грубому подходу, например:

from typing import TYPE_CHECKING

class Tags(enum.Flag):
    NONE = 0

    A = enum.auto()
    B = enum.auto()
    C = enum.auto()

globals().update(Tags.__members__)

if TYPE_CHECKING:
    NONE = Tags.NONE
    A = Tags.A
    B = Tags.B
    C = Tags.C

Константа TYPE_CHECKING всегда ложна во время выполнения, но считается всегда истинной для проверок типов.

Но если вы собираетесь go за счет прямого сообщения mypy о каждом из эти варианты, вы могли бы просто пропустить попытку автоматического обновления глобалов и сделать это вместо этого:

class Tags(enum.Flag):
    NONE = 0

    A = enum.auto()
    B = enum.auto()
    C = enum.auto()

NONE = Tags.NONE
A = Tags.A
B = Tags.B
C = Tags.C

Очевидно, что повторять себя дважды, как это, довольно неоптимально, но я не думаю, что есть легкий способ обойти это. Возможно, вы могли бы смягчить это, создав сценарий, который автоматически генерирует tags.py для вас, но это также довольно неоптимально, просто по разным причинам.

...