У меня есть декоратор класса, который добавляет несколько функций и полей в украшенный класс.
@mydecorator
@dataclass
class A:
a: str = ""
Добавлена (через setattr()
) функция .save()
и набор информации для полей класса данных в виде отдельного словаря.
Я бы хотел, чтобы VScode и mypy правильно распознали это, чтобы при использовании:
a=A()
a.save()
или a.my_fields_dict
эти 2 правильно распознаны.
Есть ли способ сделать это? Может быть, изменить аннотации типа класса A
во время выполнения?
То, что вы пытаетесь сделать, невозможно с текущей системой типов.
Если атрибуты и методы, которые вы добавляете в класс через декоратор, являются статическими (в том смысле, что они известны не только во время выполнения), то то, что вы описываете, фактически является расширением любого данного класса T
путем смешивания протоколP
. Этот протокол определяет метод save
и так далее.
Чтобы аннотировать это, вам понадобится пересечение T & P
. Это будет выглядеть примерно так:
from typing import Protocol, TypeVar
T = TypeVar("T")
class P(Protocol):
@staticmethod
def bar() -> str: ...
def dec(cls: type[T]) -> type[Intersection[T, P]]:
setattr(cls, "bar", lambda: "x")
return cls # type: ignore[return-value]
@dec
class A:
@staticmethod
def foo() -> int:
return 1
Вы могли заметить, что импорт Intersection
явно отсутствует. Это потому, что, несмотря на то, что это одна из наиболее востребованных функций для системы типов Python, на сегодняшний день она все еще отсутствует. В настоящее время нет способа выразить эту концепцию в типизации Python.
Единственный обходной путь на данный момент — это пользовательская реализация вместе с соответствующим плагином для проверки типов по вашему выбору. Я только что наткнулся на пакет typing-protocol-intersection, который делает именно это для mypy
.
Если вы установите это и добавите plugins = typing_protocol_intersection.mypy_plugin
в свою конфигурацию mypy
, вы можете написать свой код следующим образом:
from typing import Protocol, TypeVar
from typing_protocol_intersection import ProtocolIntersection
T = TypeVar("T")
class P(Protocol):
@staticmethod
def bar() -> str: ...
def dec(cls: type[T]) -> type[ProtocolIntersection[T, P]]:
setattr(cls, "bar", lambda: "x")
return cls # type: ignore[return-value]
@dec
class A:
@staticmethod
def foo() -> int:
return 1
Но тут мы сталкиваемся со следующей проблемой. Проверка этого с помощью reveal_type(A.bar())
через mypy
даст следующее:
error: "Type[A]" has no attribute "bar" [attr-defined]
note: Revealed type is "Any"
Но если мы сделаем это вместо этого:
class A:
@staticmethod
def foo() -> int:
return 1
B = dec(A)
reveal_type(B.bar())
мы не получаем жалоб от mypy
и note: Revealed type is "builtins.str"
. Хотя то, что мы делали раньше, было эквивалентно!
Это не баг плагина, а mypy
внутренностей. Это еще одна давняя проблема, которая mypy
неправильно обрабатывает декораторы классов.
Человек в этой ветке проблемы даже упомянул ваш вариант использования в сочетании с желаемым типом пересечения.
Другими словами, вам просто нужно подождать, пока эти две дыры не будут залатаны. Или вы можете надеяться, что, по крайней мере, проблема с декоратором от mypy
будет исправлена в ближайшее время, и тем временем напишите свой собственный плагин VSCode для типов пересечений. Может быть, вы сможете встретиться с человеком, стоящим за этим mypy
плагином, о котором я упоминал выше.
Это не ошибка плагина, а внутренностей mypy. Вы уверены? На первый взгляд, я полагаю, что добавления get_class_decorator_hook
к ProtocolIntersectionPlugin
должно быть достаточно. Я расследую это и, если возможно, PR, потому что это выглядит очень здорово.
@SUTerliakov Согласен, выглядит многообещающе. Но проверьте проблему mypy
, которую я связал. И попробуйте простой декоратор классов без плагина. Он все еще сломан.
Да, недавно я пытался заставить mypy
работать с простым декоратором классов, но это все еще ошибка. Может быть, крюк сможет это вылечить... После расследования я оставлю здесь записку.
@DaniilFajnberg хм, интересно, спасибо. У меня уже есть протокол для моей добавленной части. Жаль пока нет перекрестка.
Подумайте о том, что вы говорите. Вы хотите изменить аннотации во время выполнения, но статический контролер типов должен знать о них? Статические проверки типов не выполняют ваш код, они просто читают его.