eventsourcing 9.4.0b2__py3-none-any.whl → 9.4.0b4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of eventsourcing might be problematic. Click here for more details.

eventsourcing/domain.py CHANGED
@@ -24,13 +24,13 @@ from typing import (
24
24
  from uuid import UUID, uuid4
25
25
  from warnings import warn
26
26
 
27
- from typing_extensions import Self
28
-
29
27
  from eventsourcing.utils import get_method_name, get_topic, resolve_topic
30
28
 
31
29
  if TYPE_CHECKING:
32
30
  from collections.abc import Iterable, Sequence
33
31
 
32
+ from typing_extensions import Self
33
+
34
34
 
35
35
  TZINFO: tzinfo = resolve_topic(os.getenv("TZINFO_TOPIC", "datetime:timezone.utc"))
36
36
  """
@@ -45,9 +45,7 @@ domain and convert to local timezones when presenting values in user interfaces.
45
45
 
46
46
 
47
47
  class EventsourcingType(type):
48
- """
49
- Base type for event sourcing domain model types (aggregates and events).
50
- """
48
+ """Base type for event sourcing domain model types (aggregates and events)."""
51
49
 
52
50
 
53
51
  _T = TypeVar("_T")
@@ -78,8 +76,7 @@ patch_dataclasses_process_class()
78
76
 
79
77
  @runtime_checkable
80
78
  class DomainEventProtocol(Protocol):
81
- """
82
- Protocol for domain event objects.
79
+ """Protocol for domain event objects.
83
80
 
84
81
  A protocol is defined to allow the event sourcing mechanisms
85
82
  to work with different kinds of domain event classes. Whilst
@@ -93,16 +90,12 @@ class DomainEventProtocol(Protocol):
93
90
 
94
91
  @property
95
92
  def originator_id(self) -> UUID:
96
- """
97
- UUID identifying an aggregate to which the event belongs.
98
- """
93
+ """UUID identifying an aggregate to which the event belongs."""
99
94
  raise NotImplementedError # pragma: no cover
100
95
 
101
96
  @property
102
97
  def originator_version(self) -> int:
103
- """
104
- Integer identifying the version of the aggregate when the event occurred.
105
- """
98
+ """Integer identifying the version of the aggregate when the event occurred."""
106
99
  raise NotImplementedError # pragma: no cover
107
100
 
108
101
 
@@ -111,8 +104,7 @@ SDomainEvent = TypeVar("SDomainEvent", bound=DomainEventProtocol)
111
104
 
112
105
 
113
106
  class MutableAggregateProtocol(Protocol):
114
- """
115
- Protocol for mutable aggregate objects.
107
+ """Protocol for mutable aggregate objects.
116
108
 
117
109
  A protocol is defined to allow the event sourcing mechanisms
118
110
  to work with different kinds of aggregate classes. Whilst
@@ -123,29 +115,22 @@ class MutableAggregateProtocol(Protocol):
123
115
 
124
116
  @property
125
117
  def id(self) -> UUID:
126
- """
127
- Mutable aggregates have a read-only ID that is a UUID.
128
- """
118
+ """Mutable aggregates have a read-only ID that is a UUID."""
129
119
  raise NotImplementedError # pragma: no cover
130
120
 
131
121
  @property
132
122
  def version(self) -> int:
133
- """
134
- Mutable aggregates have a read-write version that is an int.
135
- """
123
+ """Mutable aggregates have a read-write version that is an int."""
136
124
  raise NotImplementedError # pragma: no cover
137
125
 
138
126
  @version.setter
139
127
  def version(self, value: int) -> None:
140
- """
141
- Mutable aggregates have a read-write version that is an int.
142
- """
128
+ """Mutable aggregates have a read-write version that is an int."""
143
129
  raise NotImplementedError # pragma: no cover
144
130
 
145
131
 
146
132
  class ImmutableAggregateProtocol(Protocol):
147
- """
148
- Protocol for immutable aggregate objects.
133
+ """Protocol for immutable aggregate objects.
149
134
 
150
135
  A protocol is defined to allow the event sourcing mechanisms
151
136
  to work with different kinds of aggregate classes. Whilst
@@ -156,16 +141,12 @@ class ImmutableAggregateProtocol(Protocol):
156
141
 
157
142
  @property
158
143
  def id(self) -> UUID:
159
- """
160
- Immutable aggregates have a read-only ID that is a UUID.
161
- """
144
+ """Immutable aggregates have a read-only ID that is a UUID."""
162
145
  raise NotImplementedError # pragma: no cover
163
146
 
164
147
  @property
165
148
  def version(self) -> int:
166
- """
167
- Immutable aggregates have a read-only version that is an int.
168
- """
149
+ """Immutable aggregates have a read-only version that is an int."""
169
150
  raise NotImplementedError # pragma: no cover
170
151
 
171
152
 
@@ -183,22 +164,16 @@ TMutableOrImmutableAggregate = TypeVar(
183
164
 
184
165
  @runtime_checkable
185
166
  class CollectEventsProtocol(Protocol):
186
- """
187
- Protocol for aggregates that support collecting pending events.
188
- """
167
+ """Protocol for aggregates that support collecting pending events."""
189
168
 
190
169
  def collect_events(self) -> Sequence[DomainEventProtocol]:
191
- """
192
- Returns a sequence of events.
193
- """
170
+ """Returns a sequence of events."""
194
171
  raise NotImplementedError # pragma: no cover
195
172
 
196
173
 
197
174
  @runtime_checkable
198
175
  class CanMutateProtocol(DomainEventProtocol, Protocol[TMutableOrImmutableAggregate]):
199
- """
200
- Protocol for events that have a mutate method.
201
- """
176
+ """Protocol for events that have a mutate method."""
202
177
 
203
178
  def mutate(
204
179
  self, aggregate: TMutableOrImmutableAggregate | None
@@ -212,7 +187,8 @@ class CanMutateProtocol(DomainEventProtocol, Protocol[TMutableOrImmutableAggrega
212
187
 
213
188
  def datetime_now_with_tzinfo() -> datetime:
214
189
  """
215
- Constructs a timezone-aware :class:`datetime` object for the current date and time.
190
+ Constructs a timezone-aware :class:`datetime`
191
+ object for the current date and time.
216
192
 
217
193
  Uses :py:obj:`TZINFO` as the timezone.
218
194
  """
@@ -220,9 +196,7 @@ def datetime_now_with_tzinfo() -> datetime:
220
196
 
221
197
 
222
198
  def create_utc_datetime_now() -> datetime:
223
- """
224
- Deprected in favour of :func:`~eventsourcing.domain.datetime_now_with_tzinfo`.
225
- """
199
+ """Deprected in favour of :func:`~eventsourcing.domain.datetime_now_with_tzinfo`."""
226
200
  msg = (
227
201
  "'create_utc_datetime_now()' is deprecated, "
228
202
  "use 'datetime_now_with_tzinfo()' instead"
@@ -232,14 +206,11 @@ def create_utc_datetime_now() -> datetime:
232
206
 
233
207
 
234
208
  class CanCreateTimestamp:
235
- """
236
- Provides a create_timestamp() method to subclasses.
237
- """
209
+ """Provides a create_timestamp() method to subclasses."""
238
210
 
239
211
  @staticmethod
240
212
  def create_timestamp() -> datetime:
241
- """
242
- Constructs a timezone-aware :class:`datetime` object
213
+ """Constructs a timezone-aware :class:`datetime` object
243
214
  representing when an event occurred.
244
215
  """
245
216
  return datetime_now_with_tzinfo()
@@ -249,9 +220,7 @@ TAggregate = TypeVar("TAggregate", bound="BaseAggregate")
249
220
 
250
221
 
251
222
  class HasOriginatorIDVersion:
252
- """
253
- Declares ``originator_id`` and ``originator_version`` attributes.
254
- """
223
+ """Declares ``originator_id`` and ``originator_version`` attributes."""
255
224
 
256
225
  originator_id: UUID
257
226
  """UUID identifying an aggregate to which the event belongs."""
@@ -260,19 +229,18 @@ class HasOriginatorIDVersion:
260
229
 
261
230
 
262
231
  class CanMutateAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
263
- """
264
- Implements a :func:`~eventsourcing.domain.CanMutateAggregate.mutate`
232
+ """Implements a :func:`~eventsourcing.domain.CanMutateAggregate.mutate`
265
233
  method that evolves the state of an aggregate.
266
234
  """
267
235
 
268
- # Todo: Move this to a HasTimestamp? Why is it here??
236
+ # TODO: Move this to a HasTimestamp? Why is it here??
269
237
  timestamp: datetime
270
238
  """Timezone-aware :class:`datetime` object representing when an event occurred."""
271
239
 
272
240
  def mutate(self, aggregate: TAggregate | None) -> TAggregate | None:
273
- """
274
- Validates and adjusted the attributes of the given ``aggregate`` argument. The
275
- argument is typed as ``Optional`` but the value is expected to be not ``None``.
241
+ """Validates and adjustes the attributes of the given ``aggregate`
242
+ argument. The argument typed as ``Optional`` but the value is
243
+ expected to be not ``None``.
276
244
 
277
245
  Validates the ``aggregate`` argument by checking the event's
278
246
  :py:attr:`~eventsourcing.domain.HasOriginatorIDVersion.originator_id` equals the
@@ -313,8 +281,7 @@ class CanMutateAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
313
281
  return aggregate
314
282
 
315
283
  def apply(self, aggregate: Any) -> None:
316
- """
317
- Applies the domain event to its aggregate.
284
+ """Applies the domain event to its aggregate.
318
285
 
319
286
  This method does nothing but exist to be
320
287
  overridden as a convenient way for users
@@ -324,8 +291,7 @@ class CanMutateAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
324
291
 
325
292
 
326
293
  class CanInitAggregate(CanMutateAggregate):
327
- """
328
- Implements a :func:`~eventsourcing.domain.CanMutateAggregate.mutate`
294
+ """Implements a :func:`~eventsourcing.domain.CanMutateAggregate.mutate`
329
295
  method that constructs the initial state of an aggregate.
330
296
  """
331
297
 
@@ -333,8 +299,7 @@ class CanInitAggregate(CanMutateAggregate):
333
299
  """String describing the path to an aggregate class."""
334
300
 
335
301
  def mutate(self, aggregate: TAggregate | None) -> TAggregate | None:
336
- """
337
- Constructs an aggregate instance according to the attributes of an event.
302
+ """Constructs an aggregate instance according to the attributes of an event.
338
303
 
339
304
  The ``aggregate`` argument is typed as an optional argument, but the
340
305
  value is expected to be ``None``.
@@ -361,12 +326,12 @@ class CanInitAggregate(CanMutateAggregate):
361
326
  self.__dict__, type(agg).__init__
362
327
  )
