eventsourcing 9.3.4__py3-none-any.whl → 9.4.0__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.

@@ -0,0 +1,3 @@
1
+ import warnings
2
+
3
+ warnings.resetwarnings() # VS Code unittest runner somehow adds warning filters :-/
@@ -5,38 +5,40 @@ import sys
5
5
  import traceback
6
6
  import warnings
7
7
  from concurrent.futures import ThreadPoolExecutor
8
- from datetime import datetime
9
8
  from decimal import Decimal
10
9
  from threading import Event, get_ident
11
10
  from time import sleep
12
11
  from timeit import timeit
13
- from typing import ClassVar, Dict, Type
12
+ from typing import TYPE_CHECKING, Any, ClassVar
14
13
  from unittest import TestCase
15
14
  from uuid import UUID, uuid4
16
15
 
17
16
  from eventsourcing.application import AggregateNotFoundError, Application
18
- from eventsourcing.domain import Aggregate
17
+ from eventsourcing.domain import Aggregate, datetime_now_with_tzinfo
19
18
  from eventsourcing.persistence import (
20
19
  InfrastructureFactory,
20
+ InfrastructureFactoryError,
21
21
  IntegrityError,
22
- Transcoder,
22
+ JSONTranscoder,
23
23
  Transcoding,
24
24
  )
25
25
  from eventsourcing.tests.domain import BankAccount, EmailAddress
26
- from eventsourcing.utils import get_topic
26
+ from eventsourcing.utils import EnvType, get_topic
27
27
 
28
- TIMEIT_FACTOR = int(os.environ.get("TEST_TIMEIT_FACTOR", default=10))
28
+ if TYPE_CHECKING:
29
+ from datetime import datetime
30
+
31
+ TIMEIT_FACTOR = int(os.environ.get("TEST_TIMEIT_FACTOR", default="10"))
29
32
 
30
33
 
31
34
  class ExampleApplicationTestCase(TestCase):
32
35
  timeit_number: ClassVar[int] = TIMEIT_FACTOR
33
- started_ats: ClassVar[Dict[Type[TestCase], datetime]] = {}
34
- counts: ClassVar[Dict[Type[TestCase], int]] = {}
36
+ started_ats: ClassVar[dict[type[TestCase], datetime]] = {}
37
+ counts: ClassVar[dict[type[TestCase], int]] = {}
35
38
  expected_factory_topic: str
36
39
 
37
- def test_example_application(self):
40
+ def test_example_application(self) -> None:
38
41
  app = BankAccounts(env={"IS_SNAPSHOTTING_ENABLED": "y"})
39
- max_notification_id = app.recorder.max_notification_id()
40
42
 
41
43
  self.assertEqual(get_topic(type(app.factory)), self.expected_factory_topic)
42
44
 
@@ -75,38 +77,38 @@ class ExampleApplicationTestCase(TestCase):
75
77
  )
76
78
 
77
79
  sleep(1) # Added to make eventsourcing-axon tests work, perhaps not necessary.
78
- section = app.notification_log[
79
- f"{max_notification_id + 1},{max_notification_id + 10}"
80
- ]
80
+ section = app.notification_log["1,10"]
81
81
  self.assertEqual(len(section.items), 4)
82
82
 
83
83
  # Take snapshot (specify version).
84
84
  app.take_snapshot(account_id, version=Aggregate.INITIAL_VERSION + 1)
85
85
 
86
+ assert app.snapshots is not None # for mypy
86
87
  snapshots = list(app.snapshots.get(account_id))
87
88
  self.assertEqual(len(snapshots), 1)
88
89
  self.assertEqual(snapshots[0].originator_version, Aggregate.INITIAL_VERSION + 1)
89
90
 
