1
1
from __future__ import annotations
2
2
3
3
from abc import ABC
4
- from typing import TYPE_CHECKING , Any , ClassVar , Literal
4
+ from typing import TYPE_CHECKING , Literal
5
+ from weakref import WeakValueDictionary
5
6
6
7
from 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
+
8
14
9
15
10
16
class ConfiguredBaseModel (BaseModel , ABC ):
@@ -29,11 +35,24 @@ def model_dump_json(self, **kwargs: Any) -> str:
29
35
return super ().model_dump_json (** kwargs )
30
36
31
37
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
+
32
49
if TYPE_CHECKING :
33
50
# Because we can't override a BaseModel's metaclass __getitem__ method without angering Pydantic during runtime,
34
51
# we define this stub here to satisfy static type checkers that introspect the metaclass method annotations
35
52
# to determine expected types in the class subscriptions.
36
53
54
+ from collections .abc import Callable
55
+
37
56
from pydantic ._internal ._model_construction import ModelMetaclass # type: ignore[import]
38
57
39
58
class EventMeta (ModelMetaclass ):
@@ -42,12 +61,32 @@ class EventMeta(ModelMetaclass):
42
61
def __getitem__ (cls , event_names : LiteralString | tuple [LiteralString , ...]) -> type [EventBase ]: ...
43
62
44
63
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
+ """
46
81
event : LiteralString
47
82
"""Name of the event used to identify what occurred.
48
83
49
84
Subclass models must define this field as a Literal type with the event name string that the model represents.
50
85
"""
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."""
51
90
52
91
@classmethod
53
92
def get_model_event_names (cls ) -> tuple [str , ...]:
@@ -56,9 +95,27 @@ def get_model_event_names(cls) -> tuple[str, ...]:
56
95
57
96
else :
58
97
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."""
62
119
63
120
event : str
64
121
"""Name of the event used to identify what occurred.
@@ -106,25 +163,30 @@ def __class_getitem__(cls: type[EventBase], event_names: LiteralString | tuple[L
106
163
def __new_subscripted_base__ (cls : type [EventBase ], new_name : str , event_name_args : tuple [str , ...]) -> type [EventBase ]:
107
164
"""Dynamically create a new Singleton subclass of EventBase with the given event names for the event field.
108
165
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.
110
167
The new subclasses created here will be ignored in the __init_subclass__ method.
111
168
"""
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 (
115
172
new_name ,
116
173
__base__ = cls ,
117
- __args__ = ( tuple [ str , ...], event_name_args ),
174
+ __event_metadata__ = ( EventMetadataDict , { "origin" : cls , "args" : event_name_args } ),
118
175
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.
120
177
)
121
178
122
- return cls ._subtypes [new_name ]
179
+ return cls ._cached_specialized_submodels [new_name ]
123
180
124
181
@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 :
128
190
# This is a subscripted subclass of EventBase, so we don't need to do anything.
129
191
return
130
192
@@ -147,4 +209,4 @@ def __event_type__(cls) -> type[object] | None:
147
209
@classmethod
148
210
def get_model_event_names (cls ) -> tuple [str , ...]:
149
211
"""Return the event names for the event model."""
150
- return cls .__args__
212
+ return cls .__event_metadata__ [ "args" ]
0 commit comments