363
328
 
364
- # Provide the aggregate id, if the aggregate subclass init method expects it.
329
+ # Provide the aggregate id, if the __init__ method expects it.
365
330
  if aggregate_class in _init_mentions_id:
366
331
  init_kwargs["id"] = self.__dict__["originator_id"]
367
332
 
368
333
  # Call the aggregate subclass class init method.
369
- agg.__init__(**init_kwargs) # type: ignore
334
+ agg.__init__(**init_kwargs) # type: ignore[misc]
370
335
 
371
336
  # Call the event apply method (alternative to using __init__())
372
337
  self.apply(agg)
@@ -376,26 +341,22 @@ class CanInitAggregate(CanMutateAggregate):
376
341
 
377
342
 
378
343
  class MetaDomainEvent(EventsourcingType):
379
- """
380
- Metaclass which ensures all domain event classes are frozen dataclasses.
381
- """
344
+ """Metaclass which ensures all domain event classes are frozen dataclasses."""
382
345
 
383
346
  def __new__(
384
347
  cls, name: str, bases: tuple[type[TDomainEvent], ...], cls_dict: dict[str, Any]
385
348
  ) -> type[TDomainEvent]:
386
349
  event_cls = cast(
387
- type[TDomainEvent], super().__new__(cls, name, bases, cls_dict)
350
+ "type[TDomainEvent]", super().__new__(cls, name, bases, cls_dict)
388
351
  )
389
352
  event_cls = dataclasses.dataclass(frozen=True)(event_cls)
390
- event_cls.__signature__ = inspect.signature(event_cls.__init__) # type: ignore
353
+ event_cls.__signature__ = inspect.signature(event_cls.__init__) # type: ignore[attr-defined]
391
354
  return event_cls
392
355
 
393
356
 
394
357
  @dataclass(frozen=True)
395
358
  class DomainEvent(CanCreateTimestamp, metaclass=MetaDomainEvent):
396
- """
397
- Frozen data class representing domain model events.
398
- """
359
+ """Frozen data class representing domain model events."""
399
360
 
400
361
  originator_id: UUID
401
362
  """UUID identifying an aggregate to which the event belongs."""
@@ -406,8 +367,7 @@ class DomainEvent(CanCreateTimestamp, metaclass=MetaDomainEvent):
406
367
 
407
368
 
408
369
  class AggregateEvent(CanMutateAggregate, DomainEvent):
409
- """
410
- Frozen data class representing aggregate events.
370
+ """Frozen data class representing aggregate events.
411
371
 
412
372
  Subclasses represent original decisions made by domain model aggregates.
413
373
  """
@@ -415,29 +375,22 @@ class AggregateEvent(CanMutateAggregate, DomainEvent):
415
375
 
416
376
  @dataclass(frozen=True)
417
377
  class AggregateCreated(CanInitAggregate, AggregateEvent):
418
- """
419
- Frozen data class representing the initial creation of an aggregate.
420
- """
378
+ """Frozen data class representing the initial creation of an aggregate."""
421
379
 
422
380
  originator_topic: str
423
381
  """String describing the path to an aggregate class."""
424
382
 
425
383
 
426
384
  class EventSourcingError(Exception):
427
- """
428
- Base exception class.
429
- """
385
+ """Base exception class."""
430
386
 
431
387
 
432
388
  class ProgrammingError(EventSourcingError):
433
- """
434
- Exception class for domain model programming errors.
435
- """
389
+ """Exception class for domain model programming errors."""
436
390
 
437
391
 
438
392
  class LogEvent(DomainEvent):
439
- """
440
- Deprecated: Inherit from DomainEvent instead.
393
+ """Deprecated: Inherit from DomainEvent instead.
441
394
 
442
395
  Base class for the events of event-sourced logs.
443
396
  """
