11from __future__ import annotations
22
33from abc import ABC
4- from typing import TYPE_CHECKING , Any , ClassVar , Literal
4+ from typing import TYPE_CHECKING , Literal
5+ from weakref import WeakValueDictionary
56
67from pydantic import BaseModel , ConfigDict , create_model
7- from typing_extensions import LiteralString , override # noqa: UP035
8+ from typing_extensions import LiteralString , TypedDict , override # noqa: UP035
9+
10+
11+ if TYPE_CHECKING :
12+ from typing import Any , ClassVar
13+
814
915
1016class ConfiguredBaseModel (BaseModel , ABC ):
@@ -29,11 +35,24 @@ def model_dump_json(self, **kwargs: Any) -> str:
2935 return super ().model_dump_json (** kwargs )
3036
3137
38+ class EventMetadataDict (TypedDict ):
39+ """Metadata for specialized EventBase submodels.
40+
41+ Similar to the __pydantic_generic_metadata__ attribute, but for use in the EventBase class——which isn't actually generic.
42+ """
43+ origin : type [EventBase ]
44+ """Origin class of the specialized EventBase submodel."""
45+ args : tuple [str , ...]
46+ """Event names for the specialized EventBase submodel."""
47+
48+
3249if TYPE_CHECKING :
3350 # Because we can't override a BaseModel's metaclass __getitem__ method without angering Pydantic during runtime,
3451 # we define this stub here to satisfy static type checkers that introspect the metaclass method annotations
3552 # to determine expected types in the class subscriptions.
3653
54+ from collections .abc import Callable
55+
3756 from pydantic ._internal ._model_construction import ModelMetaclass # type: ignore[import]
3857
3958 class EventMeta (ModelMetaclass ):
@@ -42,12 +61,32 @@ class EventMeta(ModelMetaclass):
4261 def __getitem__ (cls , event_names : LiteralString | tuple [LiteralString , ...]) -> type [EventBase ]: ...
4362
4463 class EventBase (BaseModel , metaclass = EventMeta ):
45- """Base class for all event models."""
64+ """Base class for all event models.
65+
66+ EventBase itself should not be instantiated, nor should it be subclassed directly.
67+ Instead, use a subscripted subclass of EventBase, e.g. `EventBase["eventName"]`, to subclass from.
68+
69+ Examples:
70+ ```
71+ class KeyDown(EventBase["keyDown"]):
72+ # 'event' field's type annotation is internally set here as `Literal["keyDown"]`
73+ ...
74+
75+ class TestEvent(EventBase["test", "testing"]):
76+ # 'event' field's type annotation is internally set here as `Literal["test", "testing"]`
77+ ...
78+
79+ ```
80+ """
4681 event : LiteralString
4782 """Name of the event used to identify what occurred.
4883
4984 Subclass models must define this field as a Literal type with the event name string that the model represents.
5085 """
86+ __event_metadata__ : ClassVar [EventMetadataDict ]
87+ """Metadata for specialized EventBase submodels."""
88+ __event_type__ : ClassVar [Callable [[], type [object ] | None ]]
89+ """Return the event type for the event model."""
5190
5291 @classmethod
5392 def get_model_event_names (cls ) -> tuple [str , ...]:
@@ -56,9 +95,27 @@ def get_model_event_names(cls) -> tuple[str, ...]:
5695
5796else :
5897 class EventBase (ConfiguredBaseModel , ABC ):
59- """Base class for all event models."""
60- _subtypes : ClassVar [dict [str , type [EventBase ]]] = {}
61- __args__ : ClassVar [tuple [str , ...]]
98+ """Base class for all event models.
99+
100+ EventBase itself should not be instantiated, nor should it be subclassed directly.
101+ Instead, use a subscripted subclass of EventBase, e.g. `EventBase["eventName"]`, to subclass from.
102+
103+ Examples:
104+ ```
105+ class KeyDown(EventBase["keyDown"]):
106+ # 'event' field's type annotation is internally set here as `Literal["keyDown"]`
107+ ...
108+
109+ class TestEvent(EventBase["test", "testing"]):
110+ # 'event' field's type annotation is internally set here as `Literal["test", "testing"]`
111+ ...
112+
113+ ```
114+ """
115+ # A weak reference dictionary to store subscripted subclasses of EventBase. Weak references are used to minimize memory usage.
116+ _cached_specialized_submodels : ClassVar [WeakValueDictionary [str , type [EventBase ]]] = WeakValueDictionary ()
117+ __event_metadata__ : ClassVar [EventMetadataDict ]
118+ """Metadata for specialized EventBase submodels."""
62119
63120 event : str
64121 """Name of the event used to identify what occurred.
@@ -106,25 +163,30 @@ def __class_getitem__(cls: type[EventBase], event_names: LiteralString | tuple[L
106163 def __new_subscripted_base__ (cls : type [EventBase ], new_name : str , event_name_args : tuple [str , ...]) -> type [EventBase ]:
107164 """Dynamically create a new Singleton subclass of EventBase with the given event names for the event field.
108165
109- Only create a new subscripted subclass if it doesn't already exist in the _subtypes dictionary, otherwise return the existing subclass.
166+ Only create a new subscripted subclass if it doesn't already exist in the _cached_specialized_submodels dictionary, otherwise return the existing subclass.
110167 The new subclasses created here will be ignored in the __init_subclass__ method.
111168 """
112- if new_name not in cls ._subtypes :
113- # Pass in the _is_base_subtype kwarg to __init_subclass__ to indicate that this is a base subtype of EventBase, and should be ignored .
114- cls ._subtypes [new_name ] = create_model (
169+ if new_name not in cls ._cached_specialized_submodels :
170+ # Make sure not to pass in a value `_cached_specialized_submodels` in the create_model() call, in order to avoid shadowing the class variable .
171+ cls ._cached_specialized_submodels [new_name ] = create_model (
115172 new_name ,
116173 __base__ = cls ,
117- __args__ = ( tuple [ str , ...], event_name_args ),
174+ __event_metadata__ = ( EventMetadataDict , { "origin" : cls , "args" : event_name_args } ),
118175 event = (Literal [event_name_args ], ...),
119- __cls_kwargs__ = {"_is_base_subtype " : True },
176+ __cls_kwargs__ = {"_is_specialized_base " : True }, # This gets passed to __init_subclass__ as a kwarg to indicate that this is a specialized (subscripted) subclass of EventBase.
120177 )
121178
122- return cls ._subtypes [new_name ]
179+ return cls ._cached_specialized_submodels [new_name ]
123180
124181 @classmethod
125- def __init_subclass__ (cls , _is_base_subtype : bool = False ) -> None :
126- """Validate a child class of EventBase (not a subscripted base subclass) is subclassing from a subscripted EventBase."""
127- if _is_base_subtype :
182+ def __init_subclass__ (cls , _is_specialized_base : bool = False ) -> None :
183+ """Validate a child class of EventBase (not a subscripted base subclass) is subclassing from a subscripted EventBase.
184+
185+ Args:
186+ _is_specialized_base: Whether this is a specialized submodel of EventBase (i.e., a subscripted subclass).
187+ This should only be True for the subscripted subclasses created in __class_getitem__.
188+ """
189+ if _is_specialized_base :
128190 # This is a subscripted subclass of EventBase, so we don't need to do anything.
129191 return
130192
@@ -147,4 +209,4 @@ def __event_type__(cls) -> type[object] | None:
147209 @classmethod
148210 def get_model_event_names (cls ) -> tuple [str , ...]:
149211 """Return the event names for the event model."""
150- return cls .__args__
212+ return cls .__event_metadata__ [ "args" ]
0 commit comments