aspyx 1.3.0__tar.gz → 1.4.1__tar.gz

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 aspyx might be problematic. Click here for more details.

Files changed (38) hide show
  1. {aspyx-1.3.0/src/aspyx.egg-info → aspyx-1.4.1}/PKG-INFO +235 -93
  2. {aspyx-1.3.0 → aspyx-1.4.1}/README.md +232 -92
  3. {aspyx-1.3.0 → aspyx-1.4.1}/pyproject.toml +6 -1
  4. aspyx-1.4.1/src/aspyx/__init__.py +0 -0
  5. {aspyx-1.3.0 → aspyx-1.4.1}/src/aspyx/di/__init__.py +2 -2
  6. aspyx-1.4.1/src/aspyx/di/aop/__init__.py +28 -0
  7. {aspyx-1.3.0 → aspyx-1.4.1}/src/aspyx/di/aop/aop.py +169 -102
  8. {aspyx-1.3.0 → aspyx-1.4.1}/src/aspyx/di/configuration/__init__.py +3 -3
  9. {aspyx-1.3.0 → aspyx-1.4.1}/src/aspyx/di/configuration/configuration.py +7 -5
  10. {aspyx-1.3.0 → aspyx-1.4.1}/src/aspyx/di/di.py +224 -46
  11. {aspyx-1.3.0 → aspyx-1.4.1}/src/aspyx/di/threading/synchronized.py +8 -5
  12. aspyx-1.4.1/src/aspyx/exception/__init__.py +10 -0
  13. aspyx-1.4.1/src/aspyx/exception/exception_manager.py +185 -0
  14. {aspyx-1.3.0 → aspyx-1.4.1}/src/aspyx/reflection/proxy.py +2 -4
  15. {aspyx-1.3.0 → aspyx-1.4.1}/src/aspyx/reflection/reflection.py +81 -5
  16. aspyx-1.4.1/src/aspyx/threading/__init__.py +10 -0
  17. aspyx-1.4.1/src/aspyx/threading/thread_local.py +51 -0
  18. {aspyx-1.3.0/src/aspyx/di → aspyx-1.4.1/src/aspyx}/util/stringbuilder.py +15 -0
  19. {aspyx-1.3.0 → aspyx-1.4.1/src/aspyx.egg-info}/PKG-INFO +235 -93
  20. {aspyx-1.3.0 → aspyx-1.4.1}/src/aspyx.egg-info/SOURCES.txt +10 -3
  21. aspyx-1.4.1/src/aspyx.egg-info/requires.txt +3 -0
  22. {aspyx-1.3.0 → aspyx-1.4.1}/tests/test_aop.py +67 -8
  23. {aspyx-1.3.0 → aspyx-1.4.1}/tests/test_configuration.py +8 -8
  24. aspyx-1.3.0/tests/test_di_cycle.py → aspyx-1.4.1/tests/test_cycle.py +4 -4
  25. {aspyx-1.3.0 → aspyx-1.4.1}/tests/test_di.py +54 -21
  26. aspyx-1.4.1/tests/test_exception_manager.py +76 -0
  27. aspyx-1.3.0/src/aspyx/di/aop/__init__.py +0 -14
  28. {aspyx-1.3.0 → aspyx-1.4.1}/LICENSE +0 -0
  29. {aspyx-1.3.0 → aspyx-1.4.1}/setup.cfg +0 -0
  30. {aspyx-1.3.0 → aspyx-1.4.1}/src/aspyx/di/configuration/env_configuration_source.py +0 -0
  31. {aspyx-1.3.0 → aspyx-1.4.1}/src/aspyx/di/configuration/yaml_configuration_source.py +0 -0
  32. {aspyx-1.3.0 → aspyx-1.4.1}/src/aspyx/di/threading/__init__.py +0 -0
  33. {aspyx-1.3.0 → aspyx-1.4.1}/src/aspyx/reflection/__init__.py +0 -0
  34. {aspyx-1.3.0/src/aspyx/di → aspyx-1.4.1/src/aspyx}/util/__init__.py +0 -0
  35. {aspyx-1.3.0 → aspyx-1.4.1}/src/aspyx.egg-info/dependency_links.txt +0 -0
  36. {aspyx-1.3.0 → aspyx-1.4.1}/src/aspyx.egg-info/top_level.txt +0 -0
  37. {aspyx-1.3.0 → aspyx-1.4.1}/tests/test_proxy.py +0 -0
  38. {aspyx-1.3.0 → aspyx-1.4.1}/tests/test_reflection.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aspyx
