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.

Files changed (31) hide show
  1. {aspyx-1.1.0/src/aspyx.egg-info → aspyx-1.3.0}/PKG-INFO +150 -29
  2. {aspyx-1.1.0 → aspyx-1.3.0}/README.md +149 -28
  3. {aspyx-1.1.0 → aspyx-1.3.0}/pyproject.toml +1 -1
  4. aspyx-1.3.0/src/aspyx/di/__init__.py +38 -0
  5. {aspyx-1.1.0 → aspyx-1.3.0}/src/aspyx/di/aop/aop.py +86 -4
  6. {aspyx-1.1.0 → aspyx-1.3.0}/src/aspyx/di/configuration/__init__.py +4 -1
  7. {aspyx-1.1.0 → aspyx-1.3.0}/src/aspyx/di/configuration/configuration.py +1 -51
  8. aspyx-1.3.0/src/aspyx/di/configuration/env_configuration_source.py +55 -0
  9. aspyx-1.3.0/src/aspyx/di/configuration/yaml_configuration_source.py +26 -0
  10. {aspyx-1.1.0 → aspyx-1.3.0}/src/aspyx/di/di.py +339 -207
  11. aspyx-1.3.0/src/aspyx/di/threading/__init__.py +11 -0
  12. aspyx-1.3.0/src/aspyx/di/threading/synchronized.py +46 -0
  13. {aspyx-1.1.0 → aspyx-1.3.0}/src/aspyx/reflection/reflection.py +33 -1
  14. {aspyx-1.1.0 → aspyx-1.3.0/src/aspyx.egg-info}/PKG-INFO +150 -29
  15. {aspyx-1.1.0 → aspyx-1.3.0}/src/aspyx.egg-info/SOURCES.txt +4 -0
  16. {aspyx-1.1.0 → aspyx-1.3.0}/tests/test_aop.py +49 -17
  17. {aspyx-1.1.0 → aspyx-1.3.0}/tests/test_configuration.py +31 -2
  18. {aspyx-1.1.0 → aspyx-1.3.0}/tests/test_di.py +88 -15
  19. {aspyx-1.1.0 → aspyx-1.3.0}/tests/test_di_cycle.py +2 -2
  20. aspyx-1.1.0/src/aspyx/di/__init__.py +0 -33
  21. {aspyx-1.1.0 → aspyx-1.3.0}/LICENSE +0 -0
  22. {aspyx-1.1.0 → aspyx-1.3.0}/setup.cfg +0 -0
  23. {aspyx-1.1.0 → aspyx-1.3.0}/src/aspyx/di/aop/__init__.py +0 -0
  24. {aspyx-1.1.0 → aspyx-1.3.0}/src/aspyx/di/util/__init__.py +0 -0
  25. {aspyx-1.1.0 → aspyx-1.3.0}/src/aspyx/di/util/stringbuilder.py +0 -0
  26. {aspyx-1.1.0 → aspyx-1.3.0}/src/aspyx/reflection/__init__.py +0 -0
  27. {aspyx-1.1.0 → aspyx-1.3.0}/src/aspyx/reflection/proxy.py +0 -0
  28. {aspyx-1.1.0 → aspyx-1.3.0}/src/aspyx.egg-info/dependency_links.txt +0 -0
  29. {aspyx-1.1.0 → aspyx-1.3.0}/src/aspyx.egg-info/top_level.txt +0 -0
  30. {aspyx-1.1.0 → aspyx-1.3.0}/tests/test_proxy.py +0 -0
  31. {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.1.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
 
@@ -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
- obejcts are created on every injection request
214
+ objects are created on every injection request
196
215
  - `thread`
197
- objects are cerated and cached with respect to the current thread.
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
- 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)`
@@ -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
- @value("OS")
482
- def inject_value(self, os: str):
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
- As a default environment variables are already supported.
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
- Other sources can be added dynamically by just registering them.
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 decrator
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 retur type
663
+ the return type
549
664
  - `has_decorator(decorator: Callable) -> bool`
550
- return `True`, if the method is decorated with the specified decrator
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
  ![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
 
@@ -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
- obejcts are created on every injection request
182
+ objects are created on every injection request
164
183
  - `thread`
165
- objects are cerated and cached with respect to the current thread.
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
- 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)`
@@ -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
- @value("OS")
450
- def inject_value(self, os: str):
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
- As a default environment variables are already supported.
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
- Other sources can be added dynamically by just registering them.
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 decrator
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 retur type
631
+ the return type
517
632
  - `has_decorator(decorator: Callable) -> bool`
518
- return `True`, if the method is decorated with the specified decrator
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
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "aspyx"
7
- version = "1.1.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"