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.

Files changed (31) hide show
  1. {aspyx-1.2.0/src/aspyx.egg-info → aspyx-1.3.0}/PKG-INFO +96 -16
  2. {aspyx-1.2.0 → aspyx-1.3.0}/README.md +95 -15
  3. {aspyx-1.2.0 → aspyx-1.3.0}/pyproject.toml +1 -1
  4. aspyx-1.3.0/src/aspyx/di/__init__.py +38 -0
  5. {aspyx-1.2.0 → aspyx-1.3.0}/src/aspyx/di/aop/aop.py +87 -5
  6. {aspyx-1.2.0 → aspyx-1.3.0}/src/aspyx/di/configuration/env_configuration_source.py +1 -1
  7. {aspyx-1.2.0 → aspyx-1.3.0}/src/aspyx/di/configuration/yaml_configuration_source.py +1 -1
  8. {aspyx-1.2.0 → aspyx-1.3.0}/src/aspyx/di/di.py +290 -200
  9. aspyx-1.3.0/src/aspyx/di/threading/__init__.py +11 -0
  10. aspyx-1.3.0/src/aspyx/di/threading/synchronized.py +46 -0
  11. {aspyx-1.2.0 → aspyx-1.3.0}/src/aspyx/reflection/reflection.py +4 -1
  12. {aspyx-1.2.0 → aspyx-1.3.0/src/aspyx.egg-info}/PKG-INFO +96 -16
  13. {aspyx-1.2.0 → aspyx-1.3.0}/src/aspyx.egg-info/SOURCES.txt +2 -0
  14. {aspyx-1.2.0 → aspyx-1.3.0}/tests/test_aop.py +49 -17
  15. {aspyx-1.2.0 → aspyx-1.3.0}/tests/test_di.py +76 -9
  16. aspyx-1.2.0/src/aspyx/di/__init__.py +0 -34
  17. {aspyx-1.2.0 → aspyx-1.3.0}/LICENSE +0 -0
  18. {aspyx-1.2.0 → aspyx-1.3.0}/setup.cfg +0 -0
  19. {aspyx-1.2.0 → aspyx-1.3.0}/src/aspyx/di/aop/__init__.py +0 -0
  20. {aspyx-1.2.0 → aspyx-1.3.0}/src/aspyx/di/configuration/__init__.py +0 -0
  21. {aspyx-1.2.0 → aspyx-1.3.0}/src/aspyx/di/configuration/configuration.py +0 -0
  22. {aspyx-1.2.0 → aspyx-1.3.0}/src/aspyx/di/util/__init__.py +0 -0
  23. {aspyx-1.2.0 → aspyx-1.3.0}/src/aspyx/di/util/stringbuilder.py +0 -0
  24. {aspyx-1.2.0 → aspyx-1.3.0}/src/aspyx/reflection/__init__.py +0 -0
  25. {aspyx-1.2.0 → aspyx-1.3.0}/src/aspyx/reflection/proxy.py +0 -0
  26. {aspyx-1.2.0 → aspyx-1.3.0}/src/aspyx.egg-info/dependency_links.txt +0 -0
  27. {aspyx-1.2.0 → aspyx-1.3.0}/src/aspyx.egg-info/top_level.txt +0 -0
  28. {aspyx-1.2.0 → aspyx-1.3.0}/tests/test_configuration.py +0 -0
  29. {aspyx-1.2.0 → aspyx-1.3.0}/tests/test_di_cycle.py +0 -0
  30. {aspyx-1.2.0 → aspyx-1.3.0}/tests/test_proxy.py +0 -0
  31. {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.2.0
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
  ![Python Versions](https://img.shields.io/badge/python-3.9%20|%203.10%20|%203.11%20|%203.12-blue)
38
38
  ![License](https://img.shields.io/github/license/coolsamson7/aspyx)
39
39
  ![coverage](https://img.shields.io/badge/coverage-94%25-brightgreen)
40
+ [![PyPI](https://img.shields.io/pypi/v/aspyx)](https://pypi.org/project/aspyx/)
40
41
 
41
42
  ## Table of Contents
42
43
 
43
- - [Introduction](#aspyx)
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 request scopes
83
+ - support for scopes singleton, request and thread
72
84
  - possibilty to add custom scopes
73
- - lifecycle events methods
74
- - bundling of injectable object sets by environment classes including recursive imports and inheritance
75
- - container instances that relate to environment classes and manage the lifecylce of related objects
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
- `pip install aspyx`
177
+ Just install from PyPI with
159
178
 
160
- The library is tested with all Python version > 3.9
179
+ `pip install aspyx`
161
180
 
162
- Ready to go...
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
- methods that will be executed _after_ to the original method
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, which can be retrieved by `invocation.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
- All methods are expected to hava single `Invocation` parameter, that stores, the function, args and kwargs, the return value and possible exceptions.
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
- The argument list to the corresponding decorators control, how aspects are associated with which methods.
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
  ![Python Versions](https://img.shields.io/badge/python-3.9%20|%203.10%20|%203.11%20|%203.12-blue)
6
6
  ![License](https://img.shields.io/github/license/coolsamson7/aspyx)
7
7
  ![coverage](https://img.shields.io/badge/coverage-94%25-brightgreen)
8
+ [![PyPI](https://img.shields.io/pypi/v/aspyx)](https://pypi.org/project/aspyx/)
8
9
 
9
10
  ## Table of Contents
10
11
 
11
- - [Introduction](#aspyx)
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 request scopes
51
+ - support for scopes singleton, request and thread
40
52
  - possibilty to add custom scopes
41
- - lifecycle events methods
42
- - bundling of injectable object sets by environment classes including recursive imports and inheritance
43
- - container instances that relate to environment classes and manage the lifecylce of related objects
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
- `pip install aspyx`
145
+ Just install from PyPI with
127
146
 
128
- The library is tested with all Python version > 3.9
147
+ `pip install aspyx`
129
148
 
130
- Ready to go...
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
- methods that will be executed _after_ to the original method
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, which can be retrieved by `invocation.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
- All methods are expected to hava single `Invocation` parameter, that stores, the function, args and kwargs, the return value and possible exceptions.
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
- The argument list to the corresponding decorators control, how aspects are associated with which methods.
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
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "aspyx"
7
- version = "1.2.0"
7
+ version = "1.3.0"
8
8
  description = "A DI and AOP library for Python"
9
9
  authors = [{ name = "Andreas Ernst", email = "andreas.ernst7@gmail.com" }]
10
10
  readme = "README.md"
@@ -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
- self.cache[clazz] = result
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, joinPoints in join_point_dict.items():
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
- return lambda *args, **kwargs: Invocation(member, jp).call(*args, **kwargs)
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
- setattr(instance, member.__name__, types.MethodType(wrap(joinPoints), instance))
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))
@@ -52,4 +52,4 @@ class EnvConfigurationSource(ConfigurationSource):
52
52
  else:
53
53
  exploded[key] = value
54
54
 
55
- return exploded
55
+ return exploded
@@ -23,4 +23,4 @@ class YamlConfigurationSource(ConfigurationSource):
23
23
 
24
24
  def load(self) -> dict:
25
25
  with open(self.file, "r") as file:
26
- return yaml.safe_load(file)
26
+ return yaml.safe_load(file)