@@ -459,16 +412,16 @@ def _spec_filter_kwargs_for_method_params(method: Callable[..., Any]) -> set[str
459
412
  if TYPE_CHECKING:
460
413
  EventSpecType = Union[str, type[CanMutateAggregate]]
461
414
 
462
- CommandMethod = Callable[..., None]
463
- DecoratedObjType = Union[CommandMethod, property]
464
- TDecoratedObjType = TypeVar("TDecoratedObjType", bound=DecoratedObjType)
415
+ CallableType = Callable[..., None]
416
+ DecoratableType = Union[CallableType, property]
417
+ TDecoratableType = TypeVar("TDecoratableType", bound=DecoratableType)
465
418
 
466
419
 
467
420
  class CommandMethodDecorator:
468
421
  def __init__(
469
422
  self,
470
423
  event_spec: EventSpecType | None,
471
- decorated_obj: DecoratedObjType,
424
+ decorated_obj: DecoratableType,
472
425
  ):
473
426
  self.is_name_inferred_from_method = False
474
427
  self.given_event_cls: type[CanMutateAggregate] | None = None
@@ -476,7 +429,7 @@ class CommandMethodDecorator:
476
429
  self.decorated_property: property | None = None
477
430
  self.is_property_setter = False
478
431
  self.property_setter_arg_name: str | None = None
479
- self.decorated_method: FunctionType | WrapperDescriptorType
432
+ self.decorated_func: CallableType
480
433
 
481
434
  # Event name has been specified.
482
435
  if isinstance(event_spec, str):
@@ -508,30 +461,34 @@ class CommandMethodDecorator:
508
461
  # Remember we are decorating a property.
509
462
  self.decorated_property = decorated_obj
510
463
 
511
- # Remember the decorated method as the "setter" of the property.
512
- self.decorated_method = cast(FunctionType, decorated_obj.fset)
513
-
514
- assert isinstance(self.decorated_method, FunctionType)
464
+ # TODO: Disallow unusual property setters in more detail.
465
+ assert isinstance(decorated_obj.fset, FunctionType)
515
466
 
516
467
  # Disallow deriving event class names from property names.
517
468
  if not self.given_event_cls and not self.event_cls_name:
518
- method_name = self.decorated_method.__name__
519
- msg = f"@event on {method_name}() setter requires event name or class"
469
+ method_name = decorated_obj.fset.__name__
470
+ msg = (
471
+ f"@event decorator on @{method_name}.setter "
472
+ f"requires event name or class"
473
+ )
520
474
  raise TypeError(msg)
521
475
 
476
+ # Remember property "setter" as the decorated function.
477
+ self.decorated_func = decorated_obj.fset
478
+
522
479
  # Remember the name of the second setter arg.
523
- setter_arg_names = list(inspect.signature(self.decorated_method).parameters)
480
+ setter_arg_names = list(inspect.signature(self.decorated_func).parameters)
524
481
  assert len(setter_arg_names) == 2
525
482
  self.property_setter_arg_name = setter_arg_names[1]
526
483
 
527
- # Process a decorated method.
484
+ # Process a decorated function.
528
485
  elif isinstance(decorated_obj, FunctionType):
529
- # Remember the decorated method as the decorated object.
530
- self.decorated_method = decorated_obj
486
+ # Remember the decorated obj as the decorated method.
487
+ self.decorated_func = decorated_obj
531
488
 
532
489
  # If necessary, derive an event class name from the method.
533
490
  if not self.given_event_cls and not self.event_cls_name:
534
- original_method_name = self.decorated_method.__name__
491
+ original_method_name = self.decorated_func.__name__
535
492
  if original_method_name != "__init__":
536
493
  self.is_name_inferred_from_method = True
537
494
  self.event_cls_name = "".join(
@@ -545,7 +502,11 @@ class CommandMethodDecorator:
545
502
 
546
503
  # Disallow using methods with variable params to define event class.
547
504
  if self.event_cls_name:
548
- _check_no_variable_params(self.decorated_method)
505
+ _raise_type_error_if_func_has_variable_params(self.decorated_func)
506
+
507
+ # Disallow using methods with positional only params to define event class.
508
+ if self.event_cls_name:
509
+ _raise_type_error_if_func_has_positional_only_params(self.decorated_func)
549
510
 
550
511
  def __call__(self, *args: Any, **kwargs: Any) -> None:
551
512
  # Initialised decorator was called directly, presumably by
@@ -564,7 +525,7 @@ class CommandMethodDecorator:
564
525
 
565
526
  @overload
566
527
  def __get__(
567
- self, instance: None, owner: MetaAggregate[Aggregate]
528
+ self, instance: None, owner: type[BaseAggregate]
568
529
  ) -> UnboundCommandMethodDecorator | property:
569
530
  """
570
531
  Descriptor protocol for getting decorated method or property on class object.
@@ -572,22 +533,24 @@ class CommandMethodDecorator:
572
533
 
573
534
  @overload
574
535
  def __get__(
575
- self, instance: Aggregate, owner: MetaAggregate[Aggregate]
536
+ self, instance: BaseAggregate, owner: type[BaseAggregate]
576
537
  ) -> BoundCommandMethodDecorator | Any:
577
538
  """
578
539
  Descriptor protocol for getting decorated method or property on instance object.
579
540
  """
580
541
 
581
542
  def __get__(
582
- self, instance: Aggregate | None, owner: MetaAggregate[Aggregate]
543
+ self, instance: BaseAggregate | None, owner: type[BaseAggregate]
583
544
  ) -> BoundCommandMethodDecorator | UnboundCommandMethodDecorator | property | Any:
584
- """
585
- Descriptor protocol for getting decorated method or property.
586
- """
545
+ """Descriptor protocol for getting decorated method or property."""
587
546
  # If we are decorating a property, then delegate to the property's __get__.
588
547
  if self.decorated_property:
589
548
  return self.decorated_property.__get__(instance, owner)
590
549
 
550
+ # If we are decorating an __init__ method, then delegate to the __init__ method.
551
+ if self.decorated_func.__name__ == "__init__":
552
+ return self.decorated_func.__get__(instance, owner)
553
+
591
554
  # Return a "bound" command method decorator if we have an instance.
592
555
  if instance:
593
556
  return BoundCommandMethodDecorator(self, instance)
@@ -595,10 +558,8 @@ class CommandMethodDecorator:
595
558
  # Return an "unbound" command method decorator if we have no instance.
596
559
  return UnboundCommandMethodDecorator(self)
597
560
 
598
- def __set__(self, instance: Aggregate, value: Any) -> None:
599
- """
600
- Descriptor protocol for assigning to decorated property.
601
- """
561
+ def __set__(self, instance: BaseAggregate, value: Any) -> None:
562
+ """Descriptor protocol for assigning to decorated property."""
602
563
  # Set decorated property indirectly by triggering an event.
603
564
  assert self.property_setter_arg_name
604
565
  b = BoundCommandMethodDecorator(self, instance)
@@ -607,35 +568,28 @@ class CommandMethodDecorator:
607
568
 
608
569
 
609
570
  @overload
610
- def event(arg: TDecoratedObjType) -> TDecoratedObjType:
611
- """
612
- Signature for calling ``@event`` decorator with decorated method.
613
- """
571
+ def event(arg: TDecoratableType) -> TDecoratableType:
572
+ """Signature for calling ``@event`` decorator with decorated method."""
614
573
 
615
574
 
616
575
  @overload
617
576
  def event(
618
577
  arg: EventSpecType,
619
- ) -> Callable[[TDecoratedObjType], TDecoratedObjType]:
620
- """
621
- Signature for calling ``@event`` decorator with event specification.
622
- """
578
+ ) -> Callable[[TDecoratableType], TDecoratableType]:
579
+ """Signature for calling ``@event`` decorator with event specification."""
623
580
 
624
581
 
625
582
  @overload
626
583
  def event(
627
584
  arg: None = None,
628
- ) -> Callable[[TDecoratedObjType], TDecoratedObjType]:
629
- """
630
- Signature for calling ``@event`` decorator without event specification.
631
- """
585
+ ) -> Callable[[TDecoratableType], TDecoratableType]:
586
+ """Signature for calling ``@event`` decorator without event specification."""
632
587
 
633
588
 
634
589
  def event(
635
- arg: EventSpecType | TDecoratedObjType | None = None,
636
- ) -> TDecoratedObjType | Callable[[TDecoratedObjType], TDecoratedObjType]:
637
- """
638
- Event-triggering decorator for aggregate command methods and property setters.
590
+ arg: EventSpecType | TDecoratableType | None = None,
591
+ ) -> TDecoratableType | Callable[[TDecoratableType], TDecoratableType]:
592
+ """Event-triggering decorator for aggregate command methods and property setters.
639
593
 
640
594
  Can be used to decorate an aggregate method or property setter so that an
641
595
  event will be triggered when the method is called or the property is set.
@@ -690,25 +644,24 @@ def event(
690
644
  decorated_obj=arg,
691
645
  )
692
646
  return cast(
693
- Callable[[TDecoratedObjType], TDecoratedObjType], command_method_decorator
647
+ "Callable[[TDecoratableType], TDecoratableType]", command_method_decorator
694
648
  )
695
649
 
696
650
  if (
697
651
  arg is None
698
652
  or isinstance(arg, str)
699
- or isinstance(arg, type)
700
- and issubclass(arg, CanMutateAggregate)
653
+ or (isinstance(arg, type) and issubclass(arg, CanMutateAggregate))
701
654
  ):
702
655
  event_spec = arg
703
656
 
704
657
  def create_command_method_decorator(
705
- decorated_obj: TDecoratedObjType,
706
- ) -> TDecoratedObjType:
658
+ decorated_obj: TDecoratableType,
659
+ ) -> TDecoratableType:
707
660
  command_method_decorator = CommandMethodDecorator(
708
661
  event_spec=event_spec,
709
662
  decorated_obj=decorated_obj,
710
663
  )
711
- return cast(TDecoratedObjType, command_method_decorator)
664
+ return cast("TDecoratableType", command_method_decorator)
712
665
 
713
666
  return create_command_method_decorator
714
667
 
@@ -723,21 +676,16 @@ triggers = event
723
676
 
724
677
 
725
678
  class UnboundCommandMethodDecorator:
726
- """
727
- Wraps a CommandMethodDecorator instance when accessed on an aggregate class.
728
- """
679
+ """Wraps a CommandMethodDecorator instance when accessed on an aggregate class."""
729
680
 
730
681
  def __init__(self, event_decorator: CommandMethodDecorator):
731
- """
732
-
733
- :param CommandMethodDecorator event_decorator:
734
- """
682
+ """:param CommandMethodDecorator event_decorator:"""
735
683
  self.event_decorator = event_decorator
736
- self.__module__ = event_decorator.decorated_method.__module__
737
- self.__name__ = event_decorator.decorated_method.__name__
738
- self.__qualname__ = event_decorator.decorated_method.__qualname__
739
- self.__annotations__ = event_decorator.decorated_method.__annotations__
740
- self.__doc__ = event_decorator.decorated_method.__doc__
684
+ self.__module__ = event_decorator.decorated_func.__module__
685
+ self.__name__ = event_decorator.decorated_func.__name__
686
+ self.__qualname__ = event_decorator.decorated_func.__qualname__
687
+ self.__annotations__ = event_decorator.decorated_func.__annotations__
688
+ self.__doc__ = event_decorator.decorated_func.__doc__
741
689
  # self.__wrapped__ = event_decorator.decorated_method
742
690
  # functools.update_wrapper(self, event_decorator.decorated_method)
743
691
 
@@ -754,28 +702,27 @@ class UnboundCommandMethodDecorator:
754
702
 
755
703
 
756
704
  class BoundCommandMethodDecorator:
757
- """
758
- Binds a CommandMethodDecorator with an aggregate instance so calls to
705
+ """Binds a CommandMethodDecorator with an aggregate instance so calls to
759
706
  decorated command methods can be intercepted and will trigger an event.
760
707
  """
761
708
 
762
- def __init__(self, event_decorator: CommandMethodDecorator, aggregate: Aggregate):
763
- """
764
-
765
- :param CommandMethodDecorator event_decorator:
709
+ def __init__(
710
+ self, event_decorator: CommandMethodDecorator, aggregate: BaseAggregate
711
+ ):
712
+ """:param CommandMethodDecorator event_decorator:
766
713
  :param Aggregate aggregate:
767
714
  """
768
715
  self.event_decorator = event_decorator
769
- self.__module__ = event_decorator.decorated_method.__module__
770
- self.__name__ = event_decorator.decorated_method.__name__
771
- self.__qualname__ = event_decorator.decorated_method.__qualname__
772
- self.__annotations__ = event_decorator.decorated_method.__annotations__
773
- self.__doc__ = event_decorator.decorated_method.__doc__
716
+ self.__module__ = event_decorator.decorated_func.__module__
717
+ self.__name__ = event_decorator.decorated_func.__name__
718
+ self.__qualname__ = event_decorator.decorated_func.__qualname__
719
+ self.__annotations__ = event_decorator.decorated_func.__annotations__
720
+ self.__doc__ = event_decorator.decorated_func.__doc__
774
721
  self.aggregate = aggregate
775
722
 
776
723
  def trigger(self, *args: Any, **kwargs: Any) -> None:
777
724
  kwargs = _coerce_args_to_kwargs(
778
- self.event_decorator.decorated_method, args, kwargs
725
+ self.event_decorator.decorated_func, args, kwargs
779
726
  )
780
727
  event_cls = decorator_event_classes[self.event_decorator]
781
728
  kwargs = _filter_kwargs_for_method_params(kwargs, event_cls)
@@ -786,32 +733,31 @@ class BoundCommandMethodDecorator:
786
733
 
787
734
 
788
735
  class DecoratorEvent(CanMutateAggregate):
789
- def apply(self, aggregate: Aggregate) -> None:
790
- """
791
- Applies event to aggregate by calling method decorated by @event.
792
- """
793
- # Call super method, just in case any base classes need it.
794
- super().apply(aggregate)
795
-
796
- # Identify the method that was decorated.
797
- decorated_method = _decorated_methods[type(self)]
736
+ def apply(self, aggregate: BaseAggregate) -> None:
737
+ """Applies event to aggregate by calling method decorated by @event."""
738
+ # Identify the function that was decorated.
739
+ decorated_func = _decorated_funcs[type(self)]
798
740
 
799
- # Select event attributes mentioned in method signature.
800
- kwargs = _filter_kwargs_for_method_params(self.__dict__, decorated_method)
741
+ # Select event attributes mentioned in function signature.
742
+ kwargs = _filter_kwargs_for_method_params(self.__dict__, decorated_func)
801
743
 
802
744
  # Call the original method with event attribute values.
803
- decorated_method(aggregate, **kwargs)
745
+ decorated_method = decorated_func.__get__(aggregate, type(aggregate))
746
+ decorated_method(**kwargs)
747
+
748
+ # Call super method, just in case any base classes need it.
749
+ super().apply(aggregate)
804
750
 
805
751
 
806
752
  _given_event_classes: set[type] = set()
807
- _decorated_methods: dict[type, CommandMethod] = {}
753
+ _decorated_funcs: dict[type, CallableType] = {}
808
754
  _created_event_classes: dict[type, list[type[CanInitAggregate]]] = {}
809
755
 
810
756
 
811
757
  decorator_event_classes: dict[CommandMethodDecorator, type[DecoratorEvent]] = {}
812
758
 
813
759
 
814
- def _check_no_variable_params(method: FunctionType) -> None:
760
+ def _raise_type_error_if_func_has_variable_params(method: CallableType) -> None:
815
761
  for param in inspect.signature(method).parameters.values():
816
762
  if param.kind is param.VAR_POSITIONAL:
817
763
  msg = f"*{param.name} not supported by decorator on {method.__name__}()"
@@ -826,14 +772,32 @@ def _check_no_variable_params(method: FunctionType) -> None:
826
772
  raise TypeError(msg)
827
773
 
828
774
 
775
+ def _raise_type_error_if_func_has_positional_only_params(method: CallableType) -> None:
776
+ # TODO: Support POSITIONAL_ONLY?
777
+ positional_only_params = []
778
+ for param in inspect.signature(method).parameters.values():
779
+ if param.name == "self":
780
+ continue
781
+ if param.kind is param.POSITIONAL_ONLY:
782
+ positional_only_params.append(param.name)
783
+
784
+ if positional_only_params:
785
+ msg = (
786
+ f"positional only args arg not supported by @event decorator on "
787
+ f"{method.__name__}(): {', '.join(positional_only_params)}"
788
+ )
789
+ raise TypeError(msg)
790
+
791
+
829
792
  def _coerce_args_to_kwargs(
830
- method: FunctionType | WrapperDescriptorType,
793
+ method: CallableType,
831
794
  args: Iterable[Any],
832
795
  kwargs: dict[str, Any],
833
796
  *,
834
797
  expects_id: bool = False,
835
798
  ) -> dict[str, Any]:
836
- assert isinstance(method, (FunctionType, WrapperDescriptorType))
799
+ # __init__ methods are WrapperDescriptorType, other method are FunctionType.
800
+ assert isinstance(method, (FunctionType, WrapperDescriptorType)), method
837
801
 
838
802
  args = tuple(args)
839
803
  enumerated_args_names, keyword_defaults_items = _spec_coerce_args_to_kwargs(
@@ -844,16 +808,14 @@ def _coerce_args_to_kwargs(
844
808
  )
845
809
 
846
810
  copy_kwargs = dict(kwargs)
847
- for i, name in enumerated_args_names:
848
- copy_kwargs[name] = args[i]
849
- for name, value in keyword_defaults_items:
850
- copy_kwargs[name] = value
811
+ copy_kwargs.update({name: args[i] for i, name in enumerated_args_names})
812
+ copy_kwargs.update(keyword_defaults_items)
851
813
  return copy_kwargs
852
814
 
853
815
 
854
816
  @cache
855
817
  def _spec_coerce_args_to_kwargs(
856
- method: FunctionType | WrapperDescriptorType,
818
+ method: CallableType,
857
819
  len_args: int,
858
820
  kwargs_keys: tuple[str],
859
821
  *,
@@ -907,16 +869,14 @@ def _spec_coerce_args_to_kwargs(
907
869
  f"argument{'' if num_missing == 1 else 's'}: "
908
870
  )
909
871
  _raise_missing_names_type_error(missing_names, msg)
910
- counter = 0
911
872
  args_names = []
912
- for name in positional_names:
873
+ for counter, name in enumerate(positional_names):
913
874
  if counter + 1 > len_args:
914
875
  break
915
876
  if name in kwargs_keys:
916
877
  msg = f"{method_name}() got multiple values for argument '{name}'"
917
878
  raise TypeError(msg)
918
879
  args_names.append(name)
919
- counter += 1
920
880
  missing_keyword_only_arguments = [
921
881
  name for name in required_keyword_only if name not in kwargs_keys
922
882
  ]
@@ -952,15 +912,13 @@ _create_id_param_names: dict[type[BaseAggregate], list[str]] = defaultdict(list)
952
912
 
953
913
 
954
914
  class MetaAggregate(EventsourcingType, Generic[TAggregate], type):
955
- """
956
- Metaclass for aggregate classes.
957
- """
915
+ """Metaclass for aggregate classes."""
958
916
 
959
917
  def _define_event_class(
960
918
  cls,
961
919
  name: str,
962
920
  bases: tuple[type[CanMutateAggregate], ...],
963
- apply_method: CommandMethod | None,
921
+ apply_method: CallableType | None,
964
922
  ) -> type[CanMutateAggregate]:
965
923
  # Define annotations for the event class (specs the init method).
966
924
  annotations = {}
@@ -985,12 +943,20 @@ class MetaAggregate(EventsourcingType, Generic[TAggregate], type):
985
943
  }
986
944
 
987
945
  # Create the event class object.
988
- return cast(type[CanMutateAggregate], type(name, bases, event_cls_dict))
946
+ _new_class = type(name, bases, event_cls_dict)
947
+ return cast("type[CanMutateAggregate]", _new_class)
989
948
 
990
949
  def __call__(
991
950
  cls: MetaAggregate[TAggregate], *args: Any, **kwargs: Any
992
951
  ) -> TAggregate:
952
+ if cls is BaseAggregate:
953
+ msg = "BaseAggregate class cannot be instantiated directly"
954
+ raise TypeError(msg)
993
955
  created_event_classes = _created_event_classes[cls]
956
+ # Here, unlike when calling _create(), we don't have a given event class,
957
+ # so we need to check that there is one "created" event class to use here.
958
+ # We don't check this in __init_subclass__ to allow for alternatives that
959
+ # can be selected by developers by calling _create(event_class=...).
994
960
  if len(created_event_classes) > 1:
995
961
  msg = (
996
962
  f"{cls.__qualname__} can't decide which of many "
@@ -1001,9 +967,8 @@ class MetaAggregate(EventsourcingType, Generic[TAggregate], type):
1001
967
  )
1002
968
  raise TypeError(msg)
1003
969
 
1004
- cls_init: FunctionType | WrapperDescriptorType = cls.__init__ # type: ignore
1005
970
  kwargs = _coerce_args_to_kwargs(
1006
- cls_init,
971
+ cls.__init__, # type: ignore[misc]
1007
972
  args,
1008
973
  kwargs,
1009
974
  expects_id=cls in _annotations_mention_id,
@@ -1018,23 +983,18 @@ class MetaAggregate(EventsourcingType, Generic[TAggregate], type):
1018
983
  event_class: type[CanInitAggregate],
1019
984
  **kwargs: Any,
1020
985
  ) -> TAggregate:
986
+ # Just define method signature for the __call__() method.
1021
987
  raise NotImplementedError # pragma: no cover
1022
988
 
1023
- _created_event_class: type[CanInitAggregate]
1024
-
1025
989
 
1026
990
  class BaseAggregate(metaclass=MetaAggregate):
1027
- """
1028
- Base class for aggregates.
1029
- """
991
+ """Base class for aggregates."""
1030
992
 
1031
993
  INITIAL_VERSION = 1
1032
994
 
1033
995
  @staticmethod
1034
996
  def create_id(*_: Any, **__: Any) -> UUID:
1035
- """
1036
- Returns a new aggregate ID.
1037
- """
997
+ """Returns a new aggregate ID."""
1038
998
  return uuid4()
1039
999
 
1040
1000
  @classmethod
@@ -1045,9 +1005,7 @@ class BaseAggregate(metaclass=MetaAggregate):
1045
1005
  id: UUID | None = None, # noqa: A002
1046
1006
  **kwargs: Any,
1047
1007
  ) -> Self:
1048
- """
1049
- Constructs a new aggregate object instance.
1050
- """
1008
+ """Constructs a new aggregate object instance."""
1051
1009
  # Construct the domain event with an ID and a
1052
1010
  # version, and a topic for the aggregate class.
1053
1011
  create_id_kwargs = {
@@ -1083,19 +1041,18 @@ class BaseAggregate(metaclass=MetaAggregate):
1083
1041
  msg = f"Unable to construct '{event_class.__qualname__}' event: {e}"
1084
1042
  raise TypeError(msg) from e
1085
1043
  # Construct the aggregate object.
1086
- agg = cast(Self, created_event.mutate(None))
1044
+ agg = cast("Self", created_event.mutate(None))
1087
1045
 
1088
1046
  assert agg is not None
1089
1047
  # Append the domain event to pending list.
1090
- agg._pending_events.append(created_event)
1048
+ agg.pending_events.append(created_event)
1091
1049
  # Return the aggregate.
1092
1050
  return agg
1093
1051
 
1094
1052
  def __base_init__(
1095
1053
  self, originator_id: UUID, originator_version: int, timestamp: datetime
1096
1054
  ) -> None:
1097
- """
1098
- Initialises an aggregate object with an :data:`id`, a :data:`version`
1055
+ """Initialises an aggregate object with an :data:`id`, a :data:`version`
1099
1056
  number, and a :data:`timestamp`.
1100
1057
  """
1101
1058
  self._id = originator_id
@@ -1106,16 +1063,12 @@ class BaseAggregate(metaclass=MetaAggregate):
1106
1063
 
1107
1064
  @property
1108
1065
  def id(self) -> UUID:
1109
- """
1110
- The ID of the aggregate.
1111
- """
1066
+ """The ID of the aggregate."""
1112
1067
  return self._id
1113
1068
 
1114
1069
  @property
1115
1070
  def version(self) -> int:
1116
- """
1117
- The version number of the aggregate.
1118
- """
1071
+ """The version number of the aggregate."""
1119
1072
  return self._version
1120
1073
 
1121
1074
  @version.setter
@@ -1124,16 +1077,12 @@ class BaseAggregate(metaclass=MetaAggregate):
1124
1077
 
1125
1078
  @property
1126
1079
  def created_on(self) -> datetime:
1127
- """
1128
- The date and time when the aggregate was created.
1129
- """
1080
+ """The date and time when the aggregate was created."""
1130
1081
  return self._created_on
1131
1082
 
1132
1083
  @property
1133
1084
  def modified_on(self) -> datetime:
1134
- """
1135
- The date and time when the aggregate was last modified.
1136
- """
1085
+ """The date and time when the aggregate was last modified."""
1137
1086
  return self._modified_on
1138
1087
 
1139
1088
  @modified_on.setter
@@ -1142,9 +1091,7 @@ class BaseAggregate(metaclass=MetaAggregate):
1142
1091
 
1143
1092
  @property
1144
1093
  def pending_events(self) -> list[CanMutateAggregate]:
1145
- """
1146
- A list of pending events.
1147
- """
1094
+ """A list of pending events."""
1148
1095
  return self._pending_events
1149
1096
 
1150
1097
  def trigger_event(
@@ -1152,8 +1099,7 @@ class BaseAggregate(metaclass=MetaAggregate):
1152
1099
  event_class: type[CanMutateAggregate],
1153
1100
  **kwargs: Any,
1154
1101
  ) -> None:
1155
- """
1156
- Triggers domain event of given type, by creating
1102
+ """Triggers domain event of given type, by creating
1157
1103
  an event object and using it to mutate the aggregate.
1158
1104
  """
1159
1105
  # Construct the domain event as the
@@ -1182,8 +1128,7 @@ class BaseAggregate(metaclass=MetaAggregate):
1182
1128
  self._pending_events.append(new_event)
1183
1129
 
1184
1130
  def collect_events(self) -> Sequence[CanMutateAggregate]:
1185
- """
1186
- Collects and returns a list of pending aggregate
1131
+ """Collects and returns a list of pending aggregate
1187
1132
  :class:`AggregateEvent` objects.
1188
1133
  """
1189
1134
  collected = []
@@ -1203,13 +1148,22 @@ class BaseAggregate(metaclass=MetaAggregate):
1203
1148
  return f"{type(self).__name__}({', '.join(attrs)})"
1204
1149
 
1205
1150
  def __init_subclass__(
1206
- cls: type[BaseAggregate], *, created_event_name: str | None = None
1151
+ cls: type[BaseAggregate], *, created_event_name: str = ""
1207
1152
  ) -> None:
1208
1153
  """
1209
1154
  Initialises aggregate subclass by defining __init__ method and event classes.
1210
1155
  """
1211
1156
  super().__init_subclass__()
1212
1157
 
1158
+ # Ensure we aren't defining another instance of the same class,
1159
+ # because annotations can get confused when using singledispatchmethod
1160
+ # during class definition e.g. on an aggregate projector function.
1161
+ _module = importlib.import_module(cls.__module__)
1162
+ if cls.__name__ in _module.__dict__:
1163
+ msg = f"Name '{cls.__name__}' already defined in '{cls.__module__}' module"
1164
+ raise ProgrammingError(msg)
1165
+
1166
+ # Get the class annotations.
1213
1167
  class_annotations = cls.__dict__.get("__annotations__", {})
1214
1168
  try:
1215
1169
  class_annotations.pop("id")
@@ -1217,6 +1171,11 @@ class BaseAggregate(metaclass=MetaAggregate):
1217
1171
  except KeyError:
1218
1172
  pass
1219
1173
 
1174
+ if "id" in cls.__dict__:
1175
+ msg = f"Setting attribute 'id' on class '{cls.__name__}' is not allowed"
1176
+ raise ProgrammingError(msg)
1177
+
1178
+ # Process the class as a dataclass, if there are annotations.
1220
1179
  if (
1221
1180
  class_annotations
1222
1181
  or cls in _annotations_mention_id
@@ -1224,6 +1183,29 @@ class BaseAggregate(metaclass=MetaAggregate):
1224
1183
  ):
1225
1184
  dataclasses.dataclass(eq=False, repr=False)(cls)
1226
1185
 
1186
+ # Remember if __init__ mentions ID.
1187
+ for param_name in inspect.signature(cls.__init__).parameters:
1188
+ if param_name == "id":
1189
+ _init_mentions_id.add(cls)
1190
+ break
1191
+
1192
+ # Analyse __init__ attribute, to get __init__ method and @event decorator.
1193
+ init_attr: FunctionType | CommandMethodDecorator | None = cls.__dict__.get(
1194
+ "__init__"
1195
+ )
1196
+ if init_attr is None:
1197
+ # No method, no decorator.
1198
+ init_method: CallableType | None = None
1199
+ init_decorator: CommandMethodDecorator | None = None
1200
+ elif isinstance(init_attr, CommandMethodDecorator):
1201
+ # Method decorated with @event.
1202
+ init_method = init_attr.decorated_func
1203
+ init_decorator = init_attr
1204
+ else:
1205
+ # Undecorated __init__ method.
1206
+ init_decorator = None
1207
+ init_method = init_attr
1208
+
1227
1209
  # Identify or define a base event class for this aggregate.
1228
1210
  base_event_name = "Event"
1229
1211
  base_event_cls: type[CanMutateAggregate]
@@ -1237,140 +1219,150 @@ class BaseAggregate(metaclass=MetaAggregate):
1237
1219
  )
