aspyx 1.4.0__py3-none-any.whl → 1.4.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of aspyx might be problematic. Click here for more details.
- aspyx/di/__init__.py +2 -2
- aspyx/di/aop/__init__.py +16 -2
- aspyx/di/aop/aop.py +64 -22
- aspyx/di/configuration/__init__.py +3 -3
- aspyx/di/configuration/configuration.py +7 -5
- aspyx/di/di.py +100 -23
- aspyx/exception/__init__.py +1 -1
- aspyx/exception/exception_manager.py +34 -17
- aspyx/reflection/proxy.py +2 -4
- aspyx/reflection/reflection.py +55 -15
- aspyx/threading/__init__.py +1 -1
- aspyx/threading/thread_local.py +6 -2
- aspyx/util/stringbuilder.py +6 -2
- {aspyx-1.4.0.dist-info → aspyx-1.4.1.dist-info}/METADATA +67 -47
- aspyx-1.4.1.dist-info/RECORD +25 -0
- aspyx-1.4.0.dist-info/RECORD +0 -25
- {aspyx-1.4.0.dist-info → aspyx-1.4.1.dist-info}/WHEEL +0 -0
- {aspyx-1.4.0.dist-info → aspyx-1.4.1.dist-info}/licenses/LICENSE +0 -0
- {aspyx-1.4.0.dist-info → aspyx-1.4.1.dist-info}/top_level.txt +0 -0
aspyx/di/__init__.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
This module provides dependency injection and aop capabilities for Python applications.
|
|
3
3
|
"""
|
|
4
|
-
from .di import conditional, requires_class, requires_feature, DIException, AbstractCallableProcessor, LifecycleCallable, Lifecycle, Providers, Environment, ClassInstanceProvider, injectable, factory,
|
|
4
|
+
from .di import conditional, requires_class, requires_feature, DIException, AbstractCallableProcessor, LifecycleCallable, Lifecycle, Providers, Environment, ClassInstanceProvider, injectable, factory, module, inject, order, create, on_init, on_running, on_destroy, inject_environment, Factory, PostProcessor
|
|
5
5
|
|
|
6
6
|
# import something from the subpackages, so that the decorators are executed
|
|
7
7
|
|
|
@@ -17,7 +17,7 @@ __all__ = [
|
|
|
17
17
|
"Environment",
|
|
18
18
|
"injectable",
|
|
19
19
|
"factory",
|
|
20
|
-
"
|
|
20
|
+
"module",
|
|
21
21
|
"inject",
|
|
22
22
|
"create",
|
|
23
23
|
"order",
|
aspyx/di/aop/__init__.py
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
"""
|
|
2
|
-
AOP module
|
|
2
|
+
The AOP module gives you the possibility to define aspects that will participate in method execution flows.
|
|
3
|
+
|
|
4
|
+
**Example**: all method executions of methods named "foo" will include a `before` aspect, that will be executed before the original method
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
@advice
|
|
8
|
+
class Advice:
|
|
9
|
+
@before(methods().named("foo"))
|
|
10
|
+
def before_call(self, invocation: Invocation):
|
|
11
|
+
...
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Note, that this requires that both the advice and the targeted methods need to be managed by an environment.
|
|
3
16
|
"""
|
|
4
|
-
from .aop import before, after, classes, around, error, advice, methods, Invocation
|
|
17
|
+
from .aop import before, after, classes, around, error, advice, methods, Invocation, AspectTarget
|
|
5
18
|
__all__ = [
|
|
6
19
|
"before",
|
|
7
20
|
"after",
|
|
@@ -11,4 +24,5 @@ __all__ = [
|
|
|
11
24
|
"classes",
|
|
12
25
|
"methods",
|
|
13
26
|
"Invocation",
|
|
27
|
+
"AspectTarget"
|
|
14
28
|
]
|
aspyx/di/aop/aop.py
CHANGED
|
@@ -25,6 +25,7 @@ class AspectType(Enum):
|
|
|
25
25
|
AspectType defines the types of aspect-oriented advice that can be applied to methods.
|
|
26
26
|
|
|
27
27
|
The available types are:
|
|
28
|
+
|
|
28
29
|
- BEFORE: Advice to be executed before the method invocation.
|
|
29
30
|
- AROUND: Advice that intercepts the method invocation.
|
|
30
31
|
- AFTER: Advice to be executed after the method invocation, regardless of its outcome.
|
|
@@ -98,7 +99,7 @@ class AspectTarget(ABC):
|
|
|
98
99
|
|
|
99
100
|
# fluent
|
|
100
101
|
|
|
101
|
-
def function(self, func):
|
|
102
|
+
def function(self, func) -> AspectTarget:
|
|
102
103
|
self._function = func
|
|
103
104
|
return self
|
|
104
105
|
|
|
@@ -107,39 +108,65 @@ class AspectTarget(ABC):
|
|
|
107
108
|
|
|
108
109
|
return self
|
|
109
110
|
|
|
110
|
-
def that_are_async(self):
|
|
111
|
+
def that_are_async(self) -> AspectTarget:
|
|
111
112
|
"""
|
|
112
113
|
matches methods that are async
|
|
113
|
-
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
AspectTarget: self
|
|
114
117
|
"""
|
|
115
118
|
self._async = True
|
|
116
119
|
return self
|
|
117
120
|
|
|
118
|
-
def of_type(self, type: Type):
|
|
121
|
+
def of_type(self, type: Type) -> AspectTarget:
|
|
119
122
|
"""
|
|
120
123
|
matches methods belonging to a class or classes that are subclasses of the specified type
|
|
121
|
-
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
type (Type): the type to match against
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
AspectTarget: self
|
|
122
130
|
"""
|
|
123
131
|
self.types.append(type)
|
|
124
132
|
return self
|
|
125
133
|
|
|
126
|
-
def decorated_with(self, decorator):
|
|
134
|
+
def decorated_with(self, decorator: Callable) -> AspectTarget:
|
|
127
135
|
"""
|
|
128
136
|
matches methods or classes that are decorated with the specified decorator
|
|
129
|
-
|
|
130
|
-
:
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
decorator (Callable): the decorator callable
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
AspectTarget: self
|
|
131
143
|
"""
|
|
132
144
|
self.decorators.append(decorator)
|
|
133
145
|
return self
|
|
134
146
|
|
|
135
|
-
def matches(self, pattern: str):
|
|
147
|
+
def matches(self, pattern: str) -> AspectTarget:
|
|
136
148
|
"""
|
|
137
149
|
Matches the target against a pattern.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
pattern (str): the pattern
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
AspectTarget: self
|
|
138
156
|
"""
|
|
139
157
|
self.patterns.append(re.compile(pattern))
|
|
140
158
|
return self
|
|
141
159
|
|
|
142
|
-
def named(self, name: str):
|
|
160
|
+
def named(self, name: str) -> AspectTarget:
|
|
161
|
+
"""
|
|
162
|
+
Matches the target against a name.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
name (str): the name
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
AspectTarget: self
|
|
169
|
+
"""
|
|
143
170
|
self.names.append(name)
|
|
144
171
|
return self
|
|
145
172
|
|
|
@@ -234,15 +261,21 @@ class MethodAspectTarget(AspectTarget):
|
|
|
234
261
|
|
|
235
262
|
return True
|
|
236
263
|
|
|
237
|
-
def methods():
|
|
264
|
+
def methods() -> AspectTarget:
|
|
238
265
|
"""
|
|
239
266
|
Create a new AspectTarget instance to define method aspect targets.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
AspectTarget: the method target
|
|
240
270
|
"""
|
|
241
271
|
return MethodAspectTarget()
|
|
242
272
|
|
|
243
|
-
def classes():
|
|
273
|
+
def classes() -> AspectTarget:
|
|
244
274
|
"""
|
|
245
275
|
Create a new AspectTarget instance to define class aspect targets.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
AspectTarget: the method target
|
|
246
279
|
"""
|
|
247
280
|
return ClassAspectTarget()
|
|
248
281
|
|
|
@@ -405,7 +438,7 @@ class Invocation:
|
|
|
405
438
|
|
|
406
439
|
def proceed(self, *args, **kwargs):
|
|
407
440
|
"""
|
|
408
|
-
Proceed to the next
|
|
441
|
+
Proceed to the next aspect in the around chain up to the original method.
|
|
409
442
|
"""
|
|
410
443
|
if len(args) > 0 or len(kwargs) > 0: # as soon as we have args, we replace the current ones
|
|
411
444
|
self.args = args
|
|
@@ -417,7 +450,7 @@ class Invocation:
|
|
|
417
450
|
|
|
418
451
|
async def proceed_async(self, *args, **kwargs):
|
|
419
452
|
"""
|
|
420
|
-
Proceed to the next
|
|
453
|
+
Proceed to the next aspect in the around chain up to the original method.
|
|
421
454
|
"""
|
|
422
455
|
if len(args) > 0 or len(kwargs) > 0: # as soon as we have args, we replace the current ones
|
|
423
456
|
self.args = args
|
|
@@ -428,6 +461,9 @@ class Invocation:
|
|
|
428
461
|
return await self.current_aspect.next.call_async(self)
|
|
429
462
|
|
|
430
463
|
class Advices:
|
|
464
|
+
"""
|
|
465
|
+
Internal utility class that collects all advice s
|
|
466
|
+
"""
|
|
431
467
|
# static data
|
|
432
468
|
|
|
433
469
|
targets: list[AspectTarget] = []
|
|
@@ -443,7 +479,13 @@ class Advices:
|
|
|
443
479
|
|
|
444
480
|
@classmethod
|
|
445
481
|
def collect(cls, clazz, member, type: AspectType, environment: Environment):
|
|
446
|
-
aspects = [
|
|
482
|
+
aspects = [
|
|
483
|
+
FunctionAspect(environment.get(target._clazz), target._function, None) for target in Advices.targets
|
|
484
|
+
if target._type == type
|
|
485
|
+
and target._clazz is not clazz
|
|
486
|
+
and environment.providers.get(target._clazz) is not None
|
|
487
|
+
and target._matches(clazz, member)
|
|
488
|
+
]
|
|
447
489
|
|
|
448
490
|
# sort according to order
|
|
449
491
|
|
|
@@ -518,8 +560,8 @@ def sanity_check(clazz: Type, name: str):
|
|
|
518
560
|
|
|
519
561
|
def advice(cls):
|
|
520
562
|
"""
|
|
521
|
-
Classes decorated with
|
|
522
|
-
They can contain methods decorated with
|
|
563
|
+
Classes decorated with `@advice` are treated as advice classes.
|
|
564
|
+
They can contain methods decorated with `@before`, `@after`, `@around`, or `@error` to define aspects.
|
|
523
565
|
"""
|
|
524
566
|
Providers.register(ClassInstanceProvider(cls, True))
|
|
525
567
|
|
|
@@ -528,7 +570,7 @@ def advice(cls):
|
|
|
528
570
|
for name, member in TypeDescriptor.for_type(cls).methods.items():
|
|
529
571
|
decorator = next((decorator for decorator in member.decorators if decorator.decorator in [before, after, around, error]), None)
|
|
530
572
|
if decorator is not None:
|
|
531
|
-
target = decorator.args[0] #
|
|
573
|
+
target = decorator.args[0] # multiple targets are already merged in a single! check _register
|
|
532
574
|
target._clazz = cls
|
|
533
575
|
sanity_check(cls, name)
|
|
534
576
|
Advices.targets.append(target) #??
|
|
@@ -550,7 +592,7 @@ def _register(decorator, targets: list[AspectTarget], func, aspect_type: AspectT
|
|
|
550
592
|
|
|
551
593
|
def before(*targets: AspectTarget):
|
|
552
594
|
"""
|
|
553
|
-
Methods decorated with
|
|
595
|
+
Methods decorated with `@before` will be executed before the target method is invoked.
|
|
554
596
|
"""
|
|
555
597
|
def decorator(func):
|
|
556
598
|
_register(before, targets, func, AspectType.BEFORE)
|
|
@@ -561,7 +603,7 @@ def before(*targets: AspectTarget):
|
|
|
561
603
|
|
|
562
604
|
def error(*targets: AspectTarget):
|
|
563
605
|
"""
|
|
564
|
-
Methods decorated with
|
|
606
|
+
Methods decorated with `@error` will be executed if the target method raises an exception."""
|
|
565
607
|
def decorator(func):
|
|
566
608
|
_register(error, targets, func, AspectType.ERROR)
|
|
567
609
|
|
|
@@ -571,7 +613,7 @@ def error(*targets: AspectTarget):
|
|
|
571
613
|
|
|
572
614
|
def after(*targets: AspectTarget):
|
|
573
615
|
"""
|
|
574
|
-
Methods decorated with
|
|
616
|
+
Methods decorated with `@after` will be executed after the target method is invoked.
|
|
575
617
|
"""
|
|
576
618
|
def decorator(func):
|
|
577
619
|
_register(after, targets, func, AspectType.AFTER)
|
|
@@ -582,7 +624,7 @@ def after(*targets: AspectTarget):
|
|
|
582
624
|
|
|
583
625
|
def around(*targets: AspectTarget):
|
|
584
626
|
"""
|
|
585
|
-
Methods decorated with
|
|
627
|
+
Methods decorated with `@around` will be executed around the target method.
|
|
586
628
|
Every around method must accept a single parameter of type Invocation and needs to call proceed
|
|
587
629
|
on this parameter to proceed to the next around method.
|
|
588
630
|
"""
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
This module contains functionality to read configuration values from different sources and to retrieve or inject them.
|
|
3
3
|
"""
|
|
4
|
-
from .configuration import ConfigurationManager, ConfigurationSource,
|
|
4
|
+
from .configuration import ConfigurationManager, ConfigurationSource, inject_value
|
|
5
5
|
from .env_configuration_source import EnvConfigurationSource
|
|
6
6
|
from .yaml_configuration_source import YamlConfigurationSource
|
|
7
7
|
|
|
@@ -10,5 +10,5 @@ __all__ = [
|
|
|
10
10
|
"ConfigurationSource",
|
|
11
11
|
"EnvConfigurationSource",
|
|
12
12
|
"YamlConfigurationSource",
|
|
13
|
-
"
|
|
13
|
+
"inject_value"
|
|
14
14
|
]
|
|
@@ -66,10 +66,12 @@ class ConfigurationManager:
|
|
|
66
66
|
def get(self, path: str, type: Type[T], default : Optional[T]=None) -> T:
|
|
67
67
|
"""
|
|
68
68
|
Retrieve a configuration value by path and type, with optional coercion.
|
|
69
|
-
|
|
69
|
+
|
|
70
|
+
Args:
|
|
70
71
|
path (str): The path to the configuration value, e.g. "database.host".
|
|
71
72
|
type (Type[T]): The expected type.
|
|
72
73
|
default (Optional[T]): The default value to return if the path is not found.
|
|
74
|
+
|
|
73
75
|
Returns:
|
|
74
76
|
T: The configuration value coerced to the specified type, or the default value if not found.
|
|
75
77
|
"""
|
|
@@ -119,17 +121,17 @@ class ConfigurationSource(ABC):
|
|
|
119
121
|
|
|
120
122
|
# decorator
|
|
121
123
|
|
|
122
|
-
def
|
|
124
|
+
def inject_value(key: str, default=None):
|
|
123
125
|
"""
|
|
124
126
|
Decorator to inject a configuration value into a method.
|
|
125
127
|
|
|
126
|
-
|
|
128
|
+
Args:
|
|
127
129
|
key (str): The configuration key to inject.
|
|
128
130
|
default: The default value to use if the key is not found.
|
|
129
131
|
|
|
130
132
|
"""
|
|
131
133
|
def decorator(func):
|
|
132
|
-
Decorators.add(func,
|
|
134
|
+
Decorators.add(func, inject_value, key, default)
|
|
133
135
|
|
|
134
136
|
return func
|
|
135
137
|
|
|
@@ -139,7 +141,7 @@ def value(key: str, default=None):
|
|
|
139
141
|
@order(9)
|
|
140
142
|
class ConfigurationLifecycleCallable(LifecycleCallable):
|
|
141
143
|
def __init__(self, manager: ConfigurationManager):
|
|
142
|
-
super().__init__(
|
|
144
|
+
super().__init__(inject_value, Lifecycle.ON_INJECT)
|
|
143
145
|
|
|
144
146
|
self.manager = manager
|
|
145
147
|
|
aspyx/di/di.py
CHANGED
|
@@ -62,33 +62,76 @@ class DIRuntimeException(DIException):
|
|
|
62
62
|
|
|
63
63
|
class AbstractInstanceProvider(ABC, Generic[T]):
|
|
64
64
|
"""
|
|
65
|
-
|
|
65
|
+
An AbstractInstanceProvider is responsible to create instances.
|
|
66
66
|
"""
|
|
67
67
|
@abstractmethod
|
|
68
68
|
def get_module(self) -> str:
|
|
69
|
-
|
|
69
|
+
"""
|
|
70
|
+
return the module name of the provider
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
str: the module name of the provider
|
|
74
|
+
"""
|
|
70
75
|
|
|
71
76
|
def get_host(self) -> Type[T]:
|
|
77
|
+
"""
|
|
78
|
+
return the class which is responsible for creation ( e.g. the injectable class )
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Type[T]: the class which is responsible for creation
|
|
82
|
+
"""
|
|
72
83
|
return type(self)
|
|
73
84
|
|
|
74
85
|
@abstractmethod
|
|
75
86
|
def get_type(self) -> Type[T]:
|
|
76
|
-
|
|
87
|
+
"""
|
|
88
|
+
return the type of the created instance
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Type[T: the type]
|
|
92
|
+
"""
|
|
77
93
|
|
|
78
94
|
@abstractmethod
|
|
79
95
|
def is_eager(self) -> bool:
|
|
80
|
-
|
|
96
|
+
"""
|
|
97
|
+
return True, if the provider will eagerly construct instances
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
bool: eager flag
|
|
101
|
+
"""
|
|
81
102
|
|
|
82
103
|
@abstractmethod
|
|
83
104
|
def get_scope(self) -> str:
|
|
84
|
-
|
|
105
|
+
"""
|
|
106
|
+
return the scope name
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
str: the scope name
|
|
110
|
+
"""
|
|
111
|
+
|
|
85
112
|
|
|
86
113
|
def get_dependencies(self) -> (list[Type],int):
|
|
114
|
+
"""
|
|
115
|
+
return the types that i depend on ( for constructor or setter injection ).
|
|
116
|
+
The second tuple element is the number of parameters that a construction injection will require
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
(list[Type],int): the type array and the number of parameters
|
|
120
|
+
"""
|
|
87
121
|
return [],1
|
|
88
122
|
|
|
89
123
|
@abstractmethod
|
|
90
|
-
def create(self, environment: Environment, *args):
|
|
91
|
-
|
|
124
|
+
def create(self, environment: Environment, *args) -> T:
|
|
125
|
+
"""
|
|
126
|
+
Create a new instance.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
environment: the Environment
|
|
130
|
+
*args: the required arguments
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
T: the instance
|
|
134
|
+
"""
|
|
92
135
|
|
|
93
136
|
def report(self) -> str:
|
|
94
137
|
return str(self)
|
|
@@ -163,7 +206,7 @@ class SingletonScopeInstanceProvider(InstanceProvider):
|
|
|
163
206
|
|
|
164
207
|
class EnvironmentScopeInstanceProvider(InstanceProvider):
|
|
165
208
|
def __init__(self):
|
|
166
|
-
super().__init__(SingletonScopeInstanceProvider, SingletonScope, False, "request")
|
|
209
|
+
super().__init__(SingletonScopeInstanceProvider, SingletonScope, False, "request")
|
|
167
210
|
|
|
168
211
|
def create(self, environment: Environment, *args):
|
|
169
212
|
return EnvironmentScope()
|
|
@@ -362,7 +405,7 @@ class ClassInstanceProvider(InstanceProvider):
|
|
|
362
405
|
for param in method.param_types:
|
|
363
406
|
types.append(param)
|
|
364
407
|
|
|
365
|
-
return
|
|
408
|
+
return types, self.params
|
|
366
409
|
|
|
367
410
|
def create(self, environment: Environment, *args):
|
|
368
411
|
Environment.logger.debug("%s create class %s", self, self.type.__qualname__)
|
|
@@ -449,6 +492,12 @@ class FactoryInstanceProvider(InstanceProvider):
|
|
|
449
492
|
class Lifecycle(Enum):
|
|
450
493
|
"""
|
|
451
494
|
This enum defines the lifecycle phases that can be processed by lifecycle processors.
|
|
495
|
+
Phases are:
|
|
496
|
+
|
|
497
|
+
- ON_INJECT
|
|
498
|
+
- ON_INIT
|
|
499
|
+
- ON_RUNNING
|
|
500
|
+
- ON_DESTROY
|
|
452
501
|
"""
|
|
453
502
|
|
|
454
503
|
__slots__ = []
|
|
@@ -740,6 +789,10 @@ def injectable(eager=True, scope="singleton"):
|
|
|
740
789
|
def factory(eager=True, scope="singleton"):
|
|
741
790
|
"""
|
|
742
791
|
Decorator that needs to be used on a class that implements the Factory interface.
|
|
792
|
+
|
|
793
|
+
Args:
|
|
794
|
+
eager (bool): If True, the corresponding object will be created eagerly when the environment is created.
|
|
795
|
+
scope (str): The scope of the factory, e.g. "singleton", "request", "environment".
|
|
743
796
|
"""
|
|
744
797
|
def decorator(cls):
|
|
745
798
|
Decorators.add(cls, factory)
|
|
@@ -754,6 +807,10 @@ def factory(eager=True, scope="singleton"):
|
|
|
754
807
|
def create(eager=True, scope="singleton"):
|
|
755
808
|
"""
|
|
756
809
|
Any method annotated with @create will be registered as a factory method.
|
|
810
|
+
|
|
811
|
+
Args:
|
|
812
|
+
eager (bool): If True, the corresponding object will be created eagerly when the environment is created.
|
|
813
|
+
scope (str): The scope of the factory, e.g. "singleton", "request", "environment".
|
|
757
814
|
"""
|
|
758
815
|
def decorator(func):
|
|
759
816
|
Decorators.add(func, create, eager, scope)
|
|
@@ -763,7 +820,7 @@ def create(eager=True, scope="singleton"):
|
|
|
763
820
|
|
|
764
821
|
def on_init():
|
|
765
822
|
"""
|
|
766
|
-
Methods annotated with
|
|
823
|
+
Methods annotated with `@on_init` will be called when the instance is created."""
|
|
767
824
|
def decorator(func):
|
|
768
825
|
Decorators.add(func, on_init)
|
|
769
826
|
return func
|
|
@@ -772,7 +829,7 @@ def on_init():
|
|
|
772
829
|
|
|
773
830
|
def on_running():
|
|
774
831
|
"""
|
|
775
|
-
Methods annotated with
|
|
832
|
+
Methods annotated with `@on_running` will be called when the container up and running."""
|
|
776
833
|
def decorator(func):
|
|
777
834
|
Decorators.add(func, on_running)
|
|
778
835
|
return func
|
|
@@ -781,7 +838,7 @@ def on_running():
|
|
|
781
838
|
|
|
782
839
|
def on_destroy():
|
|
783
840
|
"""
|
|
784
|
-
Methods annotated with
|
|
841
|
+
Methods annotated with `@on_destroy` will be called when the instance is destroyed.
|
|
785
842
|
"""
|
|
786
843
|
def decorator(func):
|
|
787
844
|
Decorators.add(func, on_destroy)
|
|
@@ -789,18 +846,19 @@ def on_destroy():
|
|
|
789
846
|
|
|
790
847
|
return decorator
|
|
791
848
|
|
|
792
|
-
def
|
|
849
|
+
def module(imports: Optional[list[Type]] = None):
|
|
793
850
|
"""
|
|
794
|
-
This annotation is used to mark classes that control the
|
|
795
|
-
relative to the module of the class. All
|
|
851
|
+
This annotation is used to mark classes that control the discovery process of injectables based on their location
|
|
852
|
+
relative to the module of the class. All `@injectable`s and `@factory`s that are located in the same or any sub-module will
|
|
796
853
|
be registered and managed accordingly.
|
|
797
|
-
|
|
798
|
-
|
|
854
|
+
|
|
855
|
+
Args:
|
|
856
|
+
imports (Optional[list[Type]]): Optional list of imported module types
|
|
799
857
|
"""
|
|
800
858
|
def decorator(cls):
|
|
801
859
|
Providers.register(ClassInstanceProvider(cls, True))
|
|
802
860
|
|
|
803
|
-
Decorators.add(cls,
|
|
861
|
+
Decorators.add(cls, module, imports)
|
|
804
862
|
Decorators.add(cls, injectable) # do we need that?
|
|
805
863
|
|
|
806
864
|
return cls
|
|
@@ -873,11 +931,28 @@ def conditional(*conditions: Condition):
|
|
|
873
931
|
class Environment:
|
|
874
932
|
"""
|
|
875
933
|
Central class that manages the lifecycle of instances and their dependencies.
|
|
934
|
+
|
|
935
|
+
Usage:
|
|
936
|
+
|
|
937
|
+
```python
|
|
938
|
+
@injectable()
|
|
939
|
+
class Foo:
|
|
940
|
+
def __init__(self):
|
|
941
|
+
|
|
942
|
+
@environment()
|
|
943
|
+
class SimpleEnvironment:
|
|
944
|
+
def __init__(self):
|
|
945
|
+
pass
|
|
946
|
+
|
|
947
|
+
environment = Environment(SimpleEnvironment)
|
|
948
|
+
|
|
949
|
+
foo = environment.get(Foo) # will create an instance of Foo
|
|
950
|
+
```
|
|
876
951
|
"""
|
|
877
952
|
|
|
878
953
|
# static data
|
|
879
954
|
|
|
880
|
-
logger = logging.getLogger(
|
|
955
|
+
logger = logging.getLogger("aspyx.di") # __name__ = module name
|
|
881
956
|
|
|
882
957
|
instance : 'Environment' = None
|
|
883
958
|
|
|
@@ -987,7 +1062,7 @@ class Environment:
|
|
|
987
1062
|
|
|
988
1063
|
# sanity check
|
|
989
1064
|
|
|
990
|
-
decorator = TypeDescriptor.for_type(env).get_decorator(
|
|
1065
|
+
decorator = TypeDescriptor.for_type(env).get_decorator(module)
|
|
991
1066
|
if decorator is None:
|
|
992
1067
|
raise DIRegistrationException(f"{env.__name__} is not an environment class")
|
|
993
1068
|
|
|
@@ -1136,10 +1211,11 @@ class Environment:
|
|
|
1136
1211
|
"""
|
|
1137
1212
|
Create or return a cached instance for the given type.
|
|
1138
1213
|
|
|
1139
|
-
|
|
1214
|
+
Args:
|
|
1140
1215
|
type (Type): The desired type
|
|
1141
1216
|
|
|
1142
|
-
Returns:
|
|
1217
|
+
Returns:
|
|
1218
|
+
T: The requested instance
|
|
1143
1219
|
"""
|
|
1144
1220
|
provider = self.providers.get(type, None)
|
|
1145
1221
|
if provider is None:
|
|
@@ -1413,11 +1489,12 @@ class ThreadScope(Scope):
|
|
|
1413
1489
|
def get(self, provider: AbstractInstanceProvider, environment: Environment, arg_provider: Callable[[], list]):
|
|
1414
1490
|
if not hasattr(self._local, "value"):
|
|
1415
1491
|
self._local.value = provider.create(environment, *arg_provider())
|
|
1492
|
+
|
|
1416
1493
|
return self._local.value
|
|
1417
1494
|
|
|
1418
1495
|
# internal class that is required to import technical instance providers
|
|
1419
1496
|
|
|
1420
|
-
@
|
|
1497
|
+
@module()
|
|
1421
1498
|
class Boot:
|
|
1422
1499
|
# class
|
|
1423
1500
|
|
aspyx/exception/__init__.py
CHANGED
|
@@ -32,9 +32,6 @@ def handle():
|
|
|
32
32
|
|
|
33
33
|
return decorator
|
|
34
34
|
|
|
35
|
-
class ErrorContext():
|
|
36
|
-
pass
|
|
37
|
-
|
|
38
35
|
class Handler:
|
|
39
36
|
# constructor
|
|
40
37
|
|
|
@@ -43,8 +40,13 @@ class Handler:
|
|
|
43
40
|
self.instance = instance
|
|
44
41
|
self.handler = handler
|
|
45
42
|
|
|
46
|
-
def handle(self, exception: BaseException):
|
|
47
|
-
self.handler(self.instance, exception)
|
|
43
|
+
def handle(self, exception: BaseException) -> BaseException:
|
|
44
|
+
result = self.handler(self.instance, exception)
|
|
45
|
+
|
|
46
|
+
if result is not None:
|
|
47
|
+
return result
|
|
48
|
+
else:
|
|
49
|
+
return exception
|
|
48
50
|
|
|
49
51
|
class Chain:
|
|
50
52
|
# constructor
|
|
@@ -55,11 +57,11 @@ class Chain:
|
|
|
55
57
|
|
|
56
58
|
# public
|
|
57
59
|
|
|
58
|
-
def handle(self, exception: BaseException):
|
|
59
|
-
self.handler.handle(exception)
|
|
60
|
+
def handle(self, exception: BaseException) -> BaseException:
|
|
61
|
+
return self.handler.handle(exception)
|
|
60
62
|
|
|
61
63
|
class Invocation:
|
|
62
|
-
def __init__(self, exception:
|
|
64
|
+
def __init__(self, exception: BaseException, chain: Chain):
|
|
63
65
|
self.exception = exception
|
|
64
66
|
self.chain = chain
|
|
65
67
|
self.current = self.chain
|
|
@@ -74,17 +76,26 @@ class ExceptionManager:
|
|
|
74
76
|
|
|
75
77
|
exception_handler_classes = []
|
|
76
78
|
|
|
77
|
-
invocation = ThreadLocal()
|
|
79
|
+
invocation = ThreadLocal[Invocation]()
|
|
78
80
|
|
|
79
81
|
# class methods
|
|
80
82
|
|
|
81
83
|
@classmethod
|
|
82
|
-
def proceed(cls):
|
|
84
|
+
def proceed(cls) -> BaseException:
|
|
85
|
+
"""
|
|
86
|
+
proceed with the next most applicable handler
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
BaseException: the resulting exception
|
|
90
|
+
|
|
91
|
+
"""
|
|
83
92
|
invocation = cls.invocation.get()
|
|
84
93
|
|
|
85
94
|
invocation.current = invocation.current.next
|
|
86
95
|
if invocation.current is not None:
|
|
87
|
-
invocation.current.handle(invocation.exception)
|
|
96
|
+
return invocation.current.handle(invocation.exception)
|
|
97
|
+
else:
|
|
98
|
+
return invocation.exception
|
|
88
99
|
|
|
89
100
|
# constructor
|
|
90
101
|
|
|
@@ -93,7 +104,6 @@ class ExceptionManager:
|
|
|
93
104
|
self.handler : list[Handler] = []
|
|
94
105
|
self.cache: Dict[Type, Chain] = {}
|
|
95
106
|
self.lock = RLock()
|
|
96
|
-
self.current_context: Optional[ErrorContext] = None
|
|
97
107
|
|
|
98
108
|
# internal
|
|
99
109
|
|
|
@@ -153,16 +163,23 @@ class ExceptionManager:
|
|
|
153
163
|
else:
|
|
154
164
|
return None
|
|
155
165
|
|
|
156
|
-
def handle(self, exception:
|
|
166
|
+
def handle(self, exception: BaseException) -> BaseException:
|
|
157
167
|
"""
|
|
158
|
-
handle an exception by invoking the most applicable handler (
|
|
159
|
-
|
|
168
|
+
handle an exception by invoking the most applicable handler (according to mro)
|
|
169
|
+
and return a possible modified exception as a result.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
exception (BaseException): the exception
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
BaseException: the resulting exception
|
|
160
176
|
"""
|
|
161
177
|
chain = self.get_handlers(type(exception))
|
|
162
178
|
if chain is not None:
|
|
163
|
-
|
|
164
179
|
self.invocation.set(Invocation(exception, chain))
|
|
165
180
|
try:
|
|
166
|
-
chain.handle(exception)
|
|
181
|
+
return chain.handle(exception)
|
|
167
182
|
finally:
|
|
168
183
|
self.invocation.clear()
|
|
184
|
+
else:
|
|
185
|
+
return exception # hmmm?
|
aspyx/reflection/proxy.py
CHANGED
|
@@ -14,6 +14,7 @@ class DynamicProxy(Generic[T]):
|
|
|
14
14
|
by intercepting method calls at runtime and handling them as needed.
|
|
15
15
|
|
|
16
16
|
Usage:
|
|
17
|
+
```python
|
|
17
18
|
class MyHandler(DynamicProxy.InvocationHandler):
|
|
18
19
|
def invoke(self, invocation):
|
|
19
20
|
print(f"Intercepted: {invocation.name}")
|
|
@@ -22,10 +23,7 @@ class DynamicProxy(Generic[T]):
|
|
|
22
23
|
|
|
23
24
|
proxy = DynamicProxy.create(SomeClass, MyHandler())
|
|
24
25
|
proxy.some_method(args) # Will be intercepted by MyHandler.invoke
|
|
25
|
-
|
|
26
|
-
Attributes:
|
|
27
|
-
type: The proxied class type.
|
|
28
|
-
invocation_handler: The handler that processes intercepted method calls.
|
|
26
|
+
```
|
|
29
27
|
"""
|
|
30
28
|
# inner class
|
|
31
29
|
|
aspyx/reflection/reflection.py
CHANGED
|
@@ -32,20 +32,44 @@ class Decorators:
|
|
|
32
32
|
Utility class that caches decorators ( Python does not have a feature for this )
|
|
33
33
|
"""
|
|
34
34
|
@classmethod
|
|
35
|
-
def add(cls,
|
|
36
|
-
|
|
35
|
+
def add(cls, func_or_class, decorator: Callable, *args):
|
|
36
|
+
"""
|
|
37
|
+
Remember the decorator
|
|
38
|
+
Args:
|
|
39
|
+
func_or_class: a function or class
|
|
40
|
+
decorator: the decorator
|
|
41
|
+
*args: any arguments supplied to the decorator
|
|
42
|
+
"""
|
|
43
|
+
decorators = getattr(func_or_class, '__decorators__', None)
|
|
37
44
|
if decorators is None:
|
|
38
|
-
setattr(
|
|
45
|
+
setattr(func_or_class, '__decorators__', [DecoratorDescriptor(decorator, *args)])
|
|
39
46
|
else:
|
|
40
47
|
decorators.append(DecoratorDescriptor(decorator, *args))
|
|
41
48
|
|
|
42
49
|
@classmethod
|
|
43
|
-
def has_decorator(cls,
|
|
44
|
-
|
|
50
|
+
def has_decorator(cls, func_or_class, callable: Callable) -> bool:
|
|
51
|
+
"""
|
|
52
|
+
Return True, if the function or class is decorated with the decorator
|
|
53
|
+
Args:
|
|
54
|
+
func_or_class: a function or class
|
|
55
|
+
callable: the decorator
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
bool: the result
|
|
59
|
+
"""
|
|
60
|
+
return any(decorator.decorator is callable for decorator in Decorators.get(func_or_class))
|
|
45
61
|
|
|
46
62
|
@classmethod
|
|
47
|
-
def get(cls,
|
|
48
|
-
|
|
63
|
+
def get(cls, func_or_class) -> list[DecoratorDescriptor]:
|
|
64
|
+
"""
|
|
65
|
+
return the list of decorators associated with the given function or class
|
|
66
|
+
Args:
|
|
67
|
+
func_or_class: the function or class
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
list[DecoratorDescriptor]: ths list
|
|
71
|
+
"""
|
|
72
|
+
return getattr(func_or_class, '__decorators__', [])
|
|
49
73
|
|
|
50
74
|
class TypeDescriptor:
|
|
51
75
|
"""
|
|
@@ -79,30 +103,42 @@ class TypeDescriptor:
|
|
|
79
103
|
def get_name(self) -> str:
|
|
80
104
|
"""
|
|
81
105
|
return the method name
|
|
82
|
-
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
str: the method name
|
|
83
109
|
"""
|
|
84
110
|
return self.method.__name__
|
|
85
111
|
|
|
86
112
|
def get_doc(self, default = "") -> str:
|
|
87
113
|
"""
|
|
88
114
|
return the method docstring
|
|
89
|
-
|
|
90
|
-
:
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
default: the default if no docstring is found
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
str: the docstring
|
|
91
121
|
"""
|
|
92
122
|
return self.method.__doc__ or default
|
|
93
123
|
|
|
94
124
|
def is_async(self) -> bool:
|
|
95
125
|
"""
|
|
96
126
|
return true if the method is asynchronous
|
|
97
|
-
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
bool: async flag
|
|
98
130
|
"""
|
|
99
131
|
return inspect.iscoroutinefunction(self.method)
|
|
100
132
|
|
|
101
133
|
def get_decorator(self, decorator: Callable) -> Optional[DecoratorDescriptor]:
|
|
102
134
|
"""
|
|
103
135
|
return the DecoratorDescriptor - if any - associated with the passed Callable
|
|
104
|
-
|
|
105
|
-
:
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
decorator: the decorator
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Optional[DecoratorDescriptor]: the DecoratorDescriptor or None
|
|
106
142
|
"""
|
|
107
143
|
for dec in self.decorators:
|
|
108
144
|
if dec.decorator is decorator:
|
|
@@ -113,8 +149,12 @@ class TypeDescriptor:
|
|
|
113
149
|
def has_decorator(self, decorator: Callable) -> bool:
|
|
114
150
|
"""
|
|
115
151
|
return True if the method is decorated with the decorator
|
|
116
|
-
|
|
117
|
-
:
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
decorator: the decorator callable
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
bool: True if the method is decorated with the decorator
|
|
118
158
|
"""
|
|
119
159
|
for dec in self.decorators:
|
|
120
160
|
if dec.decorator is decorator:
|
aspyx/threading/__init__.py
CHANGED
aspyx/threading/thread_local.py
CHANGED
|
@@ -22,7 +22,9 @@ class ThreadLocal(Generic[T]):
|
|
|
22
22
|
def get(self) -> Optional[T]:
|
|
23
23
|
"""
|
|
24
24
|
return the current value or invoke the optional factory to compute one
|
|
25
|
-
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Optional[T]: the value associated with the current thread
|
|
26
28
|
"""
|
|
27
29
|
if not hasattr(self.local, "value"):
|
|
28
30
|
if self.factory is not None:
|
|
@@ -35,7 +37,9 @@ class ThreadLocal(Generic[T]):
|
|
|
35
37
|
def set(self, value: T) -> None:
|
|
36
38
|
"""
|
|
37
39
|
set a value in the current thread
|
|
38
|
-
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
value: the value
|
|
39
43
|
"""
|
|
40
44
|
self.local.value = value
|
|
41
45
|
|
aspyx/util/stringbuilder.py
CHANGED
|
@@ -17,8 +17,12 @@ class StringBuilder:
|
|
|
17
17
|
def append(self, s: str) -> "StringBuilder":
|
|
18
18
|
"""
|
|
19
19
|
append a string to the end of the string builder
|
|
20
|
-
|
|
21
|
-
:
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
s (str): the string
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
StringBuilder: self
|
|
22
26
|
"""
|
|
23
27
|
self._parts.append(str(s))
|
|
24
28
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aspyx
|
|
3
|
-
Version: 1.4.
|
|
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
|
|
@@ -40,12 +40,14 @@ Dynamic: license-file
|
|
|
40
40
|

|
|
41
41
|

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

|
|
44
46
|
|
|
45
47
|
## Table of Contents
|
|
46
48
|
|
|
47
49
|
- [Motivation](#motivation)
|
|
48
|
-
- [
|
|
50
|
+
- [Overview](#overview)
|
|
49
51
|
- [Installation](#installation)
|
|
50
52
|
- [Registration](#registration)
|
|
51
53
|
- [Class](#class)
|
|
@@ -71,16 +73,19 @@ Dynamic: license-file
|
|
|
71
73
|
|
|
72
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
|
|
73
75
|
|
|
74
|
-
- bring both di and AOP features together in a lightweight library,
|
|
76
|
+
- bring both di and AOP features together in a lightweight library ( still only about 2T loc),
|
|
75
77
|
- be as minimal invasive as possible,
|
|
76
78
|
- offering mechanisms to easily extend and customize features without touching the core,
|
|
77
|
-
- while still offering a _simple_ and _readable_ api that doesnt overwhelm developers
|
|
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:
|
|
78
82
|
|
|
79
|
-
|
|
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.
|
|
80
85
|
|
|
81
|
-
#
|
|
86
|
+
# Overview
|
|
82
87
|
|
|
83
|
-
Aspyx is a lightweight Python library that provides both Dependency Injection (DI) and Aspect-Oriented Programming (AOP) support.
|
|
88
|
+
Aspyx is a lightweight - still only about 2T LOC- Python library that provides both Dependency Injection (DI) and Aspect-Oriented Programming (AOP) support.
|
|
84
89
|
|
|
85
90
|
The following DI features are supported
|
|
86
91
|
- constructor and setter injection
|
|
@@ -89,13 +94,13 @@ The following DI features are supported
|
|
|
89
94
|
- post processors
|
|
90
95
|
- support for factory classes and methods
|
|
91
96
|
- support for eager and lazy construction
|
|
92
|
-
- support for scopes singleton, request and thread
|
|
97
|
+
- support for scopes "singleton", "request" and "thread"
|
|
93
98
|
- possibility to add custom scopes
|
|
94
99
|
- conditional registration of classes and factories ( aka profiles in spring )
|
|
95
100
|
- lifecycle events methods `on_init`, `on_destroy`, `on_running`
|
|
96
|
-
- bundling of injectable objects
|
|
97
|
-
-
|
|
98
|
-
- hierarchical environments
|
|
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.
|
|
99
104
|
|
|
100
105
|
With respect to AOP:
|
|
101
106
|
- support for before, around, after and error aspects
|
|
@@ -107,7 +112,8 @@ The library is thread-safe and heavily performance optimized as most of the runt
|
|
|
107
112
|
Let's look at a simple example
|
|
108
113
|
|
|
109
114
|
```python
|
|
110
|
-
from aspyx.di import injectable, on_init, on_destroy,
|
|
115
|
+
from aspyx.di import injectable, on_init, on_destroy, module, Environment
|
|
116
|
+
|
|
111
117
|
|
|
112
118
|
@injectable()
|
|
113
119
|
class Foo:
|
|
@@ -117,27 +123,28 @@ class Foo:
|
|
|
117
123
|
def hello(self, msg: str):
|
|
118
124
|
print(f"hello {msg}")
|
|
119
125
|
|
|
126
|
+
|
|
120
127
|
@injectable() # eager and singleton by default
|
|
121
128
|
class Bar:
|
|
122
|
-
def __init__(self, foo: Foo):
|
|
129
|
+
def __init__(self, foo: Foo): # will inject the Foo dependency
|
|
123
130
|
self.foo = foo
|
|
124
131
|
|
|
125
|
-
@on_init()
|
|
132
|
+
@on_init() # a lifecycle callback called after the constructor and all possible injections
|
|
126
133
|
def init(self):
|
|
127
134
|
...
|
|
128
135
|
|
|
129
136
|
|
|
130
|
-
# this class will
|
|
131
|
-
# 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
|
|
132
138
|
|
|
133
|
-
@
|
|
134
|
-
class
|
|
139
|
+
@module()
|
|
140
|
+
class SampleModule:
|
|
135
141
|
def __init__(self):
|
|
136
142
|
pass
|
|
137
143
|
|
|
144
|
+
|
|
138
145
|
# create environment
|
|
139
146
|
|
|
140
|
-
environment = Environment(
|
|
147
|
+
environment = Environment(SampleModule)
|
|
141
148
|
|
|
142
149
|
# fetch an instance
|
|
143
150
|
|
|
@@ -171,12 +178,7 @@ class SampleAdvice:
|
|
|
171
178
|
return invocation.proceed()
|
|
172
179
|
```
|
|
173
180
|
|
|
174
|
-
|
|
175
|
-
- the method
|
|
176
|
-
- args
|
|
177
|
-
- kwargs
|
|
178
|
-
- the result
|
|
179
|
-
- 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.
|
|
180
182
|
|
|
181
183
|
Let's look at the details
|
|
182
184
|
|
|
@@ -280,21 +282,26 @@ Valid conditions are created by:
|
|
|
280
282
|
|
|
281
283
|
## Definition
|
|
282
284
|
|
|
283
|
-
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`.
|
|
284
288
|
|
|
285
289
|
**Example**:
|
|
286
290
|
```python
|
|
287
|
-
@
|
|
288
|
-
class
|
|
291
|
+
@module()
|
|
292
|
+
class SampleModule:
|
|
289
293
|
def __init__(self):
|
|
290
294
|
pass
|
|
291
|
-
|
|
292
|
-
environment = Environment(SampleEnvironment)
|
|
293
295
|
```
|
|
294
296
|
|
|
295
|
-
|
|
296
|
-
|
|
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.
|
|
297
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
|
+
```
|
|
298
305
|
|
|
299
306
|
By adding the parameter `features: list[str]`, it is possible to filter injectables by evaluating the corresponding `@conditional` decorators.
|
|
300
307
|
|
|
@@ -307,21 +314,21 @@ class DevOnly:
|
|
|
307
314
|
def __init__(self):
|
|
308
315
|
pass
|
|
309
316
|
|
|
310
|
-
@
|
|
311
|
-
class
|
|
317
|
+
@module()
|
|
318
|
+
class SampleModule():
|
|
312
319
|
def __init__(self):
|
|
313
320
|
pass
|
|
314
321
|
|
|
315
|
-
environment = Environment(
|
|
322
|
+
environment = Environment(SampleModule, features=["dev"])
|
|
316
323
|
```
|
|
317
324
|
|
|
318
325
|
|
|
319
|
-
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.
|
|
320
327
|
|
|
321
328
|
**Example**:
|
|
322
329
|
```python
|
|
323
|
-
@
|
|
324
|
-
class
|
|
330
|
+
@module()
|
|
331
|
+
class SampleModule(imports=[OtherModule]):
|
|
325
332
|
def __init__(self):
|
|
326
333
|
pass
|
|
327
334
|
```
|
|
@@ -330,8 +337,9 @@ Another possibility is to add a parent environment as an `Environment` construct
|
|
|
330
337
|
|
|
331
338
|
**Example**:
|
|
332
339
|
```python
|
|
333
|
-
rootEnvironment = Environment(
|
|
334
|
-
|
|
340
|
+
rootEnvironment = Environment(RootModule)
|
|
341
|
+
|
|
342
|
+
environment = Environment(SampleModule, parent=rootEnvironment)
|
|
335
343
|
```
|
|
336
344
|
|
|
337
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.
|
|
@@ -464,7 +472,9 @@ class SingletonScope(Scope):
|
|
|
464
472
|
|
|
465
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.
|
|
466
474
|
|
|
467
|
-
|
|
475
|
+
On the other hand, advices are also regular DI objects, as they will usually require some kind of - injected - context.
|
|
476
|
+
|
|
477
|
+
Advices are regular classes decorated with `@advice` that define aspect methods.
|
|
468
478
|
|
|
469
479
|
```python
|
|
470
480
|
@advice
|
|
@@ -516,7 +526,7 @@ All methods are expected to have single `Invocation` parameter, that stores
|
|
|
516
526
|
- `result` the result ( initially `None`)
|
|
517
527
|
- `exception` a possible caught exception ( initially `None`)
|
|
518
528
|
|
|
519
|
-
⚠️ **
|
|
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.
|
|
520
530
|
|
|
521
531
|
If the `proceed` is called with parameters, they will replace the original parameters!
|
|
522
532
|
|
|
@@ -562,6 +572,12 @@ class TransactionAdvice:
|
|
|
562
572
|
|
|
563
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!
|
|
564
574
|
|
|
575
|
+
## Advice Lifecycle and visibility.
|
|
576
|
+
|
|
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
|
+
|
|
565
581
|
# Threading
|
|
566
582
|
|
|
567
583
|
A handy decorator `@synchronized` in combination with the respective advice is implemented that automatically synchronizes methods with a `RLock` associated with the instance.
|
|
@@ -652,8 +668,8 @@ Two specific source are already implemented:
|
|
|
652
668
|
Typically you create the required configuration sources in an environment class, e.g.
|
|
653
669
|
|
|
654
670
|
```python
|
|
655
|
-
@
|
|
656
|
-
class
|
|
671
|
+
@module()
|
|
672
|
+
class SampleModule:
|
|
657
673
|
# constructor
|
|
658
674
|
|
|
659
675
|
def __init__(self):
|
|
@@ -725,8 +741,8 @@ class DerivedException(Exception):
|
|
|
725
741
|
def __init__(self):
|
|
726
742
|
pass
|
|
727
743
|
|
|
728
|
-
@
|
|
729
|
-
class
|
|
744
|
+
@module()
|
|
745
|
+
class SampleModule:
|
|
730
746
|
# constructor
|
|
731
747
|
|
|
732
748
|
def __init__(self):
|
|
@@ -820,6 +836,10 @@ class ExceptionAdvice:
|
|
|
820
836
|
- bugfixes
|
|
821
837
|
- added `@ExceptionManager`
|
|
822
838
|
|
|
839
|
+
**1.4.1**
|
|
840
|
+
|
|
841
|
+
- mkdocs
|
|
842
|
+
|
|
823
843
|
|
|
824
844
|
|
|
825
845
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
aspyx/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
aspyx/di/__init__.py,sha256=BDOloIhmCIUJWC7l4PLtuiWS1LlWyitIofLCLcyXYpQ,1097
|
|
3
|
+
aspyx/di/di.py,sha256=ozZanDcrmluzghQBqqD_vbav3Civ-V4R8XXwlBAM3MI,44068
|
|
4
|
+
aspyx/di/aop/__init__.py,sha256=rn6LSpzFtUOlgaBATyhLRWBzFmZ6XoVKA9B8SgQzYEI,746
|
|
5
|
+
aspyx/di/aop/aop.py,sha256=Cn-fqFW6PznVDM38fPX7mqlSpjGKMsgpJRBSYBv59xY,18403
|
|
6
|
+
aspyx/di/configuration/__init__.py,sha256=flM9A79J2wfA5I8goQbxs4tTqYustR9tn_9s0YO2WJQ,484
|
|
7
|
+
aspyx/di/configuration/configuration.py,sha256=cXW40bPXiUZ9hUtBoZkSATT3nLrDPWsSqxtgASIBQaM,4375
|
|
8
|
+
aspyx/di/configuration/env_configuration_source.py,sha256=FXPvREzq2ZER6_GG5xdpx154TQQDxZVf7LW7cvaylAk,1446
|
|
9
|
+
aspyx/di/configuration/yaml_configuration_source.py,sha256=NDl3SeoLMNVlzHgfP-Ysvhco1tRew_zFnBL5gGy2WRk,550
|
|
10
|
+
aspyx/di/threading/__init__.py,sha256=qrWdaq7MewQ2UmZy4J0Dn6BhY-ahfiG3xsv-EHqoqSE,191
|
|
11
|
+
aspyx/di/threading/synchronized.py,sha256=BQ9PjMQUJsF5r-qWaDgvqg3AvFm_R9QZdKB49EkoelQ,1263
|
|
12
|
+
aspyx/exception/__init__.py,sha256=OZwv-C3ZHD0Eg1rohCQMj575WLJ7lfYuk6PZD6sh1MA,211
|
|
13
|
+
aspyx/exception/exception_manager.py,sha256=ihQ8Hs_EAUi-4xtVOn6kNZVblJjznpscLQ4vKXfWq7s,5228
|
|
14
|
+
aspyx/reflection/__init__.py,sha256=r2sNJrfHDpuqaIYu4fTYsoo046gpgn4VTd7bsS3mQJY,282
|
|
15
|
+
aspyx/reflection/proxy.py,sha256=kaVPeEGuerdYgcgchMe99c8xykDskRSYR-w4OF83Ofo,1868
|
|
16
|
+
aspyx/reflection/reflection.py,sha256=AgChenUzK9elcqOc_BfMiszTQuXrsy0NHYkqy9Jsl0E,8066
|
|
17
|
+
aspyx/threading/__init__.py,sha256=3clmbCDP37GPan3dWtxTQvpg0Ti4aFzruAbUClkHGi0,147
|
|
18
|
+
aspyx/threading/thread_local.py,sha256=86dNtbA4k2B-rNUUnZgn3_pU0DAojgLrRnh8RL6zf1E,1196
|
|
19
|
+
aspyx/util/__init__.py,sha256=8H2yKkXu3nkRGeTerb8ialzKGfvzUx44XUWFUYcYuQM,125
|
|
20
|
+
aspyx/util/stringbuilder.py,sha256=a-0T4YEXSJFUuQ3ztKN1ZPARkh8dIGMSkNEEJHRN7dc,856
|
|
21
|
+
aspyx-1.4.1.dist-info/licenses/LICENSE,sha256=n4jfx_MNj7cBtPhhI7MCoB_K35cj1icP9yJ4Rh4vlvY,1070
|
|
22
|
+
aspyx-1.4.1.dist-info/METADATA,sha256=cEYH9P1Mng4RjRQID-bUp-5X6fbxUHhQWf3WDUwkPI0,26564
|
|
23
|
+
aspyx-1.4.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
24
|
+
aspyx-1.4.1.dist-info/top_level.txt,sha256=A_ZwhBY_ybIgjZlztd44eaOrWqkJAndiqjGlbJ3tR_I,6
|
|
25
|
+
aspyx-1.4.1.dist-info/RECORD,,
|
aspyx-1.4.0.dist-info/RECORD
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
aspyx/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
aspyx/di/__init__.py,sha256=OfETLGaquTbFHhhDRzzGtnSu-KkMj68aDdEaVU19KoI,1107
|
|
3
|
-
aspyx/di/di.py,sha256=qoOChsiUBla52UaclRlX6o5ZxRylaFF4IneanvwISos,42095
|
|
4
|
-
aspyx/di/aop/__init__.py,sha256=nOABex49zSyMZ2w1ezwX3Q3yrOcQRSDjDtSj0DwKVbQ,233
|
|
5
|
-
aspyx/di/aop/aop.py,sha256=36p3jCnNtrDL11jTi7NNpW1eGO_BqKL0s5rErsPdxps,17520
|
|
6
|
-
aspyx/di/configuration/__init__.py,sha256=mweJ3tZX1YJfY1d4ra-i0TWEcF3EwXBpGbHrKg1Kc6E,380
|
|
7
|
-
aspyx/di/configuration/configuration.py,sha256=KfPjrlUhhmEOUxdJiXePt5RGxKc8JczkWqlEBjpWQTg,4362
|
|
8
|
-
aspyx/di/configuration/env_configuration_source.py,sha256=FXPvREzq2ZER6_GG5xdpx154TQQDxZVf7LW7cvaylAk,1446
|
|
9
|
-
aspyx/di/configuration/yaml_configuration_source.py,sha256=NDl3SeoLMNVlzHgfP-Ysvhco1tRew_zFnBL5gGy2WRk,550
|
|
10
|
-
aspyx/di/threading/__init__.py,sha256=qrWdaq7MewQ2UmZy4J0Dn6BhY-ahfiG3xsv-EHqoqSE,191
|
|
11
|
-
aspyx/di/threading/synchronized.py,sha256=BQ9PjMQUJsF5r-qWaDgvqg3AvFm_R9QZdKB49EkoelQ,1263
|
|
12
|
-
aspyx/exception/__init__.py,sha256=2Jo0a_fZK8_U9SpPZ0j4aeAXJZ28uw6g-20TH_85JqY,200
|
|
13
|
-
aspyx/exception/exception_manager.py,sha256=8H5fbbcpzLxiK7OI-EZaXyX5Db4uZt9-VrAx5LMiSm8,4692
|
|
14
|
-
aspyx/reflection/__init__.py,sha256=r2sNJrfHDpuqaIYu4fTYsoo046gpgn4VTd7bsS3mQJY,282
|
|
15
|
-
aspyx/reflection/proxy.py,sha256=zJ6Psd6zWfFABdrKOf4cULt3gibyqCRdcR6z8WKIkzE,1982
|
|
16
|
-
aspyx/reflection/reflection.py,sha256=bzH5KVJ5X5ycQu5SG7BFZtioWN7sa1w1Y-xR_Y-oN6w,7113
|
|
17
|
-
aspyx/threading/__init__.py,sha256=_j_AQ4t1ecRaKIb9KCTT_EV0b7hivNML-2wV2XF7G6Y,125
|
|
18
|
-
aspyx/threading/thread_local.py,sha256=nOSS2DM1rIHmzdU9_fjxaUF3oXCaRTBHwe76IdwMqC8,1158
|
|
19
|
-
aspyx/util/__init__.py,sha256=8H2yKkXu3nkRGeTerb8ialzKGfvzUx44XUWFUYcYuQM,125
|
|
20
|
-
aspyx/util/stringbuilder.py,sha256=L3MkHAo4CJrBXuWmaRQASIa9EAs8O_ea7EjZoLsvp08,811
|
|
21
|
-
aspyx-1.4.0.dist-info/licenses/LICENSE,sha256=n4jfx_MNj7cBtPhhI7MCoB_K35cj1icP9yJ4Rh4vlvY,1070
|
|
22
|
-
aspyx-1.4.0.dist-info/METADATA,sha256=S9Qcqc9v6VHJSU76dGT_iEo1Z4ghvpp8nE7uxoc2NkI,25213
|
|
23
|
-
aspyx-1.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
24
|
-
aspyx-1.4.0.dist-info/top_level.txt,sha256=A_ZwhBY_ybIgjZlztd44eaOrWqkJAndiqjGlbJ3tR_I,6
|
|
25
|
-
aspyx-1.4.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|