90
- from_snapshot = app.repository.get(
91
+ from_snapshot1: BankAccount = app.repository.get(
91
92
  account_id, version=Aggregate.INITIAL_VERSION + 2
92
93
  )
93
- self.assertIsInstance(from_snapshot, BankAccount)
94
- self.assertEqual(from_snapshot.version, Aggregate.INITIAL_VERSION + 2)
95
- self.assertEqual(from_snapshot.balance, Decimal("35.00"))
94
+ self.assertIsInstance(from_snapshot1, BankAccount)
95
+ self.assertEqual(from_snapshot1.version, Aggregate.INITIAL_VERSION + 2)
96
+ self.assertEqual(from_snapshot1.balance, Decimal("35.00"))
96
97
 
97
98
  # Take snapshot (don't specify version).
98
99
  app.take_snapshot(account_id)
100
+ assert app.snapshots is not None # for mypy
99
101
  snapshots = list(app.snapshots.get(account_id))
100
102
  self.assertEqual(len(snapshots), 2)
101
103
  self.assertEqual(snapshots[0].originator_version, Aggregate.INITIAL_VERSION + 1)
102
104
  self.assertEqual(snapshots[1].originator_version, Aggregate.INITIAL_VERSION + 3)
103
105
 
104
- from_snapshot = app.repository.get(account_id)
105
- self.assertIsInstance(from_snapshot, BankAccount)
106
- self.assertEqual(from_snapshot.version, Aggregate.INITIAL_VERSION + 3)
107
- self.assertEqual(from_snapshot.balance, Decimal("65.00"))
106
+ from_snapshot2: BankAccount = app.repository.get(account_id)
107
+ self.assertIsInstance(from_snapshot2, BankAccount)
108
+ self.assertEqual(from_snapshot2.version, Aggregate.INITIAL_VERSION + 3)
109
+ self.assertEqual(from_snapshot2.balance, Decimal("65.00"))
108
110
 
109
- def test__put_performance(self):
111
+ def test__put_performance(self) -> None:
110
112
  app = BankAccounts()
111
113
 
112
114
  # Open an account.
@@ -116,7 +118,7 @@ class ExampleApplicationTestCase(TestCase):
116
118
  )
117
119
  account = app.get_account(account_id)
118
120
 
119
- def put():
121
+ def put() -> None:
120
122
  # Credit the account.
121
123
  account.append_transaction(Decimal("10.00"))
122
124
  app.save(account)
@@ -128,14 +130,14 @@ class ExampleApplicationTestCase(TestCase):
128
130
  duration = timeit(put, number=self.timeit_number)
129
131
  self.print_time("store events", duration)
130
132
 
131
- def test__get_performance_with_snapshotting_enabled(self):
133
+ def test__get_performance_with_snapshotting_enabled(self) -> None:
132
134
  print()
133
135
  self._test_get_performance(is_snapshotting_enabled=True)
134
136
 
135
- def test__get_performance_without_snapshotting_enabled(self):
137
+ def test__get_performance_without_snapshotting_enabled(self) -> None:
136
138
  self._test_get_performance(is_snapshotting_enabled=False)
137
139
 
138
- def _test_get_performance(self, *, is_snapshotting_enabled: bool):
140
+ def _test_get_performance(self, *, is_snapshotting_enabled: bool) -> None:
139
141
  app = BankAccounts(
140
142
  env={"IS_SNAPSHOTTING_ENABLED": "y" if is_snapshotting_enabled else "n"}
141
143
  )
@@ -146,7 +148,7 @@ class ExampleApplicationTestCase(TestCase):
146
148
  email_address="alice@example.com",
147
149
  )
148
150
 
149
- def read():
151
+ def read() -> None:
150
152
  # Get the account.
151
153
  app.get_account(account_id)
152
154
 
@@ -161,10 +163,10 @@ class ExampleApplicationTestCase(TestCase):
161
163
  test_label = "get without snapshotting"
162
164
  self.print_time(test_label, duration)
163
165
 
164
- def print_time(self, test_label, duration):
166
+ def print_time(self, test_label: str, duration: float) -> None:
165
167
  cls = type(self)