1238
1220
  setattr(cls, base_event_name, base_event_cls)
1239
1221
 
1240
- # Make sure all events defined on aggregate subclass the base event class.
1222
+ # Ensure all events defined on this class are subclasses of base event class.
1241
1223
  created_event_classes: dict[str, type[CanInitAggregate]] = {}
1242
1224
  for name, value in tuple(cls.__dict__.items()):
1243
1225
  if name == base_event_name:
1244
1226
  # Don't subclass the base event class again.
1245
1227
  continue
1246
1228
  if name.lower() == name:
1247
- # Don't subclass lowercase named attributes that have classes.
1229
+ # Don't subclass lowercase named attributes.
1248
1230
  continue
1249
- if (
1250
- isinstance(value, type)
1251
- and issubclass(value, AggregateEvent)
1252
- and not issubclass(value, base_event_cls)
1253
- ):
1254
- sub_class = cls._define_event_class(name, (value, base_event_cls), None)
1255
- setattr(cls, name, sub_class)
1256
- for name, value in tuple(cls.__dict__.items()):
1257
- if isinstance(value, type) and issubclass(value, CanInitAggregate):
1258
- created_event_classes[name] = value
1231
+ if isinstance(value, type) and issubclass(value, CanMutateAggregate):
1232
+ if not issubclass(value, base_event_cls):
1233
+ event_class = cls._define_event_class(
1234
+ name, (value, base_event_cls), None
1235
+ )
1236
+ setattr(cls, name, event_class)
1237
+ else:
1238
+ event_class = value
1239
+
1240
+ # Remember all "created" event classes defined on this class.
1241
+ if issubclass(event_class, CanInitAggregate):
1242
+ created_event_classes[name] = event_class
1259
1243
 
