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.
- {aspyx-1.3.0/src/aspyx.egg-info → aspyx-1.4.1}/PKG-INFO +235 -93
- {aspyx-1.3.0 → aspyx-1.4.1}/README.md +232 -92
- {aspyx-1.3.0 → aspyx-1.4.1}/pyproject.toml +6 -1
- aspyx-1.4.1/src/aspyx/__init__.py +0 -0
- {aspyx-1.3.0 → aspyx-1.4.1}/src/aspyx/di/__init__.py +2 -2
- aspyx-1.4.1/src/aspyx/di/aop/__init__.py +28 -0
- {aspyx-1.3.0 → aspyx-1.4.1}/src/aspyx/di/aop/aop.py +169 -102
- {aspyx-1.3.0 → aspyx-1.4.1}/src/aspyx/di/configuration/__init__.py +3 -3
- {aspyx-1.3.0 → aspyx-1.4.1}/src/aspyx/di/configuration/configuration.py +7 -5
- {aspyx-1.3.0 → aspyx-1.4.1}/src/aspyx/di/di.py +224 -46
- {aspyx-1.3.0 → aspyx-1.4.1}/src/aspyx/di/threading/synchronized.py +8 -5
- aspyx-1.4.1/src/aspyx/exception/__init__.py +10 -0
- aspyx-1.4.1/src/aspyx/exception/exception_manager.py +185 -0
- {aspyx-1.3.0 → aspyx-1.4.1}/src/aspyx/reflection/proxy.py +2 -4
- {aspyx-1.3.0 → aspyx-1.4.1}/src/aspyx/reflection/reflection.py +81 -5
- aspyx-1.4.1/src/aspyx/threading/__init__.py +10 -0
- aspyx-1.4.1/src/aspyx/threading/thread_local.py +51 -0
- {aspyx-1.3.0/src/aspyx/di → aspyx-1.4.1/src/aspyx}/util/stringbuilder.py +15 -0
- {aspyx-1.3.0 → aspyx-1.4.1/src/aspyx.egg-info}/PKG-INFO +235 -93
- {aspyx-1.3.0 → aspyx-1.4.1}/src/aspyx.egg-info/SOURCES.txt +10 -3
- aspyx-1.4.1/src/aspyx.egg-info/requires.txt +3 -0
- {aspyx-1.3.0 → aspyx-1.4.1}/tests/test_aop.py +67 -8
- {aspyx-1.3.0 → aspyx-1.4.1}/tests/test_configuration.py +8 -8
- aspyx-1.3.0/tests/test_di_cycle.py → aspyx-1.4.1/tests/test_cycle.py +4 -4
- {aspyx-1.3.0 → aspyx-1.4.1}/tests/test_di.py +54 -21
- aspyx-1.4.1/tests/test_exception_manager.py +76 -0
- aspyx-1.3.0/src/aspyx/di/aop/__init__.py +0 -14
- {aspyx-1.3.0 → aspyx-1.4.1}/LICENSE +0 -0
- {aspyx-1.3.0 → aspyx-1.4.1}/setup.cfg +0 -0
- {aspyx-1.3.0 → aspyx-1.4.1}/src/aspyx/di/configuration/env_configuration_source.py +0 -0
- {aspyx-1.3.0 → aspyx-1.4.1}/src/aspyx/di/configuration/yaml_configuration_source.py +0 -0
- {aspyx-1.3.0 → aspyx-1.4.1}/src/aspyx/di/threading/__init__.py +0 -0
- {aspyx-1.3.0 → aspyx-1.4.1}/src/aspyx/reflection/__init__.py +0 -0
- {aspyx-1.3.0/src/aspyx/di → aspyx-1.4.1/src/aspyx}/util/__init__.py +0 -0
- {aspyx-1.3.0 → aspyx-1.4.1}/src/aspyx.egg-info/dependency_links.txt +0 -0
- {aspyx-1.3.0 → aspyx-1.4.1}/src/aspyx.egg-info/top_level.txt +0 -0
- {aspyx-1.3.0 → aspyx-1.4.1}/tests/test_proxy.py +0 -0
- {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
|
+
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
|

|
|
39
41
|

|
|
40
42
|
[](https://pypi.org/project/aspyx/)
|
|
43
|
+
[](https://coolsamson7.github.io/aspyx/index/introduction)
|
|
41
44
|
|
|
42
|
-
|
|
45
|
+

|
|
46
|
+
|
|
47
|
+
## Table of Contents
|
|
43
48
|
|
|
44
49
|
- [Motivation](#motivation)
|
|
45
|
-
- [
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
86
|
+
# Overview
|
|
74
87
|
|
|
75
|
-
Aspyx is a
|
|
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
|
|
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
|
-
-
|
|
85
|
-
- conditional registration of classes and factories
|
|
86
|
-
- lifecycle events methods `on_init`, `on_destroy`
|
|
87
|
-
- bundling of injectable objects
|
|
88
|
-
-
|
|
89
|
-
-
|
|
90
|
-
|
|
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,
|
|
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):
|
|
129
|
+
def __init__(self, foo: Foo): # will inject the Foo dependency
|
|
115
130
|
self.foo = foo
|
|
116
131
|
|
|
117
|
-
@on_init()
|
|
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
|
|
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
|
-
@
|
|
126
|
-
class
|
|
139
|
+
@module()
|
|
140
|
+
class SampleModule:
|
|
127
141
|
def __init__(self):
|
|
128
142
|
pass
|
|
129
143
|
|
|
130
|
-
# go, forrest
|
|
131
144
|
|
|
132
|
-
|
|
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
|
|
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
|
-
|
|
169
|
+
...
|
|
153
170
|
|
|
154
171
|
@error(methods().named("hello").of_type(Foo))
|
|
155
172
|
def call_error(self, invocation: Invocation):
|
|
156
|
-
|
|
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
|
-
|
|
162
|
-
|
|
177
|
+
...
|
|
163
178
|
return invocation.proceed()
|
|
164
179
|
```
|
|
165
180
|
|
|
166
|
-
|
|
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
|
|
200
|
-
All referenced types will be injected by the
|
|
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.
|
|
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
|
-
@
|
|
280
|
-
class
|
|
291
|
+
@module()
|
|
292
|
+
class SampleModule:
|
|
281
293
|
def __init__(self):
|
|
282
294
|
pass
|
|
283
|
-
|
|
284
|
-
environment = Environment(SampleEnvironment)
|
|
285
295
|
```
|
|
286
296
|
|
|
287
|
-
|
|
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
|
-
@
|
|
301
|
-
class
|
|
317
|
+
@module()
|
|
318
|
+
class SampleModule():
|
|
302
319
|
def __init__(self):
|
|
303
320
|
pass
|
|
304
321
|
|
|
305
|
-
environment = Environment(
|
|
322
|
+
environment = Environment(SampleModule, features=["dev"])
|
|
306
323
|
```
|
|
307
324
|
|
|
308
325
|
|
|
309
|
-
By adding an `imports: list[Type]` parameter, specifying other
|
|
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
|
-
@
|
|
314
|
-
class
|
|
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(
|
|
324
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
- `
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
473
|
-
|
|
497
|
+
# error: invocation.exception
|
|
498
|
+
...
|
|
474
499
|
|
|
475
500
|
@around(methods().named("hello"))
|
|
476
501
|
def call_around(self, invocation: Invocation):
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
@
|
|
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 `
|
|
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
|
-
|
|
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
|
|
612
|
+
If required type coercion will be applied.
|
|
565
613
|
|
|
566
|
-
|
|
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
|
-
@
|
|
624
|
-
class
|
|
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
|
|
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
|
|
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.
|
|
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
|
|