Skip to content

Commit ef9da46

Browse files
committed
Factored out SupportsEventDecorator metaclass from MetaEnduringObject and MetaSlice.
Also reverted to "test_enrolment" from "check_enrolment" after figuring out why stack trace was missing some lines.
1 parent 0b26c35 commit ef9da46

File tree

5 files changed

+156
-184
lines changed

5 files changed

+156
-184
lines changed

eventsourcing/dcb/domain.py

Lines changed: 150 additions & 178 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
if 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+
106197
class Perspective(metaclass=MetaPerspective):
107198
last_known_position: int | None
108199
new_decisions: tuple[Mutates, ...]
@@ -128,115 +219,21 @@ def cb(self) -> list[Selector]:
128219
TPerspective = 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

451423
class Slice(Perspective, metaclass=MetaSlice):

examples/coursebooking/test_application.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def test_enrolment_with_postgres(self) -> None:
2323

2424

2525
class TestEnrolmentConsistency(TestEnrolmentWithAggregates):
26-
def check_enrolment(self) -> None:
26+
def test_enrolment(self) -> None:
2727
# Construct application object.
2828
app = self.construct_app()
2929

0 commit comments

Comments
 (0)