Tpye подсказки для миксинов, ссылающихся на атрибуты стороннего базового класса
Я пытаюсь добавить подсказки типов в класс-миксин, который должен использоваться вместе с внешним, сторонним классом. То есть миксин опирается на члены стороннего класса.
Например, миксин для формы Django:
# mixin_typing.py
from django import forms
class SuffixFormMixin:
suffix: str
def add_suffix(self, field_name: str) -> str:
# prefix is an attribute of forms.Form
return f"{self.prefix}__{field_name}__{self.suffix}"
class SuffixForm(SuffixFormMixin, forms.Form):
pass
Понятно, что mypy будет жаловаться на метод add_suffix
:
"SuffixFormMixin" has no attribute "prefix"
IDE (PyCharm) также выдаст предупреждение:
Unresolved attribute reference 'prefix' for class 'SuffixFormMixin'
Вопрос:
Есть ли какое-нибудь "простое" решение, позволяющее миксину понять, что self
содержит атрибуты/методы forms.Form
?
Вот вопрос на github, в котором это рассматривается, но, к сожалению, он никуда не делся: https://github.com/python/typing/issues/246
Может быть, какой-то объект типизации или другое mypy-фу, которое действует как обещание классу-миксину, что будущий "класс-партнер" имеет члены, которые использует миксин?
Попытки решения
Все предложения, которые я нашел до сих пор, имеют недостатки:
Типовая подсказка себя классу-партнеру
(работает для mypy: ✅, не работает для IDE: ❌)
Я встречал предложения вписать hint self в класс, с которым впоследствии будет использоваться миксин. Здесь forms.Form
:
def add_suffix(self: forms.Form, field_name: str) -> str:
return f"{self.prefix}__{field_name}__{self.suffix}"
mypy больше не жалуется (хотя должен бы?), но IDE все еще жалуется. На этот раз по поводу атрибута suffix
:
Unresolved attribute reference 'suffix' for class 'Form'
Типизация себя к будущему конкретному классу
(mypy: ❌, IDE: ✅)
Это идет немного дальше, чем предыдущее предложение:
class SuffixFormMixin:
suffix: str
def add_suffix(self: "SuffixForm", field_name: str) -> str:
return f"{self.prefix}__{field_name}__{self.suffix}"
class SuffixForm(SuffixFormMixin, forms.Form):
pass
При этом IDE может правильно разрешить все атрибуты, но mypy выбрасывает ошибку:
The erased type of self "mixin_typing.SuffixForm" is not a supertype of its class "mixin_typing.SuffixFormMixin"
Приведение себя к типу Union
(mypy: ❌, IDE: ✅)
def add_suffix(self: Union["SuffixFormMixin", forms.Form], field_name: str) -> str:
return f"{self.prefix}__{field_name}__{self.suffix}"
Опять же, IDE понимает, в чем дело, и может правильно разрешить атрибуты, но mypy жалуется:
Item "SuffixFormMixin" of "SuffixFormMixin | Any" has no attribute "prefix"
Переход от типа self к TypeVar
(mypy: ✅, IDE: ❌)
Это предлагает объявить будущий базовый класс как TypeVar и использовать его как подсказку для self.
FormType = TypeVar("FormType", bound=forms.Form)
...
def add_suffix(self: FormType, field_name: str) -> str:
return f"{self.prefix}__{field_name}__{self.suffix}"
Это не генерирует никаких предупреждений, но кажется, что это скорее потому, что, используя TypeVar, я скрываю типы, а не потому, что это правильное решение? Например, можно сделать так, чтобы ни mypy, ни IDE не жаловались:
FormType = TypeVar("FormType", bound=forms.Form)
class SuffixFormMixin:
suffix: str
def this_will_explode(self: FormType) -> Any:
return self.suffix + 1 # str + int!
Полагаю, что тип для self.suffix
принимается за Any
, поскольку он больше не может быть определен? IDE также не показывает никакой информации о self.suffix
при наведении.
Это тоже не совсем подходит, учитывая, что mypy абсолютно точно должен поймать подобную проблему в методе this_will_explode
.
Ввод протокола
(mypy: ✅, IDE: ✅)
Объявление протокола и наследование его миксином, как описано здесь, выглядит наиболее полным решением. Протокол описывает биты класса-партнера, который использует миксин.
# mixin_typing.py
from typing import Protocol
from django import forms
class HasPrefix(Protocol):
prefix: str
class SuffixFormMixin(HasPrefix):
suffix: str
def add_suffix(self, field_name: str) -> str:
return f"{self.prefix}__{field_name}__{self.suffix}"
class SuffixForm(SuffixFormMixin, forms.Form):
pass
mypy счастлив, IDE знает типы, но...:
- это требует добавления классов
- протокол должен отражать миксины партнерского класса
forms.Form
, над которыми у меня нет контроля - протокол "блокирует" переход IDE к фактической реализации на родительском классе (т.е. ctrl + клик на
self.prefix
переводит наHasPrefix.prefix
, а не наforms.Form
) - фактически не запускается
При создании класса SuffixForm
возникает ошибка:
>>> import mixin_typing
Traceback (most recent call last):
File "", line 36, in <module>
class SuffixForm(SuffixFormMixin, forms.Form):
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
Так что вам нужен обходной путь, который переключает базовый класс миксина в зависимости от того, проверяем ли мы в данный момент тип:
class HasPrefix(Protocol):
prefix: str
if TYPE_CHECKING:
_Base = HasPrefix
else:
_Base = object
class SuffixFormMixin(_Base):
suffix: str
def add_suffix(self, field_name: str) -> str:
return f"{self.prefix}__{field_name}__{self.suffix}"
Мне это не нравится. Это еще больше кода, который служит только для проверки типов, и теперь вам нужно объяснить, почему миксин переключает базовый класс. Может быть, это будет лучше работать в файле-заглушке?
Обязательное наследование от класса-партнера
(mypy: ✅, IDE: ✅)
Как было предложено в одном из комментариев:
Вы также можете использовать свой последний подход без протокола: условно использовать forms.Form или object в качестве базового класса. При этом все равно "нужно объяснить, почему база класса динамическая", но в случае TYPE_CHECKING это должно быть довольно очевидно.
if TYPE_CHECKING:
_Base = forms.Form
else:
_Base = object
class SuffixFormMixin(_Base):
suffix: str
def add_suffix(self, field_name: str) -> str:
return f"{self.prefix}__{field_name}__{self.suffix}"
mypy это не нравится:
mixin_typing.py:18: error: Variable "mixin_typing._Base" is not valid as a type [valid-type]
class SuffixFormMixin(_Base):
^
mixin_typing.py:18: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
mixin_typing.py:18: error: Invalid base class "_Base" [misc]
class SuffixFormMixin(_Base):
Однако добавление подсказки типа TypeAlias
к _Base
заставляет его работать (объясняется здесь):
if TYPE_CHECKING:
_Base: TypeAlias = forms.Form
else:
_Base = object
Возможно, я многое делаю неправильно (многие вещи, связанные с более глубокими подсказками, пролетают мимо моей головы), или, возможно, я упускаю что-то очевидное (это не должно быть так сложно), поэтому я буду рад любым предложениям.