aspyx 1.2.0__tar.gz → 1.3.0__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.2.0/src/aspyx.egg-info → aspyx-1.3.0}/PKG-INFO +96 -16
- {aspyx-1.2.0 → aspyx-1.3.0}/README.md +95 -15
- {aspyx-1.2.0 → aspyx-1.3.0}/pyproject.toml +1 -1
- aspyx-1.3.0/src/aspyx/di/__init__.py +38 -0
- {aspyx-1.2.0 → aspyx-1.3.0}/src/aspyx/di/aop/aop.py +87 -5
- {aspyx-1.2.0 → aspyx-1.3.0}/src/aspyx/di/configuration/env_configuration_source.py +1 -1
- {aspyx-1.2.0 → aspyx-1.3.0}/src/aspyx/di/configuration/yaml_configuration_source.py +1 -1
- {aspyx-1.2.0 → aspyx-1.3.0}/src/aspyx/di/di.py +290 -200
- aspyx-1.3.0/src/aspyx/di/threading/__init__.py +11 -0
- aspyx-1.3.0/src/aspyx/di/threading/synchronized.py +46 -0
- {aspyx-1.2.0 → aspyx-1.3.0}/src/aspyx/reflection/reflection.py +4 -1
- {aspyx-1.2.0 → aspyx-1.3.0/src/aspyx.egg-info}/PKG-INFO +96 -16
- {aspyx-1.2.0 → aspyx-1.3.0}/src/aspyx.egg-info/SOURCES.txt +2 -0
- {aspyx-1.2.0 → aspyx-1.3.0}/tests/test_aop.py +49 -17
- {aspyx-1.2.0 → aspyx-1.3.0}/tests/test_di.py +76 -9
- aspyx-1.2.0/src/aspyx/di/__init__.py +0 -34
- {aspyx-1.2.0 → aspyx-1.3.0}/LICENSE +0 -0
- {aspyx-1.2.0 → aspyx-1.3.0}/setup.cfg +0 -0
- {aspyx-1.2.0 → aspyx-1.3.0}/src/aspyx/di/aop/__init__.py +0 -0
- {aspyx-1.2.0 → aspyx-1.3.0}/src/aspyx/di/configuration/__init__.py +0 -0
- {aspyx-1.2.0 → aspyx-1.3.0}/src/aspyx/di/configuration/configuration.py +0 -0
- {aspyx-1.2.0 → aspyx-1.3.0}/src/aspyx/di/util/__init__.py +0 -0
- {aspyx-1.2.0 → aspyx-1.3.0}/src/aspyx/di/util/stringbuilder.py +0 -0
- {aspyx-1.2.0 → aspyx-1.3.0}/src/aspyx/reflection/__init__.py +0 -0
- {aspyx-1.2.0 → aspyx-1.3.0}/src/aspyx/reflection/proxy.py +0 -0
- {aspyx-1.2.0 → aspyx-1.3.0}/src/aspyx.egg-info/dependency_links.txt +0 -0
- {aspyx-1.2.0 → aspyx-1.3.0}/src/aspyx.egg-info/top_level.txt +0 -0
- {aspyx-1.2.0 → aspyx-1.3.0}/tests/test_configuration.py +0 -0
- {aspyx-1.2.0 → aspyx-1.3.0}/tests/test_di_cycle.py +0 -0
- {aspyx-1.2.0 → aspyx-1.3.0}/tests/test_proxy.py +0 -0
- {aspyx-1.2.0 → aspyx-1.3.0}/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.3.0
|
|
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
|
|
@@ -37,15 +37,18 @@ Dynamic: license-file
|
|
|
37
37
|

|
|
38
38
|

|
|
39
39
|