1260
1244
  # Identify or define the aggregate's "created" event class.
1261
1245
  created_event_class: type[CanInitAggregate] | None = None
1262
1246
 
1263
- # Is the init method decorated with a CommandMethodDecorator?
1264
- if isinstance(cls.__dict__.get("__init__"), CommandMethodDecorator):
1265
- init_decorator: CommandMethodDecorator = cls.__dict__["__init__"]
1247
+ # Analyse __init__ method decorator.
1248
+ if init_decorator:
1266
1249
 
1267
- # Set the original method on the class (un-decorate __init__).
1268
- cls.__init__ = init_decorator.decorated_method # type: ignore
1250
+ # Does the decorator specify an event class?
1251
+ if init_decorator.given_event_cls:
1269
1252
 
1270
- # Disallow using both 'created_event_name' and decorator on __init__.
1271
- if created_event_name:
1272
- msg = "Can't use both 'created_event_name' and decorator on __init__"
1273
- raise TypeError(msg)
1253
+ # Disallow conflicts between 'created_event_name' and given class.
1254
+ if (
1255
+ created_event_name
1256
+ and created_event_name != init_decorator.given_event_cls.__name__
1257
+ ):
1258
+ msg = (
1259
+ "Given 'created_event_name' conflicts "
1260
+ "with decorator on __init__"
1261
+ )
1262
+ raise TypeError(msg)
1274
1263
 
