2020
2121if TYPE_CHECKING :
2222 from collections .abc import Sequence
23+ from types import ModuleType
2324
2425_enduring_object_init_classes : dict [type [Any ], type [Initialises ]] = {}
2526
@@ -72,7 +73,7 @@ def apply(self, obj: Perspective) -> None:
7273
7374 # Identify the function that was decorated.
7475 try :
75- decorated_func = cross_cutting_decorated_funcs [(type (obj ), type (self ))]
76+ decorated_func = decorated_func_mapping [(type (obj ), type (self ))]
7677 except KeyError :
7778 return
7879
@@ -103,6 +104,96 @@ class MetaPerspective(type):
103104 pass
104105
105106
107+ class SupportsEventDecorator (MetaPerspective ):
108+ def __init__ (
109+ cls , name : str , bases : tuple [type , ...], namespace : dict [str , Any ]
110+ ) -> None :
111+ super ().__init__ (name , bases , namespace )
112+
113+ topic_prefix = construct_topic (cls ) + "."
114+
115+ # Find the event decorators on this class.
116+ func_decorators = [
117+ decorator
118+ for decorator in all_func_decorators
119+ if construct_topic (decorator .decorated_func ).startswith (topic_prefix )
120+ ]
121+
122+ for decorator in func_decorators :
123+ given = decorator .given_event_cls
124+
125+ # Keep things simple by only supporting given classes (not names).
126+ assert given is not None , "Event class not given"
127+ # TODO: Maybe support event name strings, maybe not....
128+
129+ # Make sure given event class is a Mutates subclass.
130+ assert issubclass (given , Mutates )
131+
132+ # Decorator should not have an original event class that has already
133+ # been subclassed, unless it's mentioned twice in the same projection,
134+ # which should be caught as an error. Because it will have either
135+ # already been subclassed and replaced, or never been seen before.
136+ assert given not in given_event_class_mapping
137+
138+ # Maybe redefine given event class as subclass of 'DecoratedFuncCaller'.
139+ if not issubclass (given , DecoratedFuncCaller ):
140+ # Define a subclass of the given event class.
141+ func_caller = cls ._insert_decorator_func_caller (given , topic_prefix )
142+
143+ # Remember which subclass for given event class.
144+ given_event_class_mapping [given ] = func_caller
145+
146+ else :
147+ # Check we subclassed this class.
148+ assert given in given_event_class_mapping .values ()
149+ func_caller = given
150+
151+ # If command method, remember which event class to trigger.
152+ if not construct_topic (decorator .decorated_func ).endswith ("._" ):
153+ decorated_func_callers [decorator ] = func_caller
154+
155+ # Remember which decorated func to call.
156+ decorated_func_mapping [(cls , func_caller )] = decorator .decorated_func
157+
158+ def _insert_decorator_func_caller (
159+ cls , given_event_class : type [Mutates ], topic_prefix : str
160+ ) -> type [DecoratedFuncCaller ]:
161+ # Identify the context in which the given class is defined.
162+ context : ModuleType | type
163+ if "." not in given_event_class .__qualname__ :
164+ # Looks like a non-nested class.
165+ context = sys .modules [given_event_class .__module__ ]
166+ elif construct_topic (given_event_class ).startswith (topic_prefix ):
167+ # Nested in this class.
168+ context = cls
169+ else : # pragma: no cover
170+ # Nested in another class...
171+ # TODO: Write a test that does this....
172+ msg = f"Decorating { cls } with { given_event_class } is not supported"
173+ raise ProgrammingError (msg )
174+
175+ # Check the context actually has the given event class.
176+ assert getattr (context , given_event_class .__name__ ) is given_event_class
177+
178+ # Define subclass.
179+ func_caller = cast (
180+ type [DecoratedFuncCaller ],
181+ type (
182+ given_event_class .__name__ ,
183+ (DecoratedFuncCaller , given_event_class ),
184+ {
185+ "__module__" : cls .__module__ ,
186+ "__qualname__" : given_event_class .__qualname__ ,
187+ },
188+ ),
189+ )
190+
191+ # Replace the given event class in the context.
192+ setattr (context , given_event_class .__name__ , func_caller )
193+
194+ return func_caller
195+
196+
106197class Perspective (metaclass = MetaPerspective ):
107198 last_known_position : int | None
108199 new_decisions : tuple [Mutates , ...]
@@ -128,115 +219,21 @@ def cb(self) -> list[Selector]:
128219TPerspective = TypeVar ("TPerspective" , bound = Perspective )
129220
130221
131- given_event_subclasses : dict [type [Mutates ], type [DecoratedFuncCaller ]] = {}
132- cross_cutting_decorated_funcs : dict [
133- tuple [MetaPerspective , type [Mutates ]], CallableType
134- ] = {}
222+ given_event_class_mapping : dict [type [Mutates ], type [DecoratedFuncCaller ]] = {}
223+ decorated_func_mapping : dict [tuple [MetaPerspective , type [Mutates ]], CallableType ] = {}
135224
136225
137- class MetaEnduringObject (MetaPerspective ):
226+ class MetaEnduringObject (SupportsEventDecorator ):
138227 def __init__ (
139228 cls , name : str , bases : tuple [type , ...], namespace : dict [str , Any ]
140229 ) -> None :
141230 super ().__init__ (name , bases , namespace )
142- # Find and remember the "initialised " class.
231+ # Find and remember the "initialises " class.
143232 for item in cls .__dict__ .values ():
144233 if isinstance (item , type ) and issubclass (item , Initialises ):
145234 _enduring_object_init_classes [cls ] = item
146235 break
147236
148- # Find the event decorators.
149- topic_prefix = construct_topic (cls ) + "."
150-
151- for decorator in all_func_decorators :
152- func_topic = construct_topic (decorator .decorated_func )
153- if not func_topic .startswith (topic_prefix ):
154- # Only deal with decorators on this slice.
155- continue
156-
157- given_event_class = decorator .given_event_cls
158- # Keep things simple by only supporting given classes (not names).
159- # TODO: Maybe support event name strings, maybe not....
160- assert given_event_class is not None , "Event class not given"
161- # Make sure event decorator has a Mutates class.
162- assert issubclass (given_event_class , Mutates )
163-
164- # Assume this is a cross-cutting event, and we need to register
165- # multiple handler methods for the same class. Expect its mutate
166- # method will be called once for each enduring object tagged in
167- # its instances. The decorator event can then select which
168- # method body to call, according to the type of the 'obj' argument
169- # of its apply() method. This means we do need to subclass the given
170- # event class, but only once.
171- if given_event_class in given_event_subclasses : # pragma: no cover
172- # TODO: We never get here...
173- # Decorator is seeing an original event class.
174- assert not issubclass (given_event_class , DecoratedFuncCaller )
175- event_subclass = given_event_subclasses [given_event_class ]
176- elif issubclass (given_event_class , DecoratedFuncCaller ):
177- # Decorator is already seeing a subclass of an original class.
178- assert given_event_class in given_event_subclasses .values ()
179- event_subclass = given_event_class
180- else :
181- # Subclass the original event class once only.
182- event_class_topic = construct_topic (given_event_class )
183-
184- event_class_globalns = getattr (
185- sys .modules .get (given_event_class .__module__ , None ),
186- "__dict__" ,
187- {},
188- )
189-
190- if event_class_topic .startswith (topic_prefix ):
191- # Looks like a nested event on this class.
192- assert given_event_class .__name__ in cls .__dict__
193- assert cls .__dict__ [given_event_class .__name__ ] is given_event_class
194- elif "." not in given_event_class .__qualname__ :
195- # Looks like a module-level event class.
196- # Get the global namespace for the event class.
197- # Check the given event class is in the globalns.
198- assert given_event_class .__name__ in event_class_globalns
199- assert (
200- event_class_globalns [given_event_class .__name__ ]
201- is given_event_class
202- )
203- else : # pragma: no cover
204- # TODO: Write a test that does this....
205- msg = f"Decorating { cls } with { given_event_class } is not supported"
206- raise ProgrammingError (msg )
207- # Define a subclass of the given event class.
208- event_subclass_dict = {
209- "__module__" : cls .__module__ ,
210- "__qualname__" : given_event_class .__qualname__ ,
211- }
212- subclass_name = given_event_class .__name__
213- event_subclass = cast (
214- type [DecoratedFuncCaller ],
215- type (
216- subclass_name ,
217- (DecoratedFuncCaller , given_event_class ),
218- event_subclass_dict ,
219- ),
220- )
221-
222- # Replace the given event class in its namespace.
223- if event_class_topic .startswith (topic_prefix ):
224- setattr (cls , subclass_name , event_subclass )
225- else :
226- event_class_globalns [subclass_name ] = event_subclass
227-
228- # Remember which subclass for given event class.
229- given_event_subclasses [given_event_class ] = event_subclass
230-
231- # Register decorated func for (event class, enduring object class) tuple.
232- cross_cutting_decorated_funcs [(cls , event_subclass )] = (
233- decorator .decorated_func
234- )
235-
236- # Maybe remember which event class to trigger when method is called.
237- if not func_topic .endswith ("._" ):
238- decorated_func_callers [decorator ] = event_subclass
239-
240237 def __call__ (cls : type [T ], ** kwargs : Any ) -> T :
241238 # TODO: For convenience, make this error out in the same way
242239 # as it would if the arguments didn't match the __init__
@@ -367,85 +364,60 @@ class Selector:
367364 tags : Sequence [str ] = ()
368365
369366
370- class MetaSlice (MetaPerspective ):
371- def __init__ (
372- cls , name : str , bases : tuple [type , ...], namespace : dict [str , Any ]
373- ) -> None :
374- super ().__init__ (name , bases , namespace )
375-
376- # Find the event decorators.
377- slice_topic = construct_topic (cls )
378- expected_func_prefix = slice_topic + "."
379-
380- for decorator in all_func_decorators :
381- func_topic = construct_topic (decorator .decorated_func )
382- if not func_topic .startswith (expected_func_prefix ):
383- # Only deal with decorators on this slice.
384- continue
385-
386- given_event_class = decorator .given_event_cls
387- # Keep things simple by only supporting given classes (not names).
388- # TODO: Maybe support event name strings, maybe not....
389- assert given_event_class is not None , "Event class not given"
390- # Make sure event decorator has a Mutates class.
391- assert issubclass (given_event_class , Mutates )
392-
393- # Assume this is a cross-cutting event, and we need to register
394- # multiple handler methods for the same class. Expect its mutate
395- # method will be called once for each enduring object tagged in
396- # its instances. The decorator event can then select which
397- # method body to call, according to the type of the 'obj' argument
398- # of its apply() method. This means we do need to subclass the given
399- # event class, but only once.
400- if given_event_class in given_event_subclasses : # pragma: no cover
401- # Decorator is seeing an original event class.
402- # --- apparently we never get here.
403- assert not issubclass (given_event_class , DecoratedFuncCaller )
404- event_subclass = given_event_subclasses [given_event_class ]
405- elif issubclass (given_event_class , DecoratedFuncCaller ):
406- # Decorator is already seeing a subclass of an original class.
407- assert given_event_class in given_event_subclasses .values ()
408- event_subclass = given_event_class
409- else :
410- # Subclass the cross-cutting event class once only.
411- # - keep things simple by only supporting non-nested classes
412- event_class_qual = given_event_class .__qualname__
413- assert (
414- "." not in event_class_qual
415- ), "Nested cross-cutting classes aren't supported"
416- # Get the global namespace for the event class.
417- event_class_globalns = getattr (
418- sys .modules .get (given_event_class .__module__ , None ),
419- "__dict__" ,
420- {},
421- )
422- # Check the given event class is in the globalns.
423- assert event_class_qual in event_class_globalns
424- assert event_class_globalns [event_class_qual ] is given_event_class
425- # Define a subclass of the given event class.
426- event_subclass_dict = {
427- "__module__" : cls .__module__ ,
428- "__qualname__" : event_class_qual ,
429- }
430- subclass_name = given_event_class .__name__
431- event_subclass = cast (
432- type [DecoratedFuncCaller ],
433- type (
434- subclass_name ,
435- (DecoratedFuncCaller , given_event_class ),
436- event_subclass_dict ,
437- ),
438- )
439- # Replace the given event class in its namespace.
440- event_class_globalns [event_class_qual ] = event_subclass
441-
442- # Remember which subclass for given event class.
443- given_event_subclasses [given_event_class ] = event_subclass
444-
445- # Register decorated func for event class / enduring object class.
446- cross_cutting_decorated_funcs [(cls , event_subclass )] = (
447- decorator .decorated_func
448- )
367+ class MetaSlice (SupportsEventDecorator ):
368+ pass
369+ # def __init__(
370+ # cls, name: str, bases: tuple[type, ...], namespace: dict[str, Any]
371+ # ) -> None:
372+ # super().__init__(name, bases, namespace)
373+ #
374+ # # Find the event decorators.
375+ # topic_prefix = construct_topic(cls) + "."
376+ #
377+ # my_func_decorators = cls._filter_event_decorators(topic_prefix)
378+ #
379+ # for decorator in my_func_decorators:
380+ # given_event_class = decorator.given_event_cls
381+ # # Keep things simple by only supporting given classes (not names).
382+ # # TODO: Maybe support event name strings, maybe not....
383+ # assert given_event_class is not None, "Event class not given"
384+ # # Make sure event decorator has a Mutates class.
385+ # assert issubclass(given_event_class, Mutates)
386+ #
387+ # # Decorator never has an original event class that has been subclassed.
388+ # assert given_event_class not in given_event_subclasses
389+ # if issubclass(given_event_class, DecoratedFuncCaller):
390+ # # Decorator has a subclass of an original class.
391+ # assert given_event_class in given_event_subclasses.values()
392+ # event_subclass = given_event_class
393+ # else:
394+ # event_class_qual = given_event_class.__qualname__
395+ # assert (
396+ # "." not in event_class_qual
397+ # ), "Nested cross-cutting classes aren't supported"
398+ #
399+ # # Get the global namespace for the event class.
400+ # globalns = getattr(
401+ # sys.modules.get(given_event_class.__module__, None),
402+ # "__dict__",
403+ # {},
404+ # )
405+ # # Check the given event class is in the globalns.
406+ # assert given_event_class.__name__ in globalns
407+ # assert globalns[given_event_class.__name__] is given_event_class
408+ #
409+ # # Define a subclass of the given event class.
410+ # event_subclass = cls._define_decorator_func_caller(given_event_class)
411+ # # Remember which subclass for given event class.
412+ # given_event_subclasses[given_event_class] = event_subclass
413+ #
414+ # # Replace the given event class in its namespace.
415+ # globalns[event_class_qual] = event_subclass
416+ #
417+ # # Register decorated func for event class / enduring object class.
418+ # cross_cutting_decorated_funcs[(cls, event_subclass)] = (
419+ # decorator.decorated_func
420+ # )
449421
450422
451423class Slice (Perspective , metaclass = MetaSlice ):
0 commit comments