3
- Version: 1.3.0
3
+ Version: 1.4.1
4
4
  Summary: A DI and AOP library for Python
5
5
  Author-email: Andreas Ernst <andreas.ernst7@gmail.com>
6
6
  License: MIT License
@@ -28,6 +28,8 @@ License: MIT License
28
28
  Requires-Python: >=3.9
29
29
  Description-Content-Type: text/markdown
30
30
  License-File: LICENSE
31
+ Provides-Extra: dev
32
+ Requires-Dist: mkdocstrings-python; extra == "dev"
31
33
  Dynamic: license-file
32
34
 
33
35
  # aspyx
@@ -38,11 +40,14 @@ Dynamic: license-file
38
40
  ![License](https://img.shields.io/github/license/coolsamson7/aspyx)
39
41
  ![coverage](https://img.shields.io/badge/coverage-94%25-brightgreen)
40
42
  [![PyPI](https://img.shields.io/pypi/v/aspyx)](https://pypi.org/project/aspyx/)
43
+ [![Docs](https://img.shields.io/badge/docs-online-blue?logo=github)](https://coolsamson7.github.io/aspyx/index/introduction)
41
44
 
42
- ## Table of Contents
45
+ ![image](https://github.com/user-attachments/assets/e808210a-b1a4-4fd0-93f1-b5f9845fa520)
46
+
47
+ ## Table of Contents
43
48
 
44
49
  - [Motivation](#motivation)
45
- - [Introduction](#introduction)
50
+ - [Overview](#overview)
46
51
  - [Installation](#installation)
47
52
  - [Registration](#registration)
48
53
  - [Class](#class)
@@ -58,85 +63,97 @@ Dynamic: license-file
58
63
  - [Post Processors](#post-processors)
59
64
  - [Custom scopes](#custom-scopes)
60
65
  - [AOP](#aop)
66
+ - [Threading](#threading)
61
67
  - [Configuration](#configuration)
62
68
  - [Reflection](#reflection)
69
+ - [Exceptions](#exceptions)
63
70
  - [Version History](#version-history)
64
71
 
65
72
  # Motivation
66
73
 
67
- While working on some AI related topics in Python, i required a simple DI framework.
68
- Looking at the existing solutions - there are quite a number of - i was not quite happy. Either the solutions
69
- lacked functionality - starting with that usually aop and di are separate libraries -, that i am accustomed to from other languages, or the API was in my mind too clumsy and "technical".
74
+ While working on AI-related projects in Python, I was looking for a dependency injection (DI) framework. After evaluating existing options, my impression was that the most either lacked key features — such as integrated AOP — or had APIs that felt overly technical and complex, which made me develop a library on my own with the following goals
75
+
76
+ - bring both di and AOP features together in a lightweight library ( still only about 2T loc),
77
+ - be as minimal invasive as possible,
78
+ - offering mechanisms to easily extend and customize features without touching the core,
79
+ - while still offering a _simple_ and _readable_ api that doesnt overwhelm developers and only requires a minimum initial learning curve
80
+
81
+ The AOP integration, in particular, makes a lot of sense because:
70
82
 
71
- So, why not develop one on my own...Having done that in other languages previously, the task was not that hard, and last but not least...it is fun :-)
83
+ - Aspects typically require context, which is naturally provided through DI,
84
+ - And they should only apply to objects managed by the container, rather than acting globally.
72
85
 
73
- # Introduction
86
+ # Overview
74
87
 
75
- Aspyx is a small python libary, that adds support for both dependency injection and aop.
88
+ Aspyx is a lightweight - still only about 2T LOC- Python library that provides both Dependency Injection (DI) and Aspect-Oriented Programming (AOP) support.
76
89
 
77
- The following di features are supported
90
+ The following DI features are supported
78
91
  - constructor and setter injection
92
+ - injection of configuration variables
79
93
  - possibility to define custom injections
80
94
  - post processors
81
95
  - support for factory classes and methods
82
- - support for eager construction
83
- - support for scopes singleton, request and thread
84
- - possibilty to add custom scopes
85
- - conditional registration of classes and factories
86
- - lifecycle events methods `on_init`, `on_destroy`
87
- - bundling of injectable objects according to their module location including recursive imports and inheritance
88
- - instatiation of - possibly multiple - container instances - so called environments - that manage the lifecylce of related objects
89
- - filtering of classes by associating "features" sets to environment ( similar to spring profiles )
90
- - hierarchical environments
91
-
92
- With respect to aop:
96
+ - support for eager and lazy construction
97
+ - support for scopes "singleton", "request" and "thread"
98
+ - possibility to add custom scopes
99
+ - conditional registration of classes and factories ( aka profiles in spring )
100
+ - lifecycle events methods `on_init`, `on_destroy`, `on_running`
101
+ - Automatic discovery and bundling of injectable objects based on their module location, including support for recursive imports
102
+ - Instantiation of one or possible more isolated container instances called environments each managing the lifecycle of a related set of objects,
103
+ - Support for hierarchical environments, enabling structured scoping and layered object management.
104
+
105
+ With respect to AOP:
93
106
  - support for before, around, after and error aspects
107
+ - simple fluent interface to specify which methods are targeted by an aspect
94
108
  - sync and async method support
95
- - `synchronized` decorator that adds locking to methods
96
109
 
97
- The library is thread-safe!
110
+ The library is thread-safe and heavily performance optimized as most of the runtime information is precomputed and cached!
98
111
 
99
112
  Let's look at a simple example
100
113
 
101
114
  ```python
102
- from aspyx.di import injectable, on_init, on_destroy, environment, Environment
115
+ from aspyx.di import injectable, on_init, on_destroy, module, Environment
116
+
103
117
 
104
118
  @injectable()
105
119
  class Foo:
106
120
  def __init__(self):
107
121
  pass
108
122
 
109
- def hello(msg: str):
123
+ def hello(self, msg: str):
110
124
  print(f"hello {msg}")
111
125
 
126
+
112
127
  @injectable() # eager and singleton by default
113
128
  class Bar:
114
- def __init__(self, foo: Foo): # will inject the Foo dependency
129
+ def __init__(self, foo: Foo): # will inject the Foo dependency
115
130
  self.foo = foo
116
131
 
117
- @on_init() # a lifecycle callback called after the constructor and all possible injections
132
+ @on_init() # a lifecycle callback called after the constructor and all possible injections
118
133
  def init(self):
119
134
  ...
120
135
 
121
136
 
122
- # this class will register all - specifically decorated - classes and factories in the own module
123
- # In this case Foo and Bar
137
+ # this class will discover and manage all - specifically decorated - classes and factories that are part of the own module
124
138
 
125
- @environment()
126
- class SampleEnvironment:
139
+ @module()
140
+ class SampleModule:
127
141
  def __init__(self):
128
142
  pass
129
143
 
130
- # go, forrest
131
144
 
132
- environment = Environment(SampleEnvironment)
145
+ # create environment
146
+
147
+ environment = Environment(SampleModule)
148
+
149
+ # fetch an instance
133
150
 
134
151
  bar = env.get(Bar)
135
152
 
136
153
  bar.foo.hello("world")
137
154
  ```
138
155
 
139
- The concepts should be pretty familiar as well as the names which are a combination of Spring and Angular names :-)
156
+ The concepts should be pretty familiar as well as the names as they are inspired by both Spring and Angular.
140
157
 
141
158
  Let's add some aspects...
142
159
 
@@ -149,26 +166,19 @@ class SampleAdvice:
149
166
 
150
167
  @before(methods().named("hello").of_type(Foo))
151
168
  def call_before(self, invocation: Invocation):
152
- print("before Foo.hello(...)")
169
+ ...
153
170
 
154
171
  @error(methods().named("hello").of_type(Foo))
155
172
  def call_error(self, invocation: Invocation):
156
- print("error Foo.hello(...)")
157
- print(invocation.exception)
173
+ ... # exception accessible in invocation.exception
158
174
 
159
175
  @around(methods().named("hello"))
160
176
  def call_around(self, invocation: Invocation):
161
- print("around Foo.hello()")
162
-
177
+ ...
163
178
  return invocation.proceed()
164
179
  ```
165
180
 
166
- The invocation parameter stores the complete context of the current execution, which are
167
- - the method
168
- - args
169
- - kwargs
170
- - the result
171
- - the possible caught error
181
+ While features like DI and AOP are often associated with enterprise applcations, this example hopefully demonstrates that they work just as well in small- to medium-sized projects—without introducing significant overhead—while still providing powerful tools for achieving clean architecture, resulting in maintainable and easily testable code.
172
182
 
173
183
  Let's look at the details
174
184
 
@@ -196,8 +206,8 @@ class Foo:
196
206
  def __init__(self):
197
207
  pass
198
208
  ```
199
- Please make sure, that the class defines a local constructor, as this is required to determine injected instances.
200
- All referenced types will be injected by the environemnt.
209
+ ⚠️ **Attention:** Please make sure, that the class defines a local constructor, as this is _required_ to determine injected instances.
210
+ All referenced types will be injected by the environment.
201
211
 
202
212
  Only eligible types are allowed, of course!
203
213
 
@@ -272,19 +282,26 @@ Valid conditions are created by:
272
282
 
273
283
  ## Definition
274
284
 
275
- An `Environment` is the container that manages the lifecycle of objects. The set of classes and instances is determined by a constructor argument that controls the class registry.
285
+ An `Environment` is the container that manages the lifecycle of objects.
286
+ The set of classes and instances is determined by a
287
+ constructor type argument called `module`.
276
288
 
277
289
  **Example**:
278
290
  ```python
279
- @environment()
280
- class SampleEnvironment:
291
+ @module()
292
+ class SampleModule:
281
293
  def __init__(self):
282
294
  pass
283
-
284
- environment = Environment(SampleEnvironment)
285
295
  ```
286
296
 
287
- The default is that all eligible classes, that are implemented in the containing module or in any submodule will be managed.
297
+ A module is a regular injectable class decorated with `@module` that controls the discovery of injectable classes, by filtering classes according to their module location relative to this class.
298
+ All eligible classes, that are implemented in the containing module or in any submodule will be managed.
299
+
300
+ In a second step the real container - the environment - is created based on a module:
301
+
302
+ ```python
303
+ environment = Environment(SampleModule, features=["dev"])
304
+ ```
288
305
 
289
306
  By adding the parameter `features: list[str]`, it is possible to filter injectables by evaluating the corresponding `@conditional` decorators.
290
307
 
@@ -297,21 +314,21 @@ class DevOnly:
297
314
  def __init__(self):
298
315
  pass
299
316
 
300
- @environment()
301
- class SampleEnvironmen()):
317
+ @module()
318
+ class SampleModule():
302
319
  def __init__(self):
303
320
  pass
304
321
 
305
- environment = Environment(SampleEnvironment, features=["dev"])
322
+ environment = Environment(SampleModule, features=["dev"])
306
323
  ```
307
324
 
308
325
 
309
- By adding an `imports: list[Type]` parameter, specifying other environment types, it will register the appropriate classes recursively.
326
+ By adding an `imports: list[Type]` parameter, specifying other module types, it will register the appropriate classes recursively.
310
327
 
311
328
  **Example**:
312
329
  ```python
313
- @environment()
314
- class SampleEnvironmen(imports=[OtherEnvironment])):
330
+ @module()
331
+ class SampleModule(imports=[OtherModule]):
315
332
  def __init__(self):
316
333
  pass
317
334
  ```
@@ -320,8 +337,9 @@ Another possibility is to add a parent environment as an `Environment` construct
320
337
 
321
338
  **Example**:
322
339
  ```python
323
- rootEnvironment = Environment(RootEnvironment)
324
- environment = Environment(SampleEnvironment, parent=rootEnvironment)
340
+ rootEnvironment = Environment(RootModule)
341
+
342
+ environment = Environment(SampleModule, parent=rootEnvironment)
325
343
  ```
326
344
 
327
345
  The difference is, that in the first case, class instances of imported modules will be created in the scope of the _own_ environment, while in the second case, it will return instances managed by the parent.
@@ -344,7 +362,7 @@ The container knows about class hierarchies and is able to `get` base classes, a
344
362
 
345
363
  In case of ambiguities, it will throw an exception.
346
364
 
347
- Please be aware, that a base class are not _required_ to be annotated with `@injectable`, as this would mean, that it could be created on its own as well. ( Which is possible as well, btw. )
365
+ Note that a base class are not _required_ to be annotated with `@injectable`, as this would mean, that it could be created on its own as well. ( Which is possible as well, btw. )
348
366
 
349
367
  # Instantiation logic
350
368
 
@@ -357,7 +375,7 @@ Constructing a new instance involves a number of steps executed in this order
357
375
  different decorators can mark methods that should be called during the lifecycle ( here the construction ) of an instance.
358
376
  These are various injection possibilities as well as an optional final `on_init` call
359
377
  - PostProcessors
360
- Any custom post processors, that can add isde effects or modify the instances
378
+ Any custom post processors, that can add side effects or modify the instances
361
379
 
362
380
  ## Injection methods
363
381
 
@@ -392,7 +410,7 @@ It is possible to mark specific lifecyle methods.
392
410
  - `@on_init()`
393
411
  called after the constructor and all other injections.
394
412
  - `@on_running()`
395
- called an environment has initialized all eager objects.
413
+ called after an environment has initialized completely ( e.g. created all eager objects ).
396
414
  - `@on_destroy()`
397
415
  called during shutdown of the environment
398
416
 
@@ -410,7 +428,7 @@ class SamplePostProcessor(PostProcessor):
410
428
 
411
429
  Any implementing class of `PostProcessor` that is eligible for injection will be called by passing the new instance.
412
430
 
413
- Please be aware, that a post processor will only handle instances _after_ its _own_ registration.
431
+ Note that a post processor will only handle instances _after_ its _own_ registration.
414
432
 
415
433
  As injectables within a single file will be handled in the order as they are declared, a post processor will only take effect for all classes after its declaration!
416
434
 
@@ -427,7 +445,7 @@ def get(self, provider: AbstractInstanceProvider, environment: Environment, argP
427
445
  Arguments are:
428
446
  - `provider` the actual provider that will create an instance
429
447
  - `environment`the requesting environment
430
- - `argPovider` a function that can be called to compute the required arguments recursively
448
+ - `argProvider` a function that can be called to compute the required arguments recursively
431
449
 
432
450
  **Example**: The simplified code of the singleton provider ( disregarding locking logic )
433
451
 
@@ -452,52 +470,64 @@ class SingletonScope(Scope):
452
470
 
453
471
  # AOP
454
472
 
455
- It is possible to define different Aspects, that will be part of method calling flow. This logic fits nicely in the library, since the DI framework controls the instantiation logic and can handle aspects within a regular post processor.
473
+ It is possible to define different aspects, that will be part of method calling flow. This logic fits nicely in the library, since the DI framework controls the instantiation logic and can handle aspects within a regular post processor.
474
+
475
+ On the other hand, advices are also regular DI objects, as they will usually require some kind of - injected - context.
456
476
 
457
- Advice classes need to be part of classes that add a `@advice()` decorator and can define methods that add aspects.
477
+ Advices are regular classes decorated with `@advice` that define aspect methods.
458
478
 
459
479
  ```python
460
- @advice()
480
+ @advice
461
481
  class SampleAdvice:
462
482
  def __init__(self): # could inject dependencies
463
483
  pass
464
484
 
465
485
  @before(methods().named("hello").of_type(Foo))
466
486
  def call_before(self, invocation: Invocation):
467
- # arguments: invocation.args
468
- print("before Foo.hello(...)")
487
+ # arguments: invocation.args and invocation.kwargs
488
+ ...
489
+
490
+ @after(methods().named("hello").of_type(Foo))
491
+ def call_after(self, invocation: Invocation):
492
+ # arguments: invocation.args and invocation.kwargs
493
+ ...
469
494
 
470
495
  @error(methods().named("hello").of_type(Foo))
471
496
  def call_error(self, invocation: Invocation):
472
- print("error Foo.hello(...)")
473
- print(invocation.exception)
497
+ # error: invocation.exception
498
+ ...
474
499
 
475
500
  @around(methods().named("hello"))
476
501
  def call_around(self, invocation: Invocation):
477
- print("around Foo.hello()")
478
-
479
- return invocation.proceed() # will leave a result in invocation.result or invocation.exception in case of an exception
502
+ try:
503
+ ...
504
+ return invocation.proceed() # will leave a result in invocation.result or invocation.exception in case of an exception
505
+ finally:
506
+ ...
480
507
  ```
481
508
 
482
509
  Different aspects - with the appropriate decorator - are possible:
483
510
  - `before`
484
511
  methods that will be executed _prior_ to the original method
485
512
  - `around`
486
- methods that will be executed _around_ to the original method giving it the possibility to add side effects or even change the parameters.
513
+ methods that will be executed _around_ to the original method allowing you to add side effects or even modify parameters.
487
514
  - `after`
488
515
  methods that will be executed _after_ to the original method
489
516
  - `error`
490
517
  methods that will be executed in case of a caught exception
491
518
 
519
+ The different aspects can be supplemented with an `@order(<prio>)` decorator that controls the execution order based on the passed number. Smaller values get executed first.
520
+
492
521
  All methods are expected to have single `Invocation` parameter, that stores
493
522
 
494
523
  - `func` the target function
495
- - `args` the suppliued args
524
+ - `args` the supplied args ( including the `self` instance as the first element)
496
525
  - `kwargs` the keywords args
497
526
  - `result` the result ( initially `None`)
498
- - `exception` a possible caught excpetion ( initially `None`)
527
+ - `exception` a possible caught exception ( initially `None`)
528
+
529
+ ⚠️ **Note:** It is essential for `around` methods to call `proceed()` on the invocation, which will call the next around method in the chain and finally the original method.
499
530
 
500
- It is essential for `around` methods to call `proceed()` on the invocation, which will call the next around method in the chain and finally the original method.
501
531
  If the `proceed` is called with parameters, they will replace the original parameters!
502
532
 
503
533
  **Example**: Parameter modifications
@@ -505,9 +535,7 @@ If the `proceed` is called with parameters, they will replace the original param
505
535
  ```python
506
536
  @around(methods().named("say"))
507
537
  def call_around(self, invocation: Invocation):
508
- args = [invocation.args[0],invocation.args[1] + "!"] # 0 is self!
509
-
510
- return invocation.proceed(*args)
538
+ return invocation.proceed(invocation.args[0], invocation.args[1] + "!") # 0 is self!
511
539
  ```
512
540
 
513
541
  The argument list to the corresponding decorators control which methods are targeted by the advice.
@@ -532,7 +560,7 @@ The fluent methods `named`, `matches` and `of_type` can be called multiple times
532
560
  **Example**: react on both `transactional` decorators on methods or classes
533
561
 
534
562
  ```python
535
- @injectable()
563
+ @advice
536
564
  class TransactionAdvice:
537
565
  def __init__(self):
538
566
  pass
@@ -542,9 +570,29 @@ class TransactionAdvice:
542
570
  ...
543
571
  ```
544
572
 
545
- With respect to async methods, you need to make sure, to replace a `proceeed()` with a `await proceed_async()` to have the overall chain async!
573
+ With respect to async methods, you need to make sure, to replace a `proceed()` with a `await proceed_async()` to have the overall chain async!
574
+
575
+ ## Advice Lifecycle and visibility.
546
576
 
547
- A handy decorator `@synchronized` is implemented that automatically synchronizes methods with a `RLock` associated with the instance.
577
+ Advices are always part of a specific environment, and only modify methods of objects managed by exactly this environment.
578
+
579
+ An advice of a parent environment will for example not see classes of inherited environments. What is done instead, is to recreate the advice - more technically speaking, a processor that will collect and apply the advices - in every child environment, and let it operate on the local objects. With this approach different environments are completely isolated from each other with no side effects whatsoever.
580
+
581
+ # Threading
582
+
583
+ A handy decorator `@synchronized` in combination with the respective advice is implemented that automatically synchronizes methods with a `RLock` associated with the instance.
584
+
585
+ **Example**:
586
+ ```python
587
+ @injectable()
588
+ class Foo:
589
+ def __init__(self):
590
+ pass
591
+
592
+ @synchronized()
593
+ def execute_synchronized(self):
594
+ ...
595
+ ```
548
596
 
549
597
  # Configuration
550
598
 
@@ -561,9 +609,9 @@ class Foo:
561
609
  ...
562
610
  ```
563
611
 
564
- If required a coercion will be executed.
612
+ If required type coercion will be applied.
565
613
 
566
- This concept relies on a central object `ConfigurationManager` that stores the overall configuration values as provided by so called configuration sources that are defined as follows.
614
+ Configuration values are managed centrally using a `ConfigurationManager`, which aggregates values from various configuration sources that are defined as follows.
567
615
 
568
616
  ```python
569
617
  class ConfigurationSource(ABC):
@@ -620,8 +668,8 @@ Two specific source are already implemented:
620
668
  Typically you create the required configuration sources in an environment class, e.g.
621
669
 
622
670
  ```python
623
- @environment()
624
- class SampleEnvironment:
671
+ @module()
672
+ class SampleModule:
625
673
  # constructor
626
674
 
627
675
  def __init__(self):
@@ -640,7 +688,7 @@ class SampleEnvironment:
640
688
 
641
689
  As the library heavily relies on type introspection of classes and methods, a utility class `TypeDescriptor` is available that covers type information on classes.
642
690
 
643
- After beeing instatiated with
691
+ After being instantiated with
644
692
 
645
693
  ```python
646
694
  TypeDescriptor.for_type(<type>)
@@ -656,7 +704,7 @@ it offers the methods
656
704
  - `get_decorator(decorator) -> Optional[DecoratorDescriptor]`
657
705
  return a descriptor covering the decorator. In addition to the callable, it also stores the supplied args in the `args` property
658
706
 
659
- The returned method descriptors offer:
707
+ The returned method descriptors provide:
660
708
  - `param_types`
661
709
  list of arg types
662
710
  - `return_type`
@@ -680,6 +728,87 @@ def transactional(scope):
680
728
  return decorator
681
729
  ```
682
730
 
731
+ # Exceptions
732
+
733
+ The class `ExceptionManager` is used to collect dynamic handlers for specific exceptions and is able to dispatch to the concrete functions
734
+ given a specific exception.
735
+
736
+ The handlers are declared by annoting a class with `@exception_handler` and decorating specific methods with `@handle`
737
+
738
+ **Example**:
739
+ ```python
740
+ class DerivedException(Exception):
741
+ def __init__(self):
742
+ pass
743
+
744
+ @module()
745
+ class SampleModule:
746
+ # constructor
747
+
748
+ def __init__(self):
749
+ pass
750
+
751
+ @create()
752
+ def create_exception_manager(self) -> ExceptionManager:
753
+ return ExceptionManager()
754
+
755
+ @injectable()
756
+ @exception_handler()
757
+ class TestExceptionHandler:
758
+ def __init__(self):
759
+ pass
760
+
761
+ @handle()
762
+ def handle_derived_exception(self, exception: DerivedException):
763
+ ExceptionManager.proceed()
764
+
765
+ @handle()
766
+ def handle_exception(self, exception: Exception):
767
+ pass
768
+
769
+ @handle()
770
+ def handle_base_exception(self, exception: BaseException):
771
+ pass
772
+
773
+
774
+ @advice
775
+ class ExceptionAdvice:
776
+ def __init__(self, exceptionManager: ExceptionManager):
777
+ self.exceptionManager = exceptionManager
778
+
779
+ @error(methods().of_type(Service))
780
+ def handle_error(self, invocation: Invocation):
781
+ self.exceptionManager.handle(invocation.exception)
782
+
783
+ environment = Environment(SampleEnvironment)
784
+
785
+ environment.get(ExceptionManager).handle(DerivedException())
786
+ ```
787
+
788
+ The exception maanger will first call the most appropriate method.
789
+ Any `ExceptionManager.proceed()` will in turn call the next most applicable method ( if available).
790
+
791
+ Together with a simple around advice we can now add exception handling to any method:
792
+
793
+ **Example**:
794
+ ```python
795
+ @injectable()
796
+ class Service:
797
+ def __init__(self):
798
+ pass
799
+
800
+ def throw(self):
801
+ raise DerivedException()
802
+
803
+ @advice
804
+ class ExceptionAdvice:
805
+ def __init__(self, exceptionManager: ExceptionManager):
806
+ self.exceptionManager = exceptionManager
807
+
808
+ @error(methods().of_type(Service))
809
+ def handle_error(self, invocation: Invocation):
810
+ self.exceptionManager.handle(invocation.exception)
811
+ ```
683
812
 
684
813
  # Version History
685
814
 
@@ -696,7 +825,20 @@ def transactional(scope):
696
825
 
697
826
  - added `YamlConfigurationSource`
698
827
 
699
- **1.2.1**
828
+ **1.3.0**
829
+
830
+ - added `@conditional`
831
+ - added support for `async` advices
832
+
833
+
834
+ **1.4.0**
835
+
836
+ - bugfixes
837
+ - added `@ExceptionManager`
838
+
839
+ **1.4.1**
840
+
841
+ - mkdocs
700
842
 
701
843
 
702
844