1275
- # Does the decorator specify a "created" event class?
1276
- if init_decorator.given_event_cls:
1277
- created_event_class = cast(
1278
- type[CanInitAggregate], init_decorator.given_event_cls
1279
- )
1264
+ # Check given event class can init aggregate.
1265
+ if not issubclass(init_decorator.given_event_cls, CanInitAggregate):
1266
+ msg = (
1267
+ f"class '{init_decorator.given_event_cls.__name__}' "
1268
+ f'not a "created" event class'
1269
+ )
1270
+ raise TypeError(msg)
1271
+
1272
+ # Have we already subclassed the given event class?
1273
+ for sub_class in created_event_classes.values():
1274
+ if issubclass(sub_class, init_decorator.given_event_cls):
1275
+ created_event_class = sub_class
1276
+ break
1277
+ else:
1278
+ created_event_class = init_decorator.given_event_cls
1280
1279
 
1281
- # Does the decorator specify a "created" event name?
1280
+ # Does the decorator specify an event name?
1282
1281
  elif init_decorator.event_cls_name:
1282
+ # Disallow conflicts between 'created_event_name' and given name.
1283
+ if (
1284
+ created_event_name
1285
+ and created_event_name != init_decorator.event_cls_name
1286
+ ):
1287
+ msg = (
1288
+ "Given 'created_event_name' conflicts "
1289
+ "with decorator on __init__"
1290
+ )
1291
+ raise TypeError(msg)
1292
+
1283
1293
  created_event_name = init_decorator.event_cls_name
1284
1294
 
1285
- # Disallow using decorator on __init__ without event spec.
1295
+ # Disallow using decorator on __init__ without event name or class.
1286
1296
  else:
1287
- msg = "Decorator on __init__ has neither event name nor class"
1297
+ msg = "@event decorator on __init__ has neither event name nor class"
1288
1298
  raise TypeError(msg)
1289
1299
 