|
|
40
|
+
[](https://pypi.org/project/aspyx/)
|
|
40
41
|
|
|
41
42
|
## Table of Contents
|
|
42
43
|
|
|
43
|
-
- [
|
|
44
|
+
- [Motivation](#motivation)
|
|
45
|
+
- [Introduction](#introduction)
|
|
44
46
|
- [Installation](#installation)
|
|
45
47
|
- [Registration](#registration)
|
|
46
48
|
- [Class](#class)
|
|
47
49
|
- [Class Factory](#class-factory)
|
|
48
50
|
- [Method](#method)
|
|
51
|
+
- [Conditional](#conditional)
|
|
49
52
|
- [Environment](#environment)
|
|
50
53
|
- [Definition](#definition)
|
|
51
54
|
- [Retrieval](#retrieval)
|
|
@@ -59,22 +62,38 @@ Dynamic: license-file
|
|
|
59
62
|
- [Reflection](#reflection)
|
|
60
63
|
- [Version History](#version-history)
|
|
61
64
|
|
|
65
|
+
# Motivation
|
|
66
|
+
|
|
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".
|
|
70
|
+
|
|
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 :-)
|
|
72
|
+
|
|
62
73
|
# Introduction
|
|
63
74
|
|
|
64
75
|
Aspyx is a small python libary, that adds support for both dependency injection and aop.
|
|
65
76
|
|
|
66
|
-
The following features are supported
|
|
77
|
+
The following di features are supported
|
|
67
78
|
- constructor and setter injection
|
|
79
|
+
- possibility to define custom injections
|
|
68
80
|
- post processors
|
|
69
|
-
- factory classes and methods
|
|
81
|
+
- support for factory classes and methods
|
|
70
82
|
- support for eager construction
|
|
71
|
-
- support for singleton and
|
|
83
|
+
- support for scopes singleton, request and thread
|
|
72
84
|
- possibilty to add custom scopes
|
|
73
|
-
-
|
|
74
|
-
-
|
|
75
|
-
-
|
|
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 )
|
|
76
90
|
- hierarchical environments
|
|
77
91
|
|
|
92
|
+
With respect to aop:
|
|
93
|
+
- support for before, around, after and error aspects
|
|
94
|
+
- sync and async method support
|
|
95
|
+
- `synchronized` decorator that adds locking to methods
|
|
96
|
+
|
|
78
97
|
The library is thread-safe!
|
|
79
98
|
|
|
80
99
|
Let's look at a simple example
|
|
@@ -155,11 +174,11 @@ Let's look at the details
|
|
|
155
174
|
|
|
156
175
|
# Installation
|
|
157
176
|
|
|
158
|
-
|
|
177
|
+
Just install from PyPI with
|
|
159
178
|
|
|
160
|
-
|
|
179
|
+
`pip install aspyx`
|
|
161
180
|
|
|
162
|
-
|
|
181
|
+
The library is tested with all Python version >= 3.9
|
|
163
182
|
|
|
164
183
|
# Registration
|
|
165
184
|
|
|
@@ -233,6 +252,22 @@ class Foo:
|
|
|
233
252
|
|
|
234
253
|
The same arguments as in `@injectable` are possible.
|
|
235
254
|
|
|
255
|
+
## Conditional
|
|
256
|
+
|
|
257
|
+
All `@injectable` declarations can be supplemented with
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
@conditional(<condition>, ..., <condition>)
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
decorators that act as filters in the context of an environment.
|
|
264
|
+
|
|
265
|
+
Valid conditions are created by:
|
|
266
|
+
- `requires_class(clazz: Type)`
|
|
267
|
+
the injectable is valid, if the specified class is registered as well.
|
|
268
|
+
- `requires_feature(feature: str)`
|
|
269
|
+
the injectable is valid, if the environment defines the specified feature.
|
|
270
|
+
|
|
236
271
|
# Environment
|
|
237
272
|
|
|
238
273
|
## Definition
|
|
@@ -251,6 +286,26 @@ environment = Environment(SampleEnvironment)
|
|
|
251
286
|
|
|
252
287
|
The default is that all eligible classes, that are implemented in the containing module or in any submodule will be managed.
|
|
253
288
|
|
|
289
|
+
By adding the parameter `features: list[str]`, it is possible to filter injectables by evaluating the corresponding `@conditional` decorators.
|
|
290
|
+
|
|
291
|
+
**Example**:
|
|
292
|
+
```python
|
|
293
|
+
|
|
294
|
+
@injectable()
|
|
295
|
+
@conditional(requires_feature("dev"))
|
|
296
|
+
class DevOnly:
|
|
297
|
+
def __init__(self):
|
|
298
|
+
pass
|
|
299
|
+
|
|
300
|
+
@environment()
|
|
301
|
+
class SampleEnvironmen()):
|
|
302
|
+
def __init__(self):
|
|
303
|
+
pass
|
|
304
|
+
|
|
305
|
+
environment = Environment(SampleEnvironment, features=["dev"])
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
|
|
254
309
|
By adding an `imports: list[Type]` parameter, specifying other environment types, it will register the appropriate classes recursively.
|
|
255
310
|
|
|
256
311
|
**Example**:
|
|
@@ -428,18 +483,35 @@ Different aspects - with the appropriate decorator - are possible:
|
|
|
428
483
|
- `before`
|
|
429
484
|
methods that will be executed _prior_ to the original method
|
|
430
485
|
- `around`
|
|
431
|
-
methods that will be executed _around_ to the original method giving it the possibility add side effects or even change the parameters.
|
|
486
|
+
methods that will be executed _around_ to the original method giving it the possibility to add side effects or even change the parameters.
|
|
432
487
|
- `after`
|
|
433
|
-
|
|
488
|
+
methods that will be executed _after_ to the original method
|
|
434
489
|
- `error`
|
|
435
|
-
methods that will be executed in case of a caught exception
|
|
490
|
+
methods that will be executed in case of a caught exception
|
|
491
|
+
|
|
492
|
+
All methods are expected to have single `Invocation` parameter, that stores
|
|
436
493
|
|
|
437
|
-
|
|
494
|
+
- `func` the target function
|
|
495
|
+
- `args` the suppliued args
|
|
496
|
+
- `kwargs` the keywords args
|
|
497
|
+
- `result` the result ( initially `None`)
|
|
498
|
+
- `exception` a possible caught excpetion ( initially `None`)
|
|
438
499
|
|
|
439
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.
|
|
440
501
|
If the `proceed` is called with parameters, they will replace the original parameters!
|
|
441
502
|
|
|
442
|
-
|
|
503
|
+
**Example**: Parameter modifications
|
|
504
|
+
|
|
505
|
+
```python
|
|
506
|
+
@around(methods().named("say"))
|
|
507
|
+
def call_around(self, invocation: Invocation):
|
|
508
|
+
args = [invocation.args[0],invocation.args[1] + "!"] # 0 is self!
|
|
509
|
+
|
|
510
|
+
return invocation.proceed(*args)
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
The argument list to the corresponding decorators control which methods are targeted by the advice.
|
|
514
|
+
|
|
443
515
|
A fluent interface is used describe the mapping.
|
|
444
516
|
The parameters restrict either methods or classes and are constructed by a call to either `methods()` or `classes()`.
|
|
445
517
|
|
|
@@ -448,6 +520,8 @@ Both add the fluent methods:
|
|
|
448
520
|
defines the matching classes
|
|
449
521
|
- `named(name: str)`
|
|
450
522
|
defines method or class names
|
|
523
|
+
- `that_are_async()`
|
|
524
|
+
defines async methods
|
|
451
525
|
- `matches(re: str)`
|
|
452
526
|
defines regular expressions for methods or classes
|
|
453
527
|
- `decorated_with(type: Type)`
|
|
@@ -468,6 +542,10 @@ class TransactionAdvice:
|
|
|
468
542
|
...
|
|
469
543
|
```
|
|
470
544
|
|
|
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!
|
|
546
|
+
|
|
547
|
+
A handy decorator `@synchronized` is implemented that automatically synchronizes methods with a `RLock` associated with the instance.
|
|
548
|
+
|
|
471
549
|
# Configuration
|
|
472
550
|
|
|
473
551
|
It is possible to inject configuration values, by decorating methods with `@inject-value(<name>)` given a configuration key.
|
|
@@ -618,6 +696,8 @@ def transactional(scope):
|
|
|
618
696
|
|
|
619
697
|
- added `YamlConfigurationSource`
|
|
620
698
|
|
|
699
|
+
**1.2.1**
|
|
700
|
+
|
|
621
701
|
|
|
622
702
|
|
|
623
703
|
|
|
@@ -5,15 +5,18 @@
|
|
|
5
5
|

|
|
6
6
|

|
|
7
7
|

|
|
8
|
+
[](https://pypi.org/project/aspyx/)
|
|
8
9
|
|
|
9
10
|
## Table of Contents
|
|
10
11
|
|
|
11
|
-
- [
|
|
12
|
+
- [Motivation](#motivation)
|
|
13
|
+
- [Introduction](#introduction)
|
|
12
14
|
- [Installation](#installation)
|
|
13
15
|
- [Registration](#registration)
|
|
14
16
|
- [Class](#class)
|
|
15
17
|
- [Class Factory](#class-factory)
|
|
16
18
|
- [Method](#method)
|
|
19
|
+
- [Conditional](#conditional)
|
|
17
20
|
- [Environment](#environment)
|
|
18
21
|
- [Definition](#definition)
|
|
19
22
|
- [Retrieval](#retrieval)
|
|
@@ -27,22 +30,38 @@
|
|
|
27
30
|
- [Reflection](#reflection)
|
|
28
31
|
- [Version History](#version-history)
|
|
29
32
|
|
|
33
|
+
# Motivation
|
|
34
|
+
|
|
35
|
+
While working on some AI related topics in Python, i required a simple DI framework.
|
|
36
|
+
Looking at the existing solutions - there are quite a number of - i was not quite happy. Either the solutions
|
|
37
|
+
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".
|
|
38
|
+
|
|
39
|
+
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 :-)
|
|
40
|
+
|
|
30
41
|
# Introduction
|
|
31
42
|
|
|
32
43
|
Aspyx is a small python libary, that adds support for both dependency injection and aop.
|
|
33
44
|
|
|
34
|
-
The following features are supported
|
|
45
|
+
The following di features are supported
|
|
35
46
|
- constructor and setter injection
|
|
47
|
+
- possibility to define custom injections
|
|
36
48
|
- post processors
|
|
37
|
-
- factory classes and methods
|
|
49
|
+
- support for factory classes and methods
|
|
38
50
|
- support for eager construction
|
|
39
|
-
- support for singleton and
|
|
51
|
+
- support for scopes singleton, request and thread
|
|
40
52
|
- possibilty to add custom scopes
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
53
|
+
- conditional registration of classes and factories
|
|
54
|
+
- lifecycle events methods `on_init`, `on_destroy`
|
|
55
|
+
- bundling of injectable objects according to their module location including recursive imports and inheritance
|
|
56
|
+
- instatiation of - possibly multiple - container instances - so called environments - that manage the lifecylce of related objects
|
|
57
|
+
- filtering of classes by associating "features" sets to environment ( similar to spring profiles )
|
|
44
58
|
- hierarchical environments
|
|
45
59
|
|
|
60
|
+
With respect to aop:
|
|
61
|
+
- support for before, around, after and error aspects
|
|
62
|
+
- sync and async method support
|
|
63
|
+
- `synchronized` decorator that adds locking to methods
|
|
64
|
+
|
|
46
65
|
The library is thread-safe!
|
|
47
66
|
|
|
48
67
|
Let's look at a simple example
|
|
@@ -123,11 +142,11 @@ Let's look at the details
|
|
|
123
142
|
|
|
124
143
|
# Installation
|
|
125
144
|
|
|
126
|
-
|
|
145
|
+
Just install from PyPI with
|
|
127
146
|
|
|
128
|
-
|
|
147
|
+
`pip install aspyx`
|
|
129
148
|
|
|
130
|
-
|
|
149
|
+
The library is tested with all Python version >= 3.9
|
|
131
150
|
|
|
132
151
|
# Registration
|
|
133
152
|
|
|
@@ -201,6 +220,22 @@ class Foo:
|
|
|
201
220
|
|
|
202
221
|
The same arguments as in `@injectable` are possible.
|
|
203
222
|
|
|
223
|
+
## Conditional
|
|
224
|
+
|
|
225
|
+
All `@injectable` declarations can be supplemented with
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
@conditional(<condition>, ..., <condition>)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
decorators that act as filters in the context of an environment.
|
|
232
|
+
|
|
233
|
+
Valid conditions are created by:
|
|
234
|
+
- `requires_class(clazz: Type)`
|
|
235
|
+
the injectable is valid, if the specified class is registered as well.
|
|
236
|
+
- `requires_feature(feature: str)`
|
|
237
|
+
the injectable is valid, if the environment defines the specified feature.
|
|
238
|
+
|
|
204
239
|
# Environment
|
|
205
240
|
|
|
206
241
|
## Definition
|
|
@@ -219,6 +254,26 @@ environment = Environment(SampleEnvironment)
|
|
|
219
254
|
|
|
220
255
|
The default is that all eligible classes, that are implemented in the containing module or in any submodule will be managed.
|
|
221
256
|
|
|
257
|
+
By adding the parameter `features: list[str]`, it is possible to filter injectables by evaluating the corresponding `@conditional` decorators.
|
|
258
|
+
|
|
259
|
+
**Example**:
|
|
260
|
+
```python
|
|
261
|
+
|
|
262
|
+
@injectable()
|
|
263
|
+
@conditional(requires_feature("dev"))
|
|
264
|
+
class DevOnly:
|
|
265
|
+
def __init__(self):
|
|
266
|
+
pass
|
|
267
|
+
|
|
268
|
+
@environment()
|
|
269
|
+
class SampleEnvironmen()):
|
|
270
|
+
def __init__(self):
|
|
271
|
+
pass
|
|
272
|
+
|
|
273
|
+
environment = Environment(SampleEnvironment, features=["dev"])
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
|
|
222
277
|
By adding an `imports: list[Type]` parameter, specifying other environment types, it will register the appropriate classes recursively.
|
|
223
278
|
|
|
224
279
|
**Example**:
|
|
@@ -396,18 +451,35 @@ Different aspects - with the appropriate decorator - are possible:
|
|
|
396
451
|
- `before`
|
|
397
452
|
methods that will be executed _prior_ to the original method
|
|
398
453
|
- `around`
|
|
399
|
-
methods that will be executed _around_ to the original method giving it the possibility add side effects or even change the parameters.
|
|
454
|
+
methods that will be executed _around_ to the original method giving it the possibility to add side effects or even change the parameters.
|
|
400
455
|
- `after`
|
|
401
|
-
|
|
456
|
+
methods that will be executed _after_ to the original method
|
|
402
457
|
- `error`
|
|
403
|
-
methods that will be executed in case of a caught exception
|
|
458
|
+
methods that will be executed in case of a caught exception
|
|
459
|
+
|
|
460
|
+
All methods are expected to have single `Invocation` parameter, that stores
|
|
404
461
|
|
|
405
|
-
|
|
462
|
+
- `func` the target function
|
|
463
|
+
- `args` the suppliued args
|
|
464
|
+
- `kwargs` the keywords args
|
|
465
|
+
- `result` the result ( initially `None`)
|
|
466
|
+
- `exception` a possible caught excpetion ( initially `None`)
|
|
406
467
|
|
|
407
468
|
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.
|
|
408
469
|
If the `proceed` is called with parameters, they will replace the original parameters!
|
|
409
470
|
|
|
410
|
-
|
|
471
|
+
**Example**: Parameter modifications
|
|
472
|
+
|
|
473
|
+
```python
|
|
474
|
+
@around(methods().named("say"))
|
|
475
|
+
def call_around(self, invocation: Invocation):
|
|
476
|
+
args = [invocation.args[0],invocation.args[1] + "!"] # 0 is self!
|
|
477
|
+
|
|
478
|
+
return invocation.proceed(*args)
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
The argument list to the corresponding decorators control which methods are targeted by the advice.
|
|
482
|
+
|
|
411
483
|
A fluent interface is used describe the mapping.
|
|
412
484
|
The parameters restrict either methods or classes and are constructed by a call to either `methods()` or `classes()`.
|
|
413
485
|
|
|
@@ -416,6 +488,8 @@ Both add the fluent methods:
|
|
|
416
488
|
defines the matching classes
|
|
417
489
|
- `named(name: str)`
|
|
418
490
|
defines method or class names
|
|
491
|
+
- `that_are_async()`
|
|
492
|
+
defines async methods
|
|
419
493
|
- `matches(re: str)`
|
|
420
494
|
defines regular expressions for methods or classes
|
|
421
495
|
- `decorated_with(type: Type)`
|
|
@@ -436,6 +510,10 @@ class TransactionAdvice:
|
|
|
436
510
|
...
|
|
437
511
|
```
|
|
438
512
|
|
|
513
|
+
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!
|
|
514
|
+
|
|
515
|
+
A handy decorator `@synchronized` is implemented that automatically synchronizes methods with a `RLock` associated with the instance.
|
|
516
|
+
|
|
439
517
|
# Configuration
|
|
440
518
|
|
|
441
519
|
It is possible to inject configuration values, by decorating methods with `@inject-value(<name>)` given a configuration key.
|
|
@@ -586,6 +664,8 @@ def transactional(scope):
|
|
|
586
664
|
|
|
587
665
|
- added `YamlConfigurationSource`
|
|
588
666
|
|
|
667
|
+
**1.2.1**
|
|
668
|
+
|
|
589
669
|
|
|
590
670
|
|
|
591
671
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides dependency injection and aop capabilities for Python applications.
|
|
3
|
+
"""
|
|
4
|
+
from .di import conditional, requires_class, requires_feature, DIException, AbstractCallableProcessor, LifecycleCallable, Lifecycle, Providers, Environment, ClassInstanceProvider, injectable, factory, environment, inject, order, create, on_init, on_running, on_destroy, inject_environment, Factory, PostProcessor
|
|
5
|
+
|
|
6
|
+
# import something from the subpackages, so that the decorators are executed
|
|
7
|
+
|
|
8
|
+
from .configuration import ConfigurationManager
|
|
9
|
+
from .aop import before
|
|
10
|
+
from .threading import SynchronizeAdvice
|
|
11
|
+
|
|
12
|
+
imports = [ConfigurationManager, before, SynchronizeAdvice]
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"ClassInstanceProvider",
|
|
16
|
+
"Providers",
|
|
17
|
+
"Environment",
|
|
18
|
+
"injectable",
|
|
19
|
+
"factory",
|
|
20
|
+
"environment",
|
|
21
|
+
"inject",
|
|
22
|
+
"create",
|
|
23
|
+
"order",
|
|
24
|
+
|
|
25
|
+
"on_init",
|
|
26
|
+
"on_running",
|
|
27
|
+
"on_destroy",
|
|
28
|
+
"inject_environment",
|
|
29
|
+
"Factory",
|
|
30
|
+
"PostProcessor",
|
|
31
|
+
"AbstractCallableProcessor",
|
|
32
|
+
"LifecycleCallable",
|
|
33
|
+
"DIException",
|
|
34
|
+
"Lifecycle",
|
|
35
|
+
"conditional",
|
|
36
|
+
"requires_class",
|
|
37
|
+
"requires_feature"
|
|
38
|
+
]
|
|
@@ -50,7 +50,7 @@ class AspectTarget(ABC):
|
|
|
50
50
|
__slots__ = [
|
|
51
51
|
"_function",
|
|
52
52
|
"_type",
|
|
53
|
-
|
|
53
|
+
"_async",
|
|
54
54
|
"_clazz",
|
|
55
55
|
"_instance",
|
|
56
56
|
"names",
|
|
@@ -65,6 +65,7 @@ class AspectTarget(ABC):
|
|
|
65
65
|
def __init__(self):
|
|
66
66
|
self._clazz = None
|
|
67
67
|
self._instance = None
|
|
68
|
+
self._async = False
|
|
68
69
|
self._function = None
|
|
69
70
|
self._type = None
|
|
70
71
|
|
|
@@ -108,6 +109,10 @@ class AspectTarget(ABC):
|
|
|
108
109
|
|
|
109
110
|
return self
|
|
110
111
|
|
|
112
|
+
def that_are_async(self):
|
|
113
|
+
self._async = True
|
|
114
|
+
return self
|
|
115
|
+
|
|
111
116
|
def of_type(self, type: Type):
|
|
112
117
|
self.types.append(type)
|
|
113
118
|
return self
|
|
@@ -178,6 +183,14 @@ class MethodAspectTarget(AspectTarget):
|
|
|
178
183
|
|
|
179
184
|
method_descriptor = descriptor.get_method(func.__name__)
|
|
180
185
|
|
|
186
|
+
# async
|
|
187
|
+
|
|
188
|
+
name = method_descriptor.method.__name__
|
|
189
|
+
is_async = method_descriptor.is_async()
|
|
190
|
+
|
|
191
|
+
if self._async is not method_descriptor.is_async():
|
|
192
|
+
return False
|
|
193
|
+
|
|
181
194
|
# type
|
|
182
195
|
|
|
183
196
|
if len(self.types) > 0:
|
|
@@ -234,6 +247,9 @@ class JoinPoint:
|
|
|
234
247
|
def call(self, invocation: 'Invocation'):
|
|
235
248
|
pass
|
|
236
249
|
|
|
250
|
+
async def call_async(self, invocation: 'Invocation'):
|
|
251
|
+
pass
|
|
252
|
+
|
|
237
253
|
class FunctionJoinPoint(JoinPoint):
|
|
238
254
|
__slots__ = [
|
|
239
255
|
"instance",
|
|
@@ -251,6 +267,11 @@ class FunctionJoinPoint(JoinPoint):
|
|
|
251
267
|
|
|
252
268
|
return self.func(self.instance, invocation)
|
|
253
269
|
|
|
270
|
+
async def call_async(self, invocation: 'Invocation'):
|
|
271
|
+
invocation.current_join_point = self
|
|
272
|
+
|
|
273
|
+
return await self.func(self.instance, invocation)
|
|
274
|
+
|
|
254
275
|
class MethodJoinPoint(FunctionJoinPoint):
|
|
255
276
|
__slots__ = []
|
|
256
277
|
|
|
@@ -262,6 +283,11 @@ class MethodJoinPoint(FunctionJoinPoint):
|
|
|
262
283
|
|
|
263
284
|
return self.func(*invocation.args, **invocation.kwargs)
|
|
264
285
|
|
|
286
|
+
async def call_async(self, invocation: 'Invocation'):
|
|
287
|
+
invocation.current_join_point = self
|
|
288
|
+
|
|
289
|
+
return await self.func(*invocation.args, **invocation.kwargs)
|
|
290
|
+
|
|
265
291
|
@dataclass
|
|
266
292
|
class JoinPoints:
|
|
267
293
|
before: list[JoinPoint]
|
|
@@ -328,6 +354,37 @@ class Invocation:
|
|
|
328
354
|
|
|
329
355
|
return self.result
|
|
330
356
|
|
|
357
|
+
async def call_async(self, *args, **kwargs):
|
|
358
|
+
# remember args
|
|
359
|
+
|
|
360
|
+
self.args = args
|
|
361
|
+
self.kwargs = kwargs
|
|
362
|
+
|
|
363
|
+
# run all before
|
|
364
|
+
|
|
365
|
+
for join_point in self.join_points.before:
|
|
366
|
+
join_point.call(self)
|
|
367
|
+
|
|
368
|
+
# run around's with the method being the last aspect!
|
|
369
|
+
|
|
370
|
+
try:
|
|
371
|
+
self.result = await self.join_points.around[0].call_async(self) # will follow the proceed chain
|
|
372
|
+
|
|
373
|
+
except Exception as e:
|
|
374
|
+
self.exception = e
|
|
375
|
+
for join_point in self.join_points.error:
|
|
376
|
+
join_point.call(self)
|
|
377
|
+
|
|
378
|
+
# run all before
|
|
379
|
+
|
|
380
|
+
for join_point in self.join_points.after:
|
|
381
|
+
join_point.call(self)
|
|
382
|
+
|
|
383
|
+
if self.exception is not None:
|
|
384
|
+
raise self.exception # rethrow the error
|
|
385
|
+
|
|
386
|
+
return self.result
|
|
387
|
+
|
|
331
388
|
def proceed(self, *args, **kwargs):
|
|
332
389
|
"""
|
|
333
390
|
Proceed to the next join point in the around chain up to the original method.
|
|
@@ -340,6 +397,18 @@ class Invocation:
|
|
|
340
397
|
|
|
341
398
|
return self.current_join_point.next.call(self)
|
|
342
399
|
|
|
400
|
+
async def proceed_async(self, *args, **kwargs):
|
|
401
|
+
"""
|
|
402
|
+
Proceed to the next join point in the around chain up to the original method.
|
|
403
|
+
"""
|
|
404
|
+
if len(args) > 0 or len(kwargs) > 0: # as soon as we have args, we replace the current ones
|
|
405
|
+
self.args = args
|
|
406
|
+
self.kwargs = kwargs
|
|
407
|
+
|
|
408
|
+
# next one please...
|
|
409
|
+
|
|
410
|
+
return await self.current_join_point.next.call_async(self)
|
|
411
|
+
|
|
343
412
|
@injectable()
|
|
344
413
|
class Advice:
|
|
345
414
|
# static data
|
|
@@ -381,13 +450,14 @@ class Advice:
|
|
|
381
450
|
|
|
382
451
|
if result is None:
|
|
383
452
|
result = {}
|
|
453
|
+
self.cache[clazz] = result
|
|
384
454
|
|
|
385
455
|
for _, member in inspect.getmembers(clazz, predicate=inspect.isfunction):
|
|
386
456
|
join_points = self.compute_join_points(clazz, member, environment)
|
|
387
457
|
if join_points is not None:
|
|
388
458
|
result[member] = join_points
|
|
389
459
|
|
|
390
|
-
|
|
460
|
+
|
|
391
461
|
|
|
392
462
|
# add around methods
|
|
393
463
|
|
|
@@ -533,10 +603,22 @@ class AdviceProcessor(PostProcessor):
|
|
|
533
603
|
def process(self, instance: object, environment: Environment):
|
|
534
604
|
join_point_dict = self.advice.join_points4(instance, environment)
|
|
535
605
|
|
|
536
|
-
for member,
|
|
606
|
+
for member, join_points in join_point_dict.items():
|
|
537
607
|
Environment.logger.debug("add aspects for %s:%s", type(instance), member.__name__)
|
|
538
608
|
|
|
539
609
|
def wrap(jp):
|
|
540
|
-
|
|
610
|
+
def sync_wrapper(*args, **kwargs):
|
|
611
|
+
return Invocation(member, jp).call(*args, **kwargs)
|
|
612
|
+
|
|
613
|
+
return sync_wrapper
|
|
614
|
+
|
|
615
|
+
def wrap_async(jp):
|
|
616
|
+
async def async_wrapper(*args, **kwargs):
|
|
617
|
+
return await Invocation(member, jp).call_async(*args, **kwargs)
|
|
618
|
+
|
|
619
|
+
return async_wrapper
|
|
541
620
|
|
|
542
|
-
|
|
621
|
+
if inspect.iscoroutinefunction(member):
|
|
622
|
+
setattr(instance, member.__name__, types.MethodType(wrap_async(join_points), instance))
|
|
623
|
+
else:
|
|
624
|
+
setattr(instance, member.__name__, types.MethodType(wrap(join_points), instance))
|