Ошибка MyPy при использовании переменных подкласса в метаклассе

#python #python-3.x #mypy #metaclass

Вопрос:

Я должен использовать метакласс и должен получить доступ к переменной подкласса внутри новой функции (так что это, по сути, абстрактное свойство). Это, конечно, приводит к ошибкам MyPy, потому что «тип» не имеет атрибутов с именем FILE_PATH или АДРЕСА. Есть ли здесь какой-нибудь обходной путь?

 class metaClass(type):

  def __new__(cls, name, bases, dct):
      x = super().__new__(cls, name, bases, dct)

      if not hasattr(x, "FILE_PATH") or not hasattr(x, "ADDRESS"):
          raise NotImplementedError("FILE_PATH and ADDRESS not set")

      #do stuff with x.FILE_PATH and x.ADDRESS
      path = x.FILE_PATH  #  <- "mypy --strict" errors here
      ...
      return x

class subClass(metaclass=metaClass):
    FILE_PATH = "file.type"
    ADDRESS = "sadfsadfa"
 

Обновить:

Решается с помощью dct['FILE_PATH'] и dct['ADDRESS'] для доступа к переменным, заданным в дочернем классе.

Комментарии:

1. Делает ли установка x.FILE_PATH и x.ADDRESS фиктивные значения перед созданием исключения mypy счастливыми?

2. (Однако я, похоже, не могу воспроизвести проблему.)

3. Кроме того, subClass на самом деле это не подкласс, или, скорее , это всего лишь подкласс object , как и любой другой класс без явного родительского класса.

4. Установка фиктивных значений подавляет предупреждения в PyCharm, но MyPy все еще недоволен этим.

5. Можете ли вы предоставить точный рецепт , который вызывает проблему (включая то, как вы вызываете mypy , с любыми необходимыми опциями)?

Ответ №1:

Проверка наличия атрибутов в созданном классе dct , как в описанном обходном пути, имеет один большой недостаток, который может быть применим или не применим к вашему проекту:

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

Другими словами:

 class Base(metaclass=metaClass):
    FILE_PATH = "file.type"
    ADDRESS = "sadfsadfa"

class Sub(Base):
    pass
 

Этот код не пройдет вашу проверку , но пройдет проверку hasattr , так как он делает правильные вещи при поиске атрибутов.

Я думаю, что это может быть одним из немногих мест, где использование typing.cast будет не просто быстрым и грязным способом обмануть статическую проверку , а, скорее, правильным решением: поскольку статическая проверка не может знать, какой тип возвращается вызовом type.__new__ , мы используем typing.cast , чтобы сделать это явным.

В качестве дополнительной вещи, чтобы заставить его работать, MyPy должен знать, что экземпляры metaClass должны иметь эти атрибуты. Я нашел способ сделать это, чтобы аннотировать нужные атрибуты в самом метаклассе.

В любом случае, это проходит mypy --strict

 import typing as T

class metaClass(type):

    FILE_PATH: str
    ADDRESS: str

    def __new__(mcls, name: str, bases: tuple[type, ...], dct: dict[str, T.Any]) -> "metaClass":
        x = super().__new__(mcls, name, bases, dct)

        if not hasattr(x, "FILE_PATH") or not hasattr(x, "ADDRESS"):
            raise NotImplementedError("FILE_PATH and ADDRESS not set")

        cls = T.cast(metaClass, x)
        #do stuff with x.FILE_PATH and x.ADDRESS
        a = cls.FILE_PATH
        return cls


class Base(metaclass=metaClass):
    FILE_PATH = "file.type"
    ADDRESS = "sadfsadfa"
 

(Я сделал некоторое переименование, чтобы избежать путаницы — как подКласс-> База, так как «подКласс» не является подклассом метакласса, и первый параметр to > metaclass.__new__ to mcls : так как это метакласс, и его легче читать, если мы зарезервируем cls в качестве имени для создаваемого класса)


Еще один вариант, поскольку MyPY явно не может понять механизм метакласса, состоит в том, чтобы пометить вещи внутри метода метакласса, которые MyPy будет игнорировать с # type: ignore помощью комментария. cast Однако это, по-видимому, действительно правильно в данном случае.