166
168
  if cls not in self.started_ats:
167
- self.started_ats[cls] = datetime.now()
169
+ self.started_ats[cls] = datetime_now_with_tzinfo()
168
170
  print(f"{cls.__name__: <29} timeit number: {cls.timeit_number}")
169
171
  self.counts[cls] = 1
170
172
  else:
@@ -179,8 +181,8 @@ class ExampleApplicationTestCase(TestCase):
179
181
  )
180
182
 
181
183
  if self.counts[cls] == 3:
182
- duration = datetime.now() - cls.started_ats[cls]
183
- print(f"{cls.__name__: <29} timeit duration: {duration}")
184
+ cls_duration = datetime_now_with_tzinfo() - cls.started_ats[cls]
185
+ print(f"{cls.__name__: <29} timeit duration: {cls_duration}")
184
186
  sys.stdout.flush()
185
187
 
186
188
 
@@ -198,11 +200,11 @@ class EmailAddressAsStr(Transcoding):
198
200
  class BankAccounts(Application):
199
201
  is_snapshotting_enabled = True
200
202
 
201
- def register_transcodings(self, transcoder: Transcoder) -> None:
203
+ def register_transcodings(self, transcoder: JSONTranscoder) -> None:
202
204
  super().register_transcodings(transcoder)
203
205
  transcoder.register(EmailAddressAsStr())
204
206
 
205
- def open_account(self, full_name, email_address):
207
+ def open_account(self, full_name: str, email_address: str) -> UUID:
206
208
  account = BankAccount.open(
207
209
  full_name=full_name,
208
210
  email_address=email_address,
@@ -221,7 +223,7 @@ class BankAccounts(Application):
221
223
 
222
224
  def get_account(self, account_id: UUID) -> BankAccount:
223
225
  try:
224
- aggregate = self.repository.get(account_id)
226
+ aggregate: BankAccount = self.repository.get(account_id)
225
227
  except AggregateNotFoundError:
226
228
  raise self.AccountNotFoundError(account_id) from None
227
229
  else:
@@ -233,7 +235,7 @@ class BankAccounts(Application):
233
235
 
234
236
 
235
237
  class ApplicationTestCase(TestCase):
236
- def test_name(self):
238
+ def test_name(self) -> None:
237
239
  self.assertEqual(Application.name, "Application")
238
240
 
239
241
  class MyApplication1(Application):
@@ -246,7 +248,7 @@ class ApplicationTestCase(TestCase):
246
248
 
247
249
  self.assertEqual(MyApplication2.name, "MyBoundedContext")
248
250
 
249
- def test_resolve_persistence_topics(self):
251
+ def test_resolve_persistence_topics(self) -> None:
250
252
  # None specified.
251
253
  app = Application()
252
254
  self.assertIsInstance(app.factory, InfrastructureFactory)
@@ -264,7 +266,7 @@ class ApplicationTestCase(TestCase):
264
266
  self.assertIsInstance(app.factory, InfrastructureFactory)
265
267
 
266
268
  # Check exceptions.
267
- with self.assertRaises(AssertionError) as cm:
269
+ with self.assertRaises(InfrastructureFactoryError) as cm:
268
270
  Application(env={"PERSISTENCE_MODULE": "eventsourcing.application"})
269
271
  self.assertEqual(
270
272
  cm.exception.args[0],
@@ -272,17 +274,18 @@ class ApplicationTestCase(TestCase):
272
274
  "'eventsourcing.application', expected 1.",
273
275
  )
274
276
 
275
- with self.assertRaises(AssertionError) as cm:
277
+ with self.assertRaises(InfrastructureFactoryError) as cm:
276
278
  Application(
277
279
  env={"PERSISTENCE_MODULE": "eventsourcing.application:Application"}
278
280
  )
279
281
  self.assertEqual(
282
+ "Topic 'eventsourcing.application:Application' didn't "
283
+ "resolve to a persistence module or infrastructure factory class: "
284
+ "<class 'eventsourcing.application.Application'>",
280
285
  cm.exception.args[0],
281
- "Not an infrastructure factory class or module: "
282
- "eventsourcing.application:Application",
283
286
  )
284
287
 
285
- def test_save_returns_recording_event(self):
288
+ def test_save_returns_recording_event(self) -> None:
286
289
  app = Application()
287
290
 
288
291
  recordings = app.save()
@@ -291,22 +294,22 @@ class ApplicationTestCase(TestCase):
291
294
  recordings = app.save(None)
292
295
  self.assertEqual(recordings, [])
293
296
 
294
- max_id = app.recorder.max_notification_id()
295
-
296
297
  recordings = app.save(Aggregate())
297
298
  self.assertEqual(len(recordings), 1)
298
- self.assertEqual(recordings[0].notification.id, 1 + max_id)
299
+ self.assertEqual(recordings[0].notification.id, 1)
299
300
 
300
301
  recordings = app.save(Aggregate())
301
302
  self.assertEqual(len(recordings), 1)
302
- self.assertEqual(recordings[0].notification.id, 2 + max_id)
303
+ self.assertEqual(recordings[0].notification.id, 2)
303
304
 
304
305
  recordings = app.save(Aggregate(), Aggregate())
305
306
  self.assertEqual(len(recordings), 2)
306
- self.assertEqual(recordings[0].notification.id, 3 + max_id)
307
- self.assertEqual(recordings[1].notification.id, 4 + max_id)
307
+ self.assertEqual(recordings[0].notification.id, 3)
308
+ self.assertEqual(recordings[1].notification.id, 4)
308
309
 
309
- def test_take_snapshot_raises_assertion_error_if_snapshotting_not_enabled(self):
310
+ def test_take_snapshot_raises_assertion_error_if_snapshotting_not_enabled(
311
+ self,
312
+ ) -> None:
310
313
  app = Application()
311
314
  with self.assertRaises(AssertionError) as cm:
312
315
  app.take_snapshot(uuid4())
@@ -319,12 +322,13 @@ class ApplicationTestCase(TestCase):
319
322
  "application class.",
320
323
  )
