from __future__ import annotations
import logging
from datetime import datetime, UTC
from typing import Any, Generic, Self, Protocol, TypeVar
from pydantic import AwareDatetime, BaseModel
logger = logging.getLogger(__name__)
EventDataT_co = TypeVar('EventDataT_co')
class Event(BaseModel, Generic[EventDataT_co]):
raised_at: AwareDatetime
data: tuple[EventDataT_co, ...]
@classmethod
def from_data(cls, *data: EventDataT_co) -> Self:
return cls(raised_at=datetime.now(UTC), data=data)
class EventRepository(Protocol):
def save_events_unsafe(self, *events: Event[Any]) -> None: # same as `*events: Event[Any]`
"""not type-safe (someone might make unsafe assumption on `.data`)"""
def save_events_safe(self, *events: Event[object]) -> None:
"""Only allowed if Event is covariant, but then I can't have a custom constructor"""
def save_events_gen(self, *events: Event[EventDataT_co]) -> None:
"""It implies this is a generic function, when it's more `.data`-agnostic"""
class InheritedEventRepository(EventRepository):
def save_events_unsafe(self, *events: Event[Any]) -> None:
logger.info(str([data.id for event in events for data in event.data])) # Type-checker says ok, dev made an uncaught mistake
def save_events_safe(self, *events: Event[object]) -> None:
logger.info(str([data.id for event in events for data in event.data])) # Type-checker says error: "object" has no attribute "id" [attr-defined]
def save_events_gen(self, *events: Event[EventDataT_co]) -> None:
logger.info(str([data.id for event in events for data in event.data]))
# Type-checker error: "EventDataT_co" has no attribute "id" [attr-defined] if covariant
# No error if invariant
event_1 = Event.from_data(1)
event_2 = Event.from_data('foo')
from typing import reveal_type
print(reveal_type(event_1)) # Event[builtins.int]
print(reveal_type(event_2)) # Event[builtins.str]
InheritedEventRepository().save_events_unsafe(event_1, event_2)
InheritedEventRepository().save_events_safe(event_1, event_2)
# error: Argument 1 to "save_events_safe" of "InheritedEventRepository" has incompatible type "Event[int]"; expected "Event[object]" [arg-type] if invariant
# No error if covariant
InheritedEventRepository().save_events_gen(event_1, event_2)
# error: Cannot infer type argument 1 of "save_events_gen" of "InheritedEventRepository" [misc] if invariant
# No error if covariant
Mypy выдает:
ошибка: невозможно использовать переменную ковариантного типа в качестве параметра [разное]
в строке конструктора def from_data(...)
. Я не хочу делать Event
ковариантным, но, похоже, это единственный способ разрешить save_events_safe
быть принятым mypy? Я не хочу использовать Event[Any]
в save_events
, потому что save_events
будет подклассом (я бы не хотел, чтобы у разработчика, который наследует от него, не было безопасности при проверке типов). Наконец, save_events_gen(self, *events: Event[EventDataT_co])
имеет ту же проблему, что и save_events_safe
(только ковариантно)
Это означает, что я либо застрял на ковариантной версии, которая не позволяет использовать собственный конструктор (кроме того, Event действительно не следует использовать ковариантно вне этого случая), либо на инвариантной версии, которая заставляет меня использовать Any для агностические функции. Как мне это решить?
В результате этот класс станет Generic[Event[T]]
, то есть экземпляр этого класса может принимать только один конкретный тип события (например: Event[Foo]
). Это не сработает. Класс является агностическим, а не универсальным. По сути, мне следует использовать Event[Any]
, но это отключит проверку типов, и подкласс может случайно получить незаконный доступ к атрибуту. Следовательно Event[object]
:/
Возможно, вам поможет расширить пример, чтобы продемонстрировать проблему, которая возникает, если вы используете Any
. Мне неясно, как и почему этому классу вообще разрешен доступ data
, если он должен быть полностью независимым от своего типа.
Доступ к атрибутам данных запрещен, но он может получить доступ к данным. В том-то и дело, что я не могу контролировать, что будет делать другой разработчик при наследовании этого класса, если я использую Any
. object
предотвращает этот тип доступа. (Если это поможет понять, я создаю библиотеку, которая будет использоваться другими разработчиками).
Что значит «доступ» к данным, но не к их атрибутам??? Опять же, пример кода был бы очень полезен.
Доступ к данным: event.data
-> легально / Доступ к атрибуту данных: event.data.id
-> незаконно. Надеюсь, это прояснит вам ситуацию.
Вчера было мало времени, извини. Я обновил код. Надеюсь, это прояснит проблему. Как указано в моем исходном вопросе (он был отредактирован), я не очень хорошо разбираюсь в вариациях дженериков. Я читал документацию, я понимаю, но мне нелегко жонглировать этой концепцией. Последние 4 дня я потратил на создание небольшого репозитория, совместимого с mypy, и это был ад (особенно потому, что это библиотека, ориентированная на разработчиков). Попытка объяснить проблему в сжатой форме сама по себе была непростой задачей. Я также не уверен, что понимаю, чего не хватало изначально, но надеюсь, что исправленная версия поможет.
Я сам с этим сталкивался раньше, давайте изложим факты:
Это представление:
def save_events(*events: Event[T]): ...
Удовлетворяет всем этим условиям: быть «независимым от данных» и быть «универсальным» — это, по сути, одно и то же, если вы думаете об универсальном как о «независимом от свойств некоторого внутреннего раздела».
Следующий сегмент кода:
from __future__ import annotations
from datetime import datetime, UTC
from typing import Generic, Self, TypeVar
from pydantic import AwareDatetime, BaseModel
EventDataT = TypeVar('EventDataT')
class Event(BaseModel, Generic[EventDataT]):
raised_at: AwareDatetime
data: tuple[EventDataT, ...]
@classmethod
def from_data(cls, *data: EventDataT) -> Self:
return cls(raised_at=datetime.now(UTC), data=data)
EventDataT_co = TypeVar('EventDataT_co', covariant=True)
def save_events(*events: Event[EventDataT_co]): ...
class Test:
pass
save_events(Event[Test].from_data(Test()))
... проверка типов у меня отлично работает как в строгом режиме, так и в строгом режиме mypy. Введя новый тип var, мы можем сделать один ковариантным, а другой — нековариантным. Надеюсь это поможет!
Извиняюсь, я только что заметил, что вопрос обновился, пока я это писал, но думаю, что все это по-прежнему применимо.
Спасибо !!!! Я даже не знал, что смогу это сделать (переопределить новую TypeVar для другого использования).
Если класс, в котором определен
save_events
, также является универсальным (и ему требуетсяEvent[T]
), решает ли это проблему?