aspyx 1.1.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.1.0/src/aspyx.egg-info → aspyx-1.3.0}/PKG-INFO +150 -29
- {aspyx-1.1.0 → aspyx-1.3.0}/README.md +149 -28
- {aspyx-1.1.0 → aspyx-1.3.0}/pyproject.toml +1 -1
- aspyx-1.3.0/src/aspyx/di/__init__.py +38 -0
- {aspyx-1.1.0 → aspyx-1.3.0}/src/aspyx/di/aop/aop.py +86 -4
- {aspyx-1.1.0 → aspyx-1.3.0}/src/aspyx/di/configuration/__init__.py +4 -1
- {aspyx-1.1.0 → aspyx-1.3.0}/src/aspyx/di/configuration/configuration.py +1 -51
- aspyx-1.3.0/src/aspyx/di/configuration/env_configuration_source.py +55 -0
- aspyx-1.3.0/src/aspyx/di/configuration/yaml_configuration_source.py +26 -0
- {aspyx-1.1.0 → aspyx-1.3.0}/src/aspyx/di/di.py +339 -207
- 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.1.0 → aspyx-1.3.0}/src/aspyx/reflection/reflection.py +33 -1
- {aspyx-1.1.0 → aspyx-1.3.0/src/aspyx.egg-info}/PKG-INFO +150 -29
- {aspyx-1.1.0 → aspyx-1.3.0}/src/aspyx.egg-info/SOURCES.txt +4 -0
- {aspyx-1.1.0 → aspyx-1.3.0}/tests/test_aop.py +49 -17
- {aspyx-1.1.0 → aspyx-1.3.0}/tests/test_configuration.py +31 -2
- {aspyx-1.1.0 → aspyx-1.3.0}/tests/test_di.py +88 -15
- {aspyx-1.1.0 → aspyx-1.3.0}/tests/test_di_cycle.py +2 -2
- aspyx-1.1.0/src/aspyx/di/__init__.py +0 -33
- {aspyx-1.1.0 → aspyx-1.3.0}/LICENSE +0 -0
- {aspyx-1.1.0 → aspyx-1.3.0}/setup.cfg +0 -0
- {aspyx-1.1.0 → aspyx-1.3.0}/src/aspyx/di/aop/__init__.py +0 -0
- {aspyx-1.1.0 → aspyx-1.3.0}/src/aspyx/di/util/__init__.py +0 -0
- {aspyx-1.1.0 → aspyx-1.3.0}/src/aspyx/di/util/stringbuilder.py +0 -0
- {aspyx-1.1.0 → aspyx-1.3.0}/src/aspyx/reflection/__init__.py +0 -0
- {aspyx-1.1.0 → aspyx-1.3.0}/src/aspyx/reflection/proxy.py +0 -0
- {aspyx-1.1.0 → aspyx-1.3.0}/src/aspyx.egg-info/dependency_links.txt +0 -0
- {aspyx-1.1.0 → aspyx-1.3.0}/src/aspyx.egg-info/top_level.txt +0 -0
- {aspyx-1.1.0 → aspyx-1.3.0}/tests/test_proxy.py +0 -0
- {aspyx-1.1.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
|
|
|
@@ -192,9 +211,9 @@ The decorator accepts the keyword arguments
|
|
|
192
211
|
- `singleton`
|
|
193
212
|
objects are created once inside an environment and cached. This is the default.
|
|
194
213
|
- `request`
|
|
195
|
-
|
|
214
|
+
objects are created on every injection request
|
|
196
215
|
- `thread`
|
|
197
|
-
objects are
|
|
216
|
+
objects are created and cached with respect to the current thread.
|
|
198
217
|
|
|
199
218
|
Other scopes - e.g. session related scopes - can be defined dynamically. Please check the corresponding chapter.
|
|
200
219
|
|
|
@@ -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)`
|
|
@@ -455,7 +529,7 @@ Both add the fluent methods:
|
|
|
455
529
|
|
|
456
530
|
The fluent methods `named`, `matches` and `of_type` can be called multiple times!
|
|
457
531
|
|
|
458
|
-
**Example**:
|
|
532
|
+
**Example**: react on both `transactional` decorators on methods or classes
|
|
459
533
|
|
|
460
534
|
```python
|
|
461
535
|
@injectable()
|
|
@@ -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.
|
|
@@ -478,11 +556,13 @@ class Foo:
|
|
|
478
556
|
def __init__(self):
|
|
479
557
|
pass
|
|
480
558
|
|
|
481
|
-
@
|
|
482
|
-
def
|
|
559
|
+
@inject_value("HOME")
|
|
560
|
+
def inject_home(self, os: str):
|
|
483
561
|
...
|
|
484
562
|
```
|
|
485
563
|
|
|
564
|
+
If required a coercion will be executed.
|
|
565
|
+
|
|
486
566
|
This concept relies on a central object `ConfigurationManager` that stores the overall configuration values as provided by so called configuration sources that are defined as follows.
|
|
487
567
|
|
|
488
568
|
```python
|
|
@@ -494,14 +574,24 @@ class ConfigurationSource(ABC):
|
|
|
494
574
|
|
|
495
575
|
@abstractmethod
|
|
496
576
|
def load(self) -> dict:
|
|
497
|
-
pass
|
|
498
577
|
```
|
|
499
578
|
|
|
500
579
|
The `load` method is able to return a tree-like structure by returning a `dict`.
|
|
501
580
|
|
|
502
|
-
|
|
581
|
+
Configuration variables are retrieved with the method
|
|
582
|
+
|
|
583
|
+
```python
|
|
584
|
+
def get(self, path: str, type: Type[T], default : Optional[T]=None) -> T:
|
|
585
|
+
```
|
|
503
586
|
|
|
504
|
-
|
|
587
|
+
- `path`
|
|
588
|
+
a '.' separated path
|
|
589
|
+
- `type`
|
|
590
|
+
the desired type
|
|
591
|
+
- `default`
|
|
592
|
+
a default, if no value is registered
|
|
593
|
+
|
|
594
|
+
Sources can be added dynamically by registering them.
|
|
505
595
|
|
|
506
596
|
**Example**:
|
|
507
597
|
```python
|
|
@@ -521,6 +611,31 @@ class SampleConfigurationSource(ConfigurationSource):
|
|
|
521
611
|
}
|
|
522
612
|
```
|
|
523
613
|
|
|
614
|
+
Two specific source are already implemented:
|
|
615
|
+
- `EnvConfigurationSource`
|
|
616
|
+
reads the os environment variables
|
|
617
|
+
- `YamlConfigurationSource`
|
|
618
|
+
reads a specific yaml file
|
|
619
|
+
|
|
620
|
+
Typically you create the required configuration sources in an environment class, e.g.
|
|
621
|
+
|
|
622
|
+
```python
|
|
623
|
+
@environment()
|
|
624
|
+
class SampleEnvironment:
|
|
625
|
+
# constructor
|
|
626
|
+
|
|
627
|
+
def __init__(self):
|
|
628
|
+
pass
|
|
629
|
+
|
|
630
|
+
@create()
|
|
631
|
+
def create_env_source(self) -> EnvConfigurationSource:
|
|
632
|
+
return EnvConfigurationSource()
|
|
633
|
+
|
|
634
|
+
@create()
|
|
635
|
+
def create_yaml_source(self) -> YamlConfigurationSource:
|
|
636
|
+
return YamlConfigurationSource("config.yaml")
|
|
637
|
+
```
|
|
638
|
+
|
|
524
639
|
# Reflection
|
|
525
640
|
|
|
526
641
|
As the library heavily relies on type introspection of classes and methods, a utility class `TypeDescriptor` is available that covers type information on classes.
|
|
@@ -537,7 +652,7 @@ it offers the methods
|
|
|
537
652
|
- `get_method(name: str, local=False)`
|
|
538
653
|
return a single either local or overall method
|
|
539
654
|
- `has_decorator(decorator: Callable) -> bool`
|
|
540
|
-
return `True`, if the class is decorated with the specified
|
|
655
|
+
return `True`, if the class is decorated with the specified decorator
|
|
541
656
|
- `get_decorator(decorator) -> Optional[DecoratorDescriptor]`
|
|
542
657
|
return a descriptor covering the decorator. In addition to the callable, it also stores the supplied args in the `args` property
|
|
543
658
|
|
|
@@ -545,9 +660,9 @@ The returned method descriptors offer:
|
|
|
545
660
|
- `param_types`
|
|
546
661
|
list of arg types
|
|
547
662
|
- `return_type`
|
|
548
|
-
the
|
|
663
|
+
the return type
|
|
549
664
|
- `has_decorator(decorator: Callable) -> bool`
|
|
550
|
-
return `True`, if the method is decorated with the specified
|
|
665
|
+
return `True`, if the method is decorated with the specified decorator
|
|
551
666
|
- `get_decorator(decorator: Callable) -> Optional[DecoratorDescriptor]`
|
|
552
667
|
return a descriptor covering the decorator. In addition to the callable, it also stores the supplied args in the `args` property
|
|
553
668
|
|
|
@@ -557,9 +672,9 @@ Whenver you define a custom decorator, you will need to register it accordingly.
|
|
|
557
672
|
|
|
558
673
|
**Example**:
|
|
559
674
|
```python
|
|
560
|
-
def transactional():
|
|
675
|
+
def transactional(scope):
|
|
561
676
|
def decorator(func):
|
|
562
|
-
Decorators.add(func, transactional)
|
|
677
|
+
Decorators.add(func, transactional, scope) # also add _all_ parameters in order to cache them
|
|
563
678
|
return func
|
|
564
679
|
|
|
565
680
|
return decorator
|
|
@@ -577,6 +692,12 @@ def transactional():
|
|
|
577
692
|
- added `@on_running()` callback
|
|
578
693
|
- added `thread` scope
|
|
579
694
|
|
|
695
|
+
**1.2.0**
|
|
696
|
+
|
|
697
|
+
- added `YamlConfigurationSource`
|
|
698
|
+
|
|
699
|
+
**1.2.1**
|
|
700
|
+
|
|
580
701
|
|
|
581
702
|
|
|
582
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
|
|
|
@@ -160,9 +179,9 @@ The decorator accepts the keyword arguments
|
|
|
160
179
|
- `singleton`
|
|
161
180
|
objects are created once inside an environment and cached. This is the default.
|
|
162
181
|
- `request`
|
|
163
|
-
|
|
182
|
+
objects are created on every injection request
|
|
164
183
|
- `thread`
|
|
165
|
-
objects are
|
|
184
|
+
objects are created and cached with respect to the current thread.
|
|
166
185
|
|
|
167
186
|
Other scopes - e.g. session related scopes - can be defined dynamically. Please check the corresponding chapter.
|
|
168
187
|
|
|
@@ -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)`
|
|
@@ -423,7 +497,7 @@ Both add the fluent methods:
|
|
|
423
497
|
|
|
424
498
|
The fluent methods `named`, `matches` and `of_type` can be called multiple times!
|
|
425
499
|
|
|
426
|
-
**Example**:
|
|
500
|
+
**Example**: react on both `transactional` decorators on methods or classes
|
|
427
501
|
|
|
428
502
|
```python
|
|
429
503
|
@injectable()
|
|
@@ -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.
|
|
@@ -446,11 +524,13 @@ class Foo:
|
|
|
446
524
|
def __init__(self):
|
|
447
525
|
pass
|
|
448
526
|
|
|
449
|
-
@
|
|
450
|
-
def
|
|
527
|
+
@inject_value("HOME")
|
|
528
|
+
def inject_home(self, os: str):
|
|
451
529
|
...
|
|
452
530
|
```
|
|
453
531
|
|
|
532
|
+
If required a coercion will be executed.
|
|
533
|
+
|
|
454
534
|
This concept relies on a central object `ConfigurationManager` that stores the overall configuration values as provided by so called configuration sources that are defined as follows.
|
|
455
535
|
|
|
456
536
|
```python
|
|
@@ -462,14 +542,24 @@ class ConfigurationSource(ABC):
|
|
|
462
542
|
|
|
463
543
|
@abstractmethod
|
|
464
544
|
def load(self) -> dict:
|
|
465
|
-
pass
|
|
466
545
|
```
|
|
467
546
|
|
|
468
547
|
The `load` method is able to return a tree-like structure by returning a `dict`.
|
|
469
548
|
|
|
470
|
-
|
|
549
|
+
Configuration variables are retrieved with the method
|
|
550
|
+
|
|
551
|
+
```python
|
|
552
|
+
def get(self, path: str, type: Type[T], default : Optional[T]=None) -> T:
|
|
553
|
+
```
|
|
471
554
|
|
|
472
|
-
|
|
555
|
+
- `path`
|
|
556
|
+
a '.' separated path
|
|
557
|
+
- `type`
|
|
558
|
+
the desired type
|
|
559
|
+
- `default`
|
|
560
|
+
a default, if no value is registered
|
|
561
|
+
|
|
562
|
+
Sources can be added dynamically by registering them.
|
|
473
563
|
|
|
474
564
|
**Example**:
|
|
475
565
|
```python
|
|
@@ -489,6 +579,31 @@ class SampleConfigurationSource(ConfigurationSource):
|
|
|
489
579
|
}
|
|
490
580
|
```
|
|
491
581
|
|
|
582
|
+
Two specific source are already implemented:
|
|
583
|
+
- `EnvConfigurationSource`
|
|
584
|
+
reads the os environment variables
|
|
585
|
+
- `YamlConfigurationSource`
|
|
586
|
+
reads a specific yaml file
|
|
587
|
+
|
|
588
|
+
Typically you create the required configuration sources in an environment class, e.g.
|
|
589
|
+
|
|
590
|
+
```python
|
|
591
|
+
@environment()
|
|
592
|
+
class SampleEnvironment:
|
|
593
|
+
# constructor
|
|
594
|
+
|
|
595
|
+
def __init__(self):
|
|
596
|
+
pass
|
|
597
|
+
|
|
598
|
+
@create()
|
|
599
|
+
def create_env_source(self) -> EnvConfigurationSource:
|
|
600
|
+
return EnvConfigurationSource()
|
|
601
|
+
|
|
602
|
+
@create()
|
|
603
|
+
def create_yaml_source(self) -> YamlConfigurationSource:
|
|
604
|
+
return YamlConfigurationSource("config.yaml")
|
|
605
|
+
```
|
|
606
|
+
|
|
492
607
|
# Reflection
|
|
493
608
|
|
|
494
609
|
As the library heavily relies on type introspection of classes and methods, a utility class `TypeDescriptor` is available that covers type information on classes.
|
|
@@ -505,7 +620,7 @@ it offers the methods
|
|
|
505
620
|
- `get_method(name: str, local=False)`
|
|
506
621
|
return a single either local or overall method
|
|
507
622
|
- `has_decorator(decorator: Callable) -> bool`
|
|
508
|
-
return `True`, if the class is decorated with the specified
|
|
623
|
+
return `True`, if the class is decorated with the specified decorator
|
|
509
624
|
- `get_decorator(decorator) -> Optional[DecoratorDescriptor]`
|
|
510
625
|
return a descriptor covering the decorator. In addition to the callable, it also stores the supplied args in the `args` property
|
|
511
626
|
|
|
@@ -513,9 +628,9 @@ The returned method descriptors offer:
|
|
|
513
628
|
- `param_types`
|
|
514
629
|
list of arg types
|
|
515
630
|
- `return_type`
|
|
516
|
-
the
|
|
631
|
+
the return type
|
|
517
632
|
- `has_decorator(decorator: Callable) -> bool`
|
|
518
|
-
return `True`, if the method is decorated with the specified
|
|
633
|
+
return `True`, if the method is decorated with the specified decorator
|
|
519
634
|
- `get_decorator(decorator: Callable) -> Optional[DecoratorDescriptor]`
|
|
520
635
|
return a descriptor covering the decorator. In addition to the callable, it also stores the supplied args in the `args` property
|
|
521
636
|
|
|
@@ -525,9 +640,9 @@ Whenver you define a custom decorator, you will need to register it accordingly.
|
|
|
525
640
|
|
|
526
641
|
**Example**:
|
|
527
642
|
```python
|
|
528
|
-
def transactional():
|
|
643
|
+
def transactional(scope):
|
|
529
644
|
def decorator(func):
|
|
530
|
-
Decorators.add(func, transactional)
|
|
645
|
+
Decorators.add(func, transactional, scope) # also add _all_ parameters in order to cache them
|
|
531
646
|
return func
|
|
532
647
|
|
|
533
648
|
return decorator
|
|
@@ -545,6 +660,12 @@ def transactional():
|
|
|
545
660
|
- added `@on_running()` callback
|
|
546
661
|
- added `thread` scope
|
|
547
662
|
|
|
663
|
+
**1.2.0**
|
|
664
|
+
|
|
665
|
+
- added `YamlConfigurationSource`
|
|
666
|
+
|
|
667
|
+
**1.2.1**
|
|
668
|
+
|
|
548
669
|
|
|
549
670
|
|
|
550
671
|
|