1290
- # TODO: Write a test to cover this when "Created" class is explicitly defined.
1291
- # Check if init mentions ID.
1292
- for param_name in inspect.signature(cls.__init__).parameters:
1293
- if param_name == "id":
1294
- _init_mentions_id.add(cls)
1295
- break
1300
+ # Do we need to define a created event class?
1301
+ if not created_event_class:
1302
+ # If we have a "created" event class that matches the name, then use it.
1303
+ if created_event_name in created_event_classes:
1304
+ created_event_class = created_event_classes[created_event_name]
1305
+ # Otherwise, if we have no name and only one class defined, then use it.
1306
+ elif not created_event_name and len(created_event_classes) == 1:
1307
+ created_event_class = next(iter(created_event_classes.values()))
1296
1308
 
1297
- if created_event_class:
1298
- # Check specified "created" event class can init aggregate.
1299
- if not issubclass(created_event_class, CanInitAggregate):
1300
- msg = (
1301
- f"{created_event_class} not subclass of {CanInitAggregate.__name__}"
1302
- )
1303
- raise TypeError(msg)
1304
-
1305
- for sub_class in created_event_classes.values():
1306
- if issubclass(sub_class, created_event_class):
1307
- # We just subclassed the created event class, so reassign it.
1308
- created_event_class = sub_class
1309
-
1310
- # Is a "created" event class already defined that matches the name?
1311
- elif created_event_name and created_event_name in created_event_classes:
1312
- created_event_class = created_event_classes[created_event_name]
1309
+ # Otherwise, if there are no "created" events, or a name is
1310
+ # specified that hasn't matched, then define a "created" event class.
1311
+ elif len(created_event_classes) == 0 or created_event_name:
1312
+ # Decide the base "created" event class.
1313
1313
 