321
324
 
322
- def test_application_with_cached_aggregates_and_fastforward(self):
325
+ def test_application_with_cached_aggregates_and_fastforward(self) -> None:
323
326
  app = Application(env={"AGGREGATE_CACHE_MAXSIZE": "10"})
324
327
 
325
328
  aggregate = Aggregate()
326
329
  app.save(aggregate)
327
330
  # Should not put the aggregate in the cache.
331
+ assert app.repository.cache is not None # for mypy
328
332
  with self.assertRaises(KeyError):
329
333
  self.assertEqual(aggregate, app.repository.cache.get(aggregate.id))
330
334
 
@@ -344,111 +348,171 @@ class ApplicationTestCase(TestCase):
344
348
  app.repository.get(aggregate.id)
345
349
  self.assertEqual(aggregate, app.repository.cache.get(aggregate.id))
346
350
 
347
- def test_application_fastforward_skipping_during_contention(self):
348
- app = Application(
351
+ def test_check_aggregate_fastforwarding_nonblocking(self) -> None:
352
+ self._check_aggregate_fastforwarding_during_contention(
349
353
  env={
350
354
  "AGGREGATE_CACHE_MAXSIZE": "10",
351
355
  "AGGREGATE_CACHE_FASTFORWARD_SKIPPING": "y",
352
356
  }
353
357
  )
354
358
 
355
- aggregate = Aggregate()
356
- aggregate_id = aggregate.id
357
- app.save(aggregate)
359
+ def test_check_aggregate_fastforwarding_blocking(self) -> None:
360
+ self._check_aggregate_fastforwarding_during_contention(
361
+ env={"AGGREGATE_CACHE_MAXSIZE": "10"}
362
+ )
363
+
364
+ def _check_aggregate_fastforwarding_during_contention(self, env: EnvType) -> None:
365
+ app = Application(env=env)
366
+
367
+ self.assertEqual(len(app.repository._fastforward_locks_inuse), 0)
368
+
369
+ # Create one aggregate.
370
+ original_aggregate = Aggregate()
371
+ app.save(original_aggregate)
372
+ obj_ids = set()
373
+
374
+ # Prime the cache.
375
+ app.repository.get(original_aggregate.id)
376
+
377
+ # Remember the aggregate ID.
378
+ aggregate_id = original_aggregate.id
358
379
 
359
380
  stopped = Event()
381
+ errors: list[BaseException] = []
382
+ successful_thread_ids = set()
360
383
 
361
- # Trigger, save, get, check.
362
- def trigger_save_get_check():
384
+ def trigger_save_get_check() -> None:
363
385
  while not stopped.is_set():
364
386
  try:
365
- aggregate = app.repository.get(aggregate_id)
387
+ # Get the aggregate.
388
+ aggregate: Aggregate = app.repository.get(aggregate_id)
389
+ original_version = aggregate.version
390
+
391
+ # Try to record a new event.
366
392
  aggregate.trigger_event(Aggregate.Event)
367
- saved_version = aggregate.version
393
+ # Give other threads a chance.
368
394
  try:
369
395
  app.save(aggregate)
370
396
  except IntegrityError:
397
+ # Start again if we didn't record a new event.
398
+ # print("Got integrity error")
399
+ sleep(0.001)
371
400
  continue
372
- cached_version = app.repository.get(aggregate_id).version
373
- if saved_version > cached_version:
374
- print(f"Skipped fast-forwarding at version {saved_version}")
375
- stopped.set()
376
- if aggregate.version % 1000 == 0:
377
- print("Version:", aggregate.version, get_ident())
378
- sleep(0.00)
379
- except BaseException:
380
- print(traceback.format_exc())
381
- raise
382
401
 
383
- executor = ThreadPoolExecutor(max_workers=100)
384
- for _ in range(100):
385
- executor.submit(trigger_save_get_check)
402
+ # Get the aggregate from the cache.
403
+ assert app.repository.cache is not None
404
+ cached: Any = app.repository.cache.get(aggregate_id)
405
+ obj_ids.add(id(cached))
386
406
 
387
- if not stopped.wait(timeout=100):
388
- stopped.set()
389
- self.fail("Didn't skip fast forwarding before test timed out...")
390
- executor.shutdown()
407
+ if len(obj_ids) > 1:
408
+ stopped.set()
409
+ continue
391
410
 
392
- def test_application_fastforward_blocking_during_contention(self):
393
- app = Application(
394
- env={
395
- "AGGREGATE_CACHE_MAXSIZE": "10",
396
- }
397
- )
411
+ # Fast-forward the cached aggregate.
412
+ fastforwarded: Aggregate = app.repository.get(aggregate_id)
398
413
 
399
- aggregate = Aggregate()
400
- aggregate_id = aggregate.id
401
- app.save(aggregate)
414
+ # Check cached aggregate was fast-forwarded with recorded event.
415
+ if fastforwarded.version < original_version:
416
+ try:
417
+ self.fail(
418
+ f"Failed to fast-forward at version {original_version}"
419
+ )
420
+ except AssertionError as e:
421
+ errors.append(e)
422
+ stopped.set()
423
+ continue
402
424
 
403
- stopped = Event()
425
+ # Monitor number of threads getting involved.
426
+ thread_id = get_ident()
427
+ successful_thread_ids.add(thread_id)
404
428
 
405
- # Trigger, save, get, check.
406
- def trigger_save_get_check():
407
- while not stopped.is_set():
408
- try:
409
- aggregate = app.repository.get(aggregate_id)
410
- aggregate.trigger_event(Aggregate.Event)
411
- saved_version = aggregate.version
412
- try:
413
- app.save(aggregate)
414
- except IntegrityError:
415
- continue
416
- cached_version = app.repository.get(aggregate_id).version
417
- if saved_version > cached_version:
418
- print(f"Skipped fast-forwarding at version {saved_version}")
429
+ # print("Version:", aggregate.version, thread_id)
430
+
431
+ # See if we have done enough.
432
+ if len(successful_thread_ids) > 10 and aggregate.version >= 25:
419
433
  stopped.set()
420
- if aggregate.version % 1000 == 0:
421
- print("Version:", aggregate.version, get_ident())
422
- sleep(0.00)
423
- except BaseException:
434
+ continue
435
+
436
+ sleep(0.0001)
437
+ # sleep(0.001)
438
+ except BaseException as e:
439
+ errors.append(e)
440
+ stopped.set()
424
441
  print(traceback.format_exc())
425
442
  raise
426
443
 
427
444
  executor = ThreadPoolExecutor(max_workers=100)
445
+ futures = []
428
446
  for _ in range(100):
429
- executor.submit(trigger_save_get_check)
430
-
431
- if not stopped.wait(timeout=3):
432
- stopped.set()
433
- else:
434
- self.fail("Wrongly skipped fast forwarding")
447
+ f = executor.submit(trigger_save_get_check)
448
+ futures.append(f)
449
+
450
+ # Run for three seconds.
451
+ stopped.wait(timeout=10)
452
+ for f in futures:
453
+ f.result()
454
+ # print("Got all results, shutting down executor")
435
455
  executor.shutdown()
436
456
 
437
- def test_application_with_cached_aggregates_not_fastforward(self):
457
+ try:
458
+ if errors:
459
+ raise errors[0]
460
+ if len(obj_ids) > 1:
461
+ self.fail(f"More than one instance used in the cache: {len(obj_ids)}")
462
+ if len(successful_thread_ids) < 3:
463
+ self.fail("Insufficient sharing across contentious threads")
464
+
465
+ final_aggregate: Aggregate = app.repository.get(aggregate_id)
466
+ # print("Final aggregate version:", final_aggregate.version)
467
+ if final_aggregate.version < 25:
468
+ self.fail(f"Insufficient version increment: {final_aggregate.version}")
469
+
470
+ self.assertEqual(len(app.repository._fastforward_locks_inuse), 0)
471
+
472
+ finally:
473
+ # print("Closing application")
474
+ app.close()
475
+
476
+ def test_application_with_cached_aggregates_not_fastforward(self) -> None:
438
477
  app = Application(
439
478
  env={
440
479
  "AGGREGATE_CACHE_MAXSIZE": "10",
441
480
  "AGGREGATE_CACHE_FASTFORWARD": "f",
442
481
  }
443
482
  )
444
- aggregate = Aggregate()
445
- app.save(aggregate)
446
- # Should put the aggregate in the cache.
447
- self.assertEqual(aggregate, app.repository.cache.get(aggregate.id))
448
- app.repository.get(aggregate.id)
449
- self.assertEqual(aggregate, app.repository.cache.get(aggregate.id))
483
+ aggregate1 = Aggregate()
484
+ app.save(aggregate1)
485
+ aggregate_id = aggregate1.id
450
486
 
451
- def test_application_with_deepcopy_from_cache_arg(self):
487
+ # Should put the aggregate in the cache.
488
+ assert app.repository.cache is not None # for mypy
489
+ self.assertEqual(aggregate1, app.repository.cache.get(aggregate_id))
490
+ app.repository.get(aggregate_id)
491
+ self.assertEqual(aggregate1, app.repository.cache.get(aggregate_id))
492
+
493
+ aggregate2 = Aggregate()
494
+ aggregate2._id = aggregate_id
495
+ aggregate2.trigger_event(Aggregate.Event)
496
+
497
+ # This will replace object in cache.
498
+ app.save(aggregate2)
499
+
500
+ self.assertEqual(aggregate2.version, aggregate1.version + 1)
501
+ aggregate3: Aggregate = app.repository.get(aggregate_id)
502
+ self.assertEqual(aggregate3.version, aggregate3.version)
503
+ self.assertEqual(id(aggregate3.version), id(aggregate3.version))
504
+
505
+ # This will mess things up because the cache has a stale aggregate.
506
+ aggregate3.trigger_event(Aggregate.Event)
507
+ app.events.put(aggregate3.collect_events())
508
+
509
+ # And so using the aggregate to record new events will cause an IntegrityError.
510
+ aggregate4: Aggregate = app.repository.get(aggregate_id)
511
+ aggregate4.trigger_event(Aggregate.Event)
512
+ with self.assertRaises(IntegrityError):
513
+ app.save(aggregate4)
514
+
515
+ def test_application_with_deepcopy_from_cache_arg(self) -> None:
452
516
  app = Application(
453
517
  env={
454
518
  "AGGREGATE_CACHE_MAXSIZE": "10",
@@ -457,14 +521,15 @@ class ApplicationTestCase(TestCase):
457
521
  aggregate = Aggregate()
458
522
  app.save(aggregate)
459
523
  self.assertEqual(aggregate.version, 1)
460
- aggregate = app.repository.get(aggregate.id)
461
- aggregate.version = 101
524
+ reconstructed: Aggregate = app.repository.get(aggregate.id)
525
+ reconstructed.version = 101
526
+ assert app.repository.cache is not None # for mypy
462
527
  self.assertEqual(app.repository.cache.get(aggregate.id).version, 1)
463
- aggregate = app.repository.get(aggregate.id, deepcopy_from_cache=False)
464
- aggregate.version = 101
528
+ cached: Aggregate = app.repository.get(aggregate.id, deepcopy_from_cache=False)
529
+ cached.version = 101
465
530
  self.assertEqual(app.repository.cache.get(aggregate.id).version, 101)
466
531
 
467
- def test_application_with_deepcopy_from_cache_attribute(self):
532
+ def test_application_with_deepcopy_from_cache_attribute(self) -> None:
468
533
  app = Application(
469
534
  env={
470
535
  "AGGREGATE_CACHE_MAXSIZE": "10",
@@ -473,15 +538,16 @@ class ApplicationTestCase(TestCase):
473
538
  aggregate = Aggregate()
474
539
  app.save(aggregate)
475
540
  self.assertEqual(aggregate.version, 1)
476
- aggregate = app.repository.get(aggregate.id)
477
- aggregate.version = 101
541
+ reconstructed: Aggregate = app.repository.get(aggregate.id)
542
+ reconstructed.version = 101
543
+ assert app.repository.cache is not None # for mypy
478
544
  self.assertEqual(app.repository.cache.get(aggregate.id).version, 1)
479
545
  app.repository.deepcopy_from_cache = False
480
- aggregate = app.repository.get(aggregate.id)
481
- aggregate.version = 101
546
+ cached: Aggregate = app.repository.get(aggregate.id)
547
+ cached.version = 101
482
548
  self.assertEqual(app.repository.cache.get(aggregate.id).version, 101)
483
549
 
484
- def test_application_log(self):
550
+ def test_application_log(self) -> None:
485
551
  # Check the old 'log' attribute presents the 'notification log' object.
486
552
  app = Application()
487
553
 
@@ -491,6 +557,6 @@ class ApplicationTestCase(TestCase):
491
557
 
492
558
  self.assertEqual(1, len(w))
493
559
  self.assertIs(w[-1].category, DeprecationWarning)
494
- self.assertEqual(
495
- "'log' is deprecated, use 'notifications' instead", w[-1].message.args[0]
560
+ self.assertIn(
561
+ "'log' is deprecated, use 'notifications' instead", str(w[-1].message)
496
562
  )