1314
- # If there is only one class defined, then use it.
1315
- elif len(created_event_classes) == 1 and not created_event_name:
1316
- created_event_class = next(iter(created_event_classes.values()))
1317
-
1318
- # If there are no "created" event classes already defined, or a name is
1319
- # specified that hasn't matched, then define a "created" event class.
1320
- elif len(created_event_classes) == 0 or created_event_name:
1321
-
1322
- # Decide the base classes for the new "created" event class.
1323
- if created_event_name and len(created_event_classes) == 1:
1324
- base_created_event_cls = next(iter(created_event_classes.values()))
1325
- else:
1326
- # Todo: This could probably be improved.
1327
- # Look for first class in MRO that has one specified "created" class.
1328
- for base_cls in cls.__mro__:
1329
- if (
1330
- base_cls in _created_event_classes
1331
- and len(_created_event_classes[base_cls]) == 1
1332
- ):
1333
- base_created_event_cls = _created_event_classes[base_cls][0]
1334
- break
1335
- else: # pragma: no cover
1336
- # Todo: Write a test to cover this.
1337
- msg = (
1338
- "Can't find base aggregate class with "
1339
- "a specified 'created' event class"
1314
+ try:
1315
+ # Look for a base class with the same name.
1316
+ base_created_event_cls = cast(
1317
+ "type[CanInitAggregate]",
1318
+ getattr(cls, created_event_name),
1340
1319
  )
1341
- raise TypeError(msg)
1320
+ except AttributeError:
1321
+ # Look for base class with one nominated "created" event.
1322
+ for base_cls in cls.__mro__:
1323
+ if (
1324
+ base_cls in _created_event_classes
1325
+ and len(_created_event_classes[base_cls]) == 1
1326
+ ):
1327
+ base_created_event_cls = _created_event_classes[base_cls][0]
1328
+ break
1329
+ else:
1330
+ msg = (
1331
+ "Can't identify suitable base class for "
1332
+ f"\"created\" event class on class '{cls.__name__}'"
1333
+ )
1334
+ raise TypeError(msg) from None
1342
1335
 
1343
- if not created_event_name:
1344
- created_event_name = base_created_event_cls.__name__
1336
+ if not created_event_name:
1337
+ created_event_name = base_created_event_cls.__name__
1345
1338
 
1346
- # Disallow init method from having variable params if
1347
- # we are using it to define a "created" event class.
1348
- try:
1349
- init_method = cls.__dict__["__init__"]
1350
- except KeyError:
1351
- init_method = None
1352
- else:
1353
- try:
1354
- _check_no_variable_params(init_method)
1355
- except TypeError:
1356
- raise
1339
+ # Disallow init method from having variable params, because
1340
+ # we are using it to define a "created" event class.
1341
+ if init_method:
1342
+ _raise_type_error_if_func_has_variable_params(init_method)
1357
1343
 
1358
- # Define a "created" event class for this aggregate.
1359
- if issubclass(base_created_event_cls, base_event_cls):
1360
1344
  # Don't subclass from base event class twice.
1361
- bases: tuple[type[CanMutateAggregate], ...] = (base_created_event_cls,)
1362
- else:
1363
- bases = (base_created_event_cls, base_event_cls)
1364
- created_event_class = cast(
1365
- type[CanInitAggregate],
1366
- cls._define_event_class(
1367
- created_event_name,
1368
- bases,
1369
- init_method,
1370
- ),
1371
- )
1372
- # Set the event class as an attribute of the aggregate class.
1373
- setattr(cls, created_event_name, created_event_class)
1345
+ assert isinstance(base_created_event_cls, type), base_created_event_cls
1346
+ assert not issubclass(
1347
+ base_created_event_cls, base_event_cls
1348
+ ), base_created_event_cls
1349
+
1350
+ # Define "created" event class.
1351
+ assert created_event_name
1352
+ assert issubclass(base_created_event_cls, CanInitAggregate)
1353
+ created_event_class_bases = (base_created_event_cls, base_event_cls)
1354
+ created_event_class = cast(
1355
+ "type[CanInitAggregate]",
1356
+ cls._define_event_class(
1357
+ created_event_name,
1358
+ created_event_class_bases,
1359
+ init_method,
1360
+ ),
1361
+ )
1362
+ # Set the event class as an attribute of the aggregate class.
1363
+ setattr(cls, created_event_name, created_event_class)
1364
+
1365
+ assert created_event_class or len(created_event_classes) > 1
1374
1366
 
1375
1367
  if created_event_class:
1376
1368
  _created_event_classes[cls] = [created_event_class]
@@ -1378,26 +1370,30 @@ class BaseAggregate(metaclass=MetaAggregate):
1378
1370
  # Prepare to disallow ambiguity of choice between created event classes.
1379
1371
  _created_event_classes[cls] = list(created_event_classes.values())
1380
1372
 
1381
- # Prepare the subsequent event classes.
1373
+ # Find and analyse any @event decorators.
1382
1374
  for attr_name, attr_value in tuple(cls.__dict__.items()):
1383
1375
  event_decorator: CommandMethodDecorator | None = None
1384
1376
 
1385
- if isinstance(attr_value, CommandMethodDecorator):
1386
- event_decorator = attr_value
1377
+ # Ignore a decorator on the __init__ method.
1378
+ if isinstance(attr_value, CommandMethodDecorator) and (
1379
+ attr_value.decorated_func.__name__ == "__init__"
1380
+ ):
1381
+ continue
1387
1382
 
1388
- elif isinstance(attr_value, property) and isinstance(
1383
+ # Handle @property.setter decorator on top of @event decorator.
1384
+ if isinstance(attr_value, property) and isinstance(
1389
1385
  attr_value.fset, CommandMethodDecorator
1390
1386
  ):
1391
1387
  event_decorator = attr_value.fset
1392
1388
  # Inspect the setter method.
1393
- method_signature = inspect.signature(event_decorator.decorated_method)
1389
+ method_signature = inspect.signature(event_decorator.decorated_func)
1394
1390
  assert len(method_signature.parameters) == 2
1395
1391
  event_decorator.is_property_setter = True
1396
1392
  event_decorator.property_setter_arg_name = list(
1397
1393
  method_signature.parameters
1398
1394
  )[1]
1399
- if event_decorator.decorated_method.__name__ != attr_name:
1400
- attr = cls.__dict__[event_decorator.decorated_method.__name__]
1395
+ if event_decorator.decorated_func.__name__ != attr_name:
1396
+ attr = cls.__dict__[event_decorator.decorated_func.__name__]
1401
1397
  if isinstance(attr, CommandMethodDecorator):
1402
1398
  # This is the "x = property(getx, setx) form" where setx
1403
1399
  # is a decorated method.
@@ -1406,13 +1402,16 @@ class BaseAggregate(metaclass=MetaAggregate):
1406
1402
  elif event_decorator.is_name_inferred_from_method:
1407
1403
  # This is the "@property.setter \ @event" form. We don't want
1408
1404
  # event class name inferred from property (not past participle).
1409
- method_name = event_decorator.decorated_method.__name__
1405
+ method_name = event_decorator.decorated_func.__name__
1410
1406
  msg = (
1411
- f"@event under {method_name}() property setter requires "
1412
- "event class name"
1407
+ f"@event decorator under @{method_name}.setter "
1408
+ "requires event name or class"
1413
1409
  )
1414
1410
  raise TypeError(msg)
1415
1411
 
1412
+ elif isinstance(attr_value, CommandMethodDecorator):
1413
+ event_decorator = attr_value
1414
+
1416
1415
  if event_decorator is not None:
1417
1416
  if event_decorator.given_event_cls:
1418
1417
  # Check this is not a "created" event class.
@@ -1425,7 +1424,7 @@ class BaseAggregate(metaclass=MetaAggregate):
1425
1424
 
1426
1425
  # Define event class as subclass of given class.
1427
1426
  given_subclass = cast(
1428
- type[CanMutateAggregate],
1427
+ "type[CanMutateAggregate]",
1429
1428
  getattr(cls, event_decorator.given_event_cls.__name__),
1430
1429
  )
1431
1430
  event_cls = cls._define_event_class(
@@ -1448,21 +1447,21 @@ class BaseAggregate(metaclass=MetaAggregate):
1448
1447
  event_cls = cls._define_event_class(
1449
1448
  event_decorator.event_cls_name,
1450
1449
  (DecoratorEvent, base_event_cls),
1451
- event_decorator.decorated_method,
1450
+ event_decorator.decorated_func,
1452
1451
  )
1453
1452
 
1454
1453
  # Cache the decorated method for the event class to use.
1455
- _decorated_methods[event_cls] = event_decorator.decorated_method
1454
+ _decorated_funcs[event_cls] = event_decorator.decorated_func
1456
1455
 
1457
1456
  # Set the event class as an attribute of the aggregate class.
1458
1457
  setattr(cls, event_cls.__name__, event_cls)
1459
1458
 
1460
1459
  # Remember which event class to trigger.
1461
1460
  decorator_event_classes[event_decorator] = cast(
1462
- type[DecoratorEvent], event_cls
1461
+ "type[DecoratorEvent]", event_cls
1463
1462
  )
1464
1463
 
1465
- # Check any create_id method defined on this class is static or class method.
1464
+ # Check any create_id() method defined on this class is static or class method.
1466
1465
  if "create_id" in cls.__dict__ and not isinstance(
1467
1466
  cls.__dict__["create_id"], (staticmethod, classmethod)
1468
1467
  ):
@@ -1477,7 +1476,7 @@ class BaseAggregate(metaclass=MetaAggregate):
1477
1476
  if param.kind in [param.KEYWORD_ONLY, param.POSITIONAL_OR_KEYWORD]:
1478
1477
  _create_id_param_names[cls].append(name)
1479
1478
 
1480
- # Define event classes for all events on bases.
1479
+ # Define event classes for all events on all bases if not defined on this class.
1481
1480
  for aggregate_base_class in cls.__bases__:
1482
1481
  for name, value in aggregate_base_class.__dict__.items():
1483
1482
  if (
@@ -1486,10 +1485,10 @@ class BaseAggregate(metaclass=MetaAggregate):
1486
1485
  and name not in cls.__dict__
1487
1486
  and name.lower() != name
1488
1487
  ):
1489
- sub_class = cls._define_event_class(
1488
+ event_class = cls._define_event_class(
1490
1489
  name, (base_event_cls, value), None
1491
1490
  )
1492
- setattr(cls, name, sub_class)
1491
+ setattr(cls, name, event_class)
1493
1492
 
1494
1493
 
1495
1494
  class Aggregate(BaseAggregate):
@@ -1515,8 +1514,7 @@ def aggregate(
1515
1514
  *,
1516
1515
  created_event_name: str = "",
1517
1516
  ) -> type[Aggregate] | Callable[[Any], type[Aggregate]]:
1518
- """
1519
- Converts the class that was passed in to inherit from Aggregate.
1517
+ """Converts the class that was passed in to inherit from Aggregate.
1520
1518
 
1521
1519
  .. code-block:: python
1522
1520
 
@@ -1558,8 +1556,7 @@ def aggregate(
1558
1556
 
1559
1557
 
1560
1558
  class OriginatorIDError(EventSourcingError):
1561
- """
1562
- Raised when a domain event can't be applied to
1559
+ """Raised when a domain event can't be applied to
1563
1560
  an aggregate due to an ID mismatch indicating
1564
1561
  the domain event is not in the aggregate's
1565
1562
  sequence of events.
@@ -1567,38 +1564,23 @@ class OriginatorIDError(EventSourcingError):
1567
1564
 
1568
1565
 
1569
1566
  class OriginatorVersionError(EventSourcingError):
1570
- """
1571
- Raised when a domain event can't be applied to
1567
+ """Raised when a domain event can't be applied to
1572
1568
  an aggregate due to version mismatch indicating
1573
1569
  the domain event is not the next in the aggregate's
1574
1570
  sequence of events.
1575
1571
  """
1576
1572
 
1577
1573
 
1578
- class VersionError(OriginatorVersionError):
1579
- """
1580
- Old name for 'OriginatorVersionError'.
1581
-
1582
- This class exists to maintain backwards-compatibility
1583
- but will be removed in a future version Please use
1584
- 'OriginatorVersionError' instead.
1585
- """
1586
-
1587
-
1588
1574
  class SnapshotProtocol(DomainEventProtocol, Protocol):
1589
1575
  @property
1590
1576
  def state(self) -> dict[str, Any]:
1591
- """
1592
- Snapshots have a read-only 'state'.
1593
- """
1577
+ """Snapshots have a read-only 'state'."""
1594
1578
  raise NotImplementedError # pragma: no cover
1595
1579
 
1596
1580
  # TODO: Improve on this 'Any'.
1597
1581
  @classmethod
1598
1582
  def take(cls: Any, aggregate: Any) -> Any:
1599
- """
1600
- Snapshots have a 'take()' class method.
1601
- """
1583
+ """Snapshots have a 'take()' class method."""
1602
1584
 
1603
1585
 
1604
1586
  TCanSnapshotAggregate = TypeVar("TCanSnapshotAggregate", bound="CanSnapshotAggregate")
@@ -1620,12 +1602,10 @@ class CanSnapshotAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
1620
1602
 
1621
1603
  @classmethod
1622
1604
  def take(
1623
- cls: type[TCanSnapshotAggregate],
1605
+ cls,
1624
1606
  aggregate: MutableOrImmutableAggregate,
1625
- ) -> TCanSnapshotAggregate:
1626
- """
1627
- Creates a snapshot of the given :class:`Aggregate` object.
1628
- """
1607
+ ) -> Self:
1608
+ """Creates a snapshot of the given :class:`Aggregate` object."""
1629
1609
  aggregate_state = dict(aggregate.__dict__)
1630
1610
  class_version = getattr(type(aggregate), "class_version", 1)
1631
1611
  if class_version > 1:
@@ -1643,10 +1623,8 @@ class CanSnapshotAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
1643
1623
  )
1644
1624
 
1645
1625
  def mutate(self, _: None) -> Aggregate:
1646
- """
1647
- Reconstructs the snapshotted :class:`Aggregate` object.
1648
- """
1649
- cls = cast(type[Aggregate], resolve_topic(self.topic))
1626
+ """Reconstructs the snapshotted :class:`Aggregate` object."""
1627
+ cls = cast("type[Aggregate]", resolve_topic(self.topic))
1650
1628
  aggregate_state = dict(self.state)
1651
1629
  from_version = aggregate_state.pop("class_version", 1)
1652
1630
  class_version = getattr(cls, "class_version", 1)
@@ -1666,8 +1644,7 @@ class CanSnapshotAggregate(HasOriginatorIDVersion, CanCreateTimestamp):
1666
1644
 
1667
1645
  @dataclass(frozen=True)
1668
1646
  class Snapshot(CanSnapshotAggregate, DomainEvent):
1669
- """
1670
- Snapshots represent the state of an aggregate at a particular
1647
+ """Snapshots represent the state of an aggregate at a particular
1671
1648
  version.
1672
1649
 
1673
1650
  Constructor arguments: