aspyx 1.5.0__tar.gz → 1.5.2__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 (39) hide show
  1. aspyx-1.5.2/PKG-INFO +839 -0
  2. aspyx-1.5.2/README.md +807 -0
  3. {aspyx-1.5.0 → aspyx-1.5.2}/pyproject.toml +1 -1
  4. {aspyx-1.5.0 → aspyx-1.5.2}/src/aspyx/di/aop/aop.py +45 -9
  5. {aspyx-1.5.0 → aspyx-1.5.2}/src/aspyx/di/di.py +30 -73
  6. {aspyx-1.5.0 → aspyx-1.5.2}/src/aspyx/di/threading/synchronized.py +3 -1
  7. {aspyx-1.5.0 → aspyx-1.5.2}/src/aspyx/exception/exception_manager.py +19 -17
  8. {aspyx-1.5.0 → aspyx-1.5.2}/src/aspyx/reflection/proxy.py +10 -3
  9. {aspyx-1.5.0 → aspyx-1.5.2}/src/aspyx/reflection/reflection.py +8 -4
  10. {aspyx-1.5.0 → aspyx-1.5.2}/tests/test_aop.py +1 -0
  11. aspyx-1.5.2/tests/test_decorator.py +61 -0
  12. {aspyx-1.5.0 → aspyx-1.5.2}/tests/test_di.py +11 -2
  13. {aspyx-1.5.0 → aspyx-1.5.2}/tests/test_exception_manager.py +1 -0
  14. aspyx-1.5.0/PKG-INFO +0 -33
  15. aspyx-1.5.0/README.md +0 -1
  16. {aspyx-1.5.0 → aspyx-1.5.2}/.gitignore +0 -0
  17. {aspyx-1.5.0 → aspyx-1.5.2}/LICENSE +0 -0
  18. {aspyx-1.5.0 → aspyx-1.5.2}/src/aspyx/__init__.py +0 -0
  19. {aspyx-1.5.0 → aspyx-1.5.2}/src/aspyx/di/__init__.py +0 -0
  20. {aspyx-1.5.0 → aspyx-1.5.2}/src/aspyx/di/aop/__init__.py +0 -0
  21. {aspyx-1.5.0 → aspyx-1.5.2}/src/aspyx/di/configuration/__init__.py +0 -0
  22. {aspyx-1.5.0 → aspyx-1.5.2}/src/aspyx/di/configuration/configuration.py +0 -0
  23. {aspyx-1.5.0 → aspyx-1.5.2}/src/aspyx/di/configuration/env_configuration_source.py +0 -0
  24. {aspyx-1.5.0 → aspyx-1.5.2}/src/aspyx/di/configuration/yaml_configuration_source.py +0 -0
  25. {aspyx-1.5.0 → aspyx-1.5.2}/src/aspyx/di/threading/__init__.py +0 -0
  26. {aspyx-1.5.0 → aspyx-1.5.2}/src/aspyx/exception/__init__.py +0 -0
  27. {aspyx-1.5.0 → aspyx-1.5.2}/src/aspyx/reflection/__init__.py +0 -0
  28. {aspyx-1.5.0 → aspyx-1.5.2}/src/aspyx/threading/__init__.py +0 -0
  29. {aspyx-1.5.0 → aspyx-1.5.2}/src/aspyx/threading/thread_local.py +0 -0
  30. {aspyx-1.5.0 → aspyx-1.5.2}/src/aspyx/util/__init__.py +0 -0
  31. {aspyx-1.5.0 → aspyx-1.5.2}/src/aspyx/util/stringbuilder.py +0 -0
  32. {aspyx-1.5.0 → aspyx-1.5.2}/tests/config.yaml +0 -0
  33. {aspyx-1.5.0 → aspyx-1.5.2}/tests/config1.yaml +0 -0
  34. {aspyx-1.5.0 → aspyx-1.5.2}/tests/di_import.py +0 -0
  35. {aspyx-1.5.0 → aspyx-1.5.2}/tests/sub_import.py +0 -0
  36. {aspyx-1.5.0 → aspyx-1.5.2}/tests/test_configuration.py +0 -0
  37. {aspyx-1.5.0 → aspyx-1.5.2}/tests/test_cycle.py +0 -0
  38. {aspyx-1.5.0 → aspyx-1.5.2}/tests/test_proxy.py +0 -0
  39. {aspyx-1.5.0 → aspyx-1.5.2}/tests/test_reflection.py +0 -0
aspyx-1.5.2/PKG-INFO ADDED
@@ -0,0 +1,839 @@
1
+ Metadata-Version: 2.4
2
+ Name: aspyx
3
+ Version: 1.5.2
4
+ Summary: A DI and AOP library for Python
5
+ Author-email: Andreas Ernst <andreas.ernst7@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 Andreas Ernst
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ License-File: LICENSE
28
+ Requires-Python: >=3.9
29
+ Requires-Dist: python-dotenv~=1.1.0
30
+ Requires-Dist: pyyaml~=6.0.2
31
+ Description-Content-Type: text/markdown
32
+
33
+ # aspyx
34
+
35
+ ![Pylint](https://github.com/coolsamson7/aspyx/actions/workflows/pylint.yml/badge.svg)
36
+ ![Build Status](https://github.com/coolsamson7/aspyx/actions/workflows/ci.yml/badge.svg)
37
+ ![Python Versions](https://img.shields.io/badge/python-3.9%20|%203.10%20|%203.11%20|%203.12-blue)
38
+ ![License](https://img.shields.io/github/license/coolsamson7/aspyx)
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/)
41
+ [![Docs](https://img.shields.io/badge/docs-online-blue?logo=github)](https://coolsamson7.github.io/aspyx/index/introduction)
42
+
43
+ ![image](https://github.com/user-attachments/assets/e808210a-b1a4-4fd0-93f1-b5f9845fa520)
44
+
45
+ ## Table of Contents
46
+
47
+ - [Motivation](#motivation)
48
+ - [Overview](#overview)
49
+ - [Installation](#installation)
50
+ - [Registration](#registration)
51
+ - [Class](#class)
52
+ - [Class Factory](#class-factory)
53
+ - [Method](#method)
54
+ - [Conditional](#conditional)
55
+ - [Environment](#environment)
56
+ - [Definition](#definition)
57
+ - [Retrieval](#retrieval)
58
+ - [Instantiation logic](#instantiation-logic)
59
+ - [Injection Methods](#injection-methods)
60
+ - [Lifecycle Methods](#lifecycle-methods)
61
+ - [Post Processors](#post-processors)
62
+ - [Custom scopes](#custom-scopes)
63
+ - [AOP](#aop)
64
+ - [Threading](#threading)
65
+ - [Configuration](#configuration)
66
+ - [Reflection](#reflection)
67
+ - [Exceptions](#exceptions)
68
+ - [Version History](#version-history)
69
+
70
+ # Motivation
71
+
72
+ 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
+
74
+ - bring both di and AOP features together in a lightweight library,
75
+ - be as minimal invasive as possible,
76
+ - 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 and only requires a minimum initial learning curve
78
+
79
+ The AOP integration, in particular, makes a lot of sense because:
80
+
81
+ - Aspects typically require context, which is naturally provided through DI,
82
+ - And they should only apply to objects managed by the container, rather than acting globally.
83
+
84
+ # Overview
85
+
86
+ Aspyx is a lightweight - still only about 2K LOC - Python library that provides both Dependency Injection (DI) and Aspect-Oriented Programming (AOP) support.
87
+
88
+ The following DI features are supported
89
+ - constructor and setter injection
90
+ - injection of configuration variables
91
+ - possibility to define custom injections
92
+ - post processors
93
+ - support for factory classes and methods
94
+ - support for eager and lazy construction
95
+ - support for scopes "singleton", "request" and "thread"
96
+ - possibility to add custom scopes
97
+ - conditional registration of classes and factories ( aka profiles in spring )
98
+ - lifecycle events methods `on_init`, `on_destroy`, `on_running`
99
+ - Automatic discovery and bundling of injectable objects based on their module location, including support for recursive imports
100
+ - Instantiation of one or possible more isolated container instances — called environments — each managing the lifecycle of a related set of objects,
101
+ - Support for hierarchical environments, enabling structured scoping and layered object management.
102
+
103
+ With respect to AOP:
104
+ - support for before, around, after and error aspects
105
+ - simple fluent interface to specify which methods are targeted by an aspect
106
+ - sync and async method support
107
+
108
+ The library is thread-safe and heavily performance optimized as most of the runtime information is precomputed and cached!
109
+
110
+ Let's look at a simple example
111
+
112
+ ```python
113
+ from aspyx.di import injectable, on_init, on_destroy, module, Environment
114
+
115
+
116
+ @injectable()
117
+ class Foo:
118
+ def __init__(self):
119
+ pass
120
+
121
+ def hello(self, msg: str):
122
+ print(f"hello {msg}")
123
+
124
+
125
+ @injectable() # eager and singleton by default
126
+ class Bar:
127
+ def __init__(self, foo: Foo): # will inject the Foo dependency
128
+ self.foo = foo
129
+
130
+ @on_init() # a lifecycle callback called after the constructor and all possible injections
131
+ def init(self):
132
+ ...
133
+
134
+
135
+ # this class will discover and manage all - specifically decorated - classes and factories that are part of the own module
136
+
137
+ @module()
138
+ class SampleModule:
139
+ def __init__(self):
140
+ pass
141
+
142
+
143
+ # create environment
144
+
145
+ environment = Environment(SampleModule)
146
+
147
+ # fetch an instance
148
+
149
+ bar = env.get(Bar)
150
+
151
+ bar.foo.hello("world")
152
+ ```
153
+
154
+ The concepts should be pretty familiar as well as the names as they are inspired by both Spring and Angular.
155
+
156
+ Let's add some aspects...
157
+
158
+ ```python
159
+
160
+ @advice
161
+ class SampleAdvice:
162
+ def __init__(self): # could inject additional stuff
163
+ pass
164
+
165
+ @before(methods().named("hello").of_type(Foo))
166
+ def call_before(self, invocation: Invocation):
167
+ ...
168
+
169
+ @error(methods().named("hello").of_type(Foo))
170
+ def call_error(self, invocation: Invocation):
171
+ ... # exception accessible in invocation.exception
172
+
173
+ @around(methods().named("hello"))
174
+ def call_around(self, invocation: Invocation):
175
+ ...
176
+ return invocation.proceed()
177
+ ```
178
+
179
+ 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
+
181
+ Let's look at the details
182
+
183
+ # Installation
184
+
185
+ Just install from PyPI with
186
+
187
+ `pip install aspyx`
188
+
189
+ The library is tested with all Python version >= 3.9
190
+
191
+ # Registration
192
+
193
+ Different mechanisms are available that make classes eligible for injection
194
+
195
+ ## Class
196
+
197
+ Any class annotated with `@injectable` is eligible for injection
198
+
199
+ **Example**:
200
+
201
+ ```python
202
+ @injectable()
203
+ class Foo:
204
+ def __init__(self):
205
+ pass
206
+ ```
207
+ ⚠️ **Attention:** Please make sure, that the class defines a local constructor, as this is _required_ to determine injected instances.
208
+ All referenced types will be injected by the environment.
209
+
210
+ Only eligible types are allowed, of course!
211
+
212
+ The decorator accepts the keyword arguments
213
+ - `eager : boolean`
214
+ if `True`, the container will create the instances automatically while booting the environment. This is the default.
215
+ - `scope: str`
216
+ the name of a - registered - scope which will determine how often instances will be created.
217
+
218
+ The following scopes are implemented out of the box:
219
+ - `singleton`
220
+ objects are created once inside an environment and cached. This is the default.
221
+ - `request`
222
+ objects are created on every injection request
223
+ - `thread`
224
+ objects are created and cached with respect to the current thread.
225
+
226
+ Other scopes - e.g. session related scopes - can be defined dynamically. Please check the corresponding chapter.
227
+
228
+ ## Class Factory
229
+
230
+ Classes that implement the `Factory` base class and are annotated with `@factory` will register the appropriate classes returned by the `create` method.
231
+
232
+ **Example**:
233
+ ```python
234
+ @factory()
235
+ class TestFactory(Factory[Foo]):
236
+ def __init__(self):
237
+ pass
238
+
239
+ def create(self) -> Foo:
240
+ return Foo()
241
+ ```
242
+
243
+ As in `@injectable`, the same arguments are possible.
244
+
245
+ ## Method
246
+
247
+ Any `injectable` can define methods decorated with `@create()`, that will create appropriate instances.
248
+
249
+ **Example**:
250
+ ```python
251
+ @injectable()
252
+ class Foo:
253
+ def __init__(self):
254
+ pass
255
+
256
+ @create(scope="request")
257
+ def create(self) -> Baz:
258
+ return Baz()
259
+ ```
260
+
261
+ The same arguments as in `@injectable` are possible.
262
+
263
+ ## Conditional
264
+
265
+ All `@injectable` declarations can be supplemented with
266
+
267
+ ```python
268
+ @conditional(<condition>, ..., <condition>)
269
+ ```
270
+
271
+ decorators that act as filters in the context of an environment.
272
+
273
+ Valid conditions are created by:
274
+ - `requires_class(clazz: Type)`
275
+ the injectable is valid, if the specified class is registered as well.
276
+ - `requires_feature(feature: str)`
277
+ the injectable is valid, if the environment defines the specified feature.
278
+
279
+ # Environment
280
+
281
+ ## Definition
282
+
283
+ An `Environment` is the container that manages the lifecycle of objects.
284
+ The set of classes and instances is determined by a
285
+ constructor type argument called `module`.
286
+
287
+ **Example**:
288
+ ```python
289
+ @module()
290
+ class SampleModule:
291
+ def __init__(self):
292
+ pass
293
+ ```
294
+
295
+ 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.
296
+ All eligible classes, that are implemented in the containing module or in any submodule will be managed.
297
+
298
+ In a second step the real container - the environment - is created based on a module:
299
+
300
+ ```python
301
+ environment = Environment(SampleModule, features=["dev"])
302
+ ```
303
+
304
+ By adding the parameter `features: list[str]`, it is possible to filter injectables by evaluating the corresponding `@conditional` decorators.
305
+
306
+ **Example**:
307
+ ```python
308
+
309
+ @injectable()
310
+ @conditional(requires_feature("dev"))
311
+ class DevOnly:
312
+ def __init__(self):
313
+ pass
314
+
315
+ @module()
316
+ class SampleModule():
317
+ def __init__(self):
318
+ pass
319
+
320
+ environment = Environment(SampleModule, features=["dev"])
321
+ ```
322
+
323
+
324
+ By adding an `imports: list[Type]` parameter, specifying other module types, it will register the appropriate classes recursively.
325
+
326
+ **Example**:
327
+ ```python
328
+ @module()
329
+ class SampleModule(imports=[OtherModule]):
330
+ def __init__(self):
331
+ pass
332
+ ```
333
+
334
+ Another possibility is to add a parent environment as an `Environment` constructor parameter
335
+
336
+ **Example**:
337
+ ```python
338
+ rootEnvironment = Environment(RootModule)
339
+
340
+ environment = Environment(SampleModule, parent=rootEnvironment)
341
+ ```
342
+
343
+ 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.
344
+
345
+ The method
346
+
347
+ ```shutdown()```
348
+
349
+ is used when a container is not needed anymore. It will call any `on_destroy()` of all created instances.
350
+
351
+ ## Retrieval
352
+
353
+ ```python
354
+ def get(type: Type[T]) -> T
355
+ ```
356
+
357
+ is used to retrieve object instances. Depending on the respective scope it will return either cached instances or newly instantiated objects.
358
+
359
+ The container knows about class hierarchies and is able to `get` base classes, as long as there is only one implementation.
360
+
361
+ In case of ambiguities, it will throw an exception.
362
+
363
+ Note that a base class are not _required_ to be annotated with `@injectable`, as this would mean, that it could be created on its own as well. ( Which is possible as well, btw. )
364
+
365
+ # Instantiation logic
366
+
367
+ Constructing a new instance involves a number of steps executed in this order
368
+ - Constructor call
369
+ the constructor is called with the resolved parameters
370
+ - Advice injection
371
+ All methods involving aspects are updated
372
+ - Lifecycle methods
373
+ different decorators can mark methods that should be called during the lifecycle ( here the construction ) of an instance.
374
+ These are various injection possibilities as well as an optional final `on_init` call
375
+ - PostProcessors
376
+ Any custom post processors, that can add side effects or modify the instances
377
+
378
+ ## Injection methods
379
+
380
+ Different decorators are implemented, that call methods with computed values
381
+
382
+ - `@inject`
383
+ the method is called with all resolved parameter types ( same as the constructor call)
384
+ - `@inject_environment`
385
+ the method is called with the creating environment as a single parameter
386
+ - `@inject_value()`
387
+ the method is called with a resolved configuration value. Check the corresponding chapter
388
+
389
+ **Example**:
390
+ ```python
391
+ @injectable()
392
+ class Foo:
393
+ def __init__(self):
394
+ pass
395
+
396
+ @inject_environment()
397
+ def initEnvironment(self, env: Environment):
398
+ ...
399
+
400
+ @inject()
401
+ def set(self, baz: Baz) -> None:
402
+ ...
403
+ ```
404
+
405
+ ## Lifecycle methods
406
+
407
+ It is possible to mark specific lifecyle methods.
408
+ - `@on_init()`
409
+ called after the constructor and all other injections.
410
+ - `@on_running()`
411
+ called after an environment has initialized completely ( e.g. created all eager objects ).
412
+ - `@on_destroy()`
413
+ called during shutdown of the environment
414
+
415
+ ## Post Processors
416
+
417
+ As part of the instantiation logic it is possible to define post processors that execute any side effect on newly created instances.
418
+
419
+ **Example**:
420
+ ```python
421
+ @injectable()
422
+ class SamplePostProcessor(PostProcessor):
423
+ def process(self, instance: object, environment: Environment):
424
+ print(f"created a {instance}")
425
+ ```
426
+
427
+ Any implementing class of `PostProcessor` that is eligible for injection will be called by passing the new instance.
428
+
429
+ Note that a post processor will only handle instances _after_ its _own_ registration.
430
+
431
+ As injectables within a single file will be handled in the order as they are declared, a post processor will only take effect for all classes after its declaration!
432
+
433
+ # Custom scopes
434
+
435
+ As explained, available scopes are "singleton" and "request".
436
+
437
+ It is easily possible to add custom scopes by inheriting the base-class `Scope`, decorating the class with `@scope(<name>)` and overriding the method `get`
438
+
439
+ ```python
440
+ def get(self, provider: AbstractInstanceProvider, environment: Environment, argProvider: Callable[[],list]):
441
+ ```
442
+
443
+ Arguments are:
444
+ - `provider` the actual provider that will create an instance
445
+ - `environment`the requesting environment
446
+ - `argProvider` a function that can be called to compute the required arguments recursively
447
+
448
+ **Example**: The simplified code of the singleton provider ( disregarding locking logic )
449
+
450
+ ```python
451
+ @scope("singleton")
452
+ class SingletonScope(Scope):
453
+ # constructor
454
+
455
+ def __init__(self):
456
+ super().__init__()
457
+
458
+ self.value = None
459
+
460
+ # override
461
+
462
+ def get(self, provider: AbstractInstanceProvider, environment: Environment, argProvider: Callable[[],list]):
463
+ if self.value is None:
464
+ self.value = provider.create(environment, *argProvider())
465
+
466
+ return self.value
467
+ ```
468
+
469
+ # AOP
470
+
471
+ 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.
472
+
473
+ On the other hand, advices are also regular DI objects, as they will usually require some kind of - injected - context.
474
+
475
+ Advices are regular classes decorated with `@advice` that define aspect methods.
476
+
477
+ ```python
478
+ @advice
479
+ class SampleAdvice:
480
+ def __init__(self): # could inject dependencies
481
+ pass
482
+
483
+ @before(methods().named("hello").of_type(Foo))
484
+ def call_before(self, invocation: Invocation):
485
+ # arguments: invocation.args and invocation.kwargs
486
+ ...
487
+
488
+ @after(methods().named("hello").of_type(Foo))
489
+ def call_after(self, invocation: Invocation):
490
+ # arguments: invocation.args and invocation.kwargs
491
+ ...
492
+
493
+ @error(methods().named("hello").of_type(Foo))
494
+ def call_error(self, invocation: Invocation):
495
+ # error: invocation.exception
496
+ ...
497
+
498
+ @around(methods().named("hello"))
499
+ def call_around(self, invocation: Invocation):
500
+ try:
501
+ ...
502
+ return invocation.proceed() # will leave a result in invocation.result or invocation.exception in case of an exception
503
+ finally:
504
+ ...
505
+ ```
506
+
507
+ Different aspects - with the appropriate decorator - are possible:
508
+ - `before`
509
+ methods that will be executed _prior_ to the original method
510
+ - `around`
511
+ methods that will be executed _around_ to the original method allowing you to add side effects or even modify parameters.
512
+ - `after`
513
+ methods that will be executed _after_ to the original method
514
+ - `error`
515
+ methods that will be executed in case of a caught exception
516
+
517
+ The different aspects can be supplemented with an `@order(<prio>)` decorator that controls the execution order based on the passed number. Smaller values get executed first.
518
+
519
+ All methods are expected to have single `Invocation` parameter, that stores
520
+
521
+ - `func` the target function
522
+ - `args` the supplied args ( including the `self` instance as the first element)
523
+ - `kwargs` the keywords args
524
+ - `result` the result ( initially `None`)
525
+ - `exception` a possible caught exception ( initially `None`)
526
+
527
+ ⚠️ **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.
528
+
529
+ If the `proceed` is called with parameters, they will replace the original parameters!
530
+
531
+ **Example**: Parameter modifications
532
+
533
+ ```python
534
+ @around(methods().named("say"))
535
+ def call_around(self, invocation: Invocation):
536
+ return invocation.proceed(invocation.args[0], invocation.args[1] + "!") # 0 is self!
537
+ ```
538
+
539
+ The argument list to the corresponding decorators control which methods are targeted by the advice.
540
+
541
+ A fluent interface is used describe the mapping.
542
+ The parameters restrict either methods or classes and are constructed by a call to either `methods()` or `classes()`.
543
+
544
+ Both add the fluent methods:
545
+ - `of_type(type: Type)`
546
+ defines the matching classes
547
+ - `named(name: str)`
548
+ defines method or class names
549
+ - `that_are_async()`
550
+ defines async methods
551
+ - `matches(re: str)`
552
+ defines regular expressions for methods or classes
553
+ - `decorated_with(type: Type)`
554
+ defines decorators on methods or classes
555
+
556
+ The fluent methods `named`, `matches` and `of_type` can be called multiple times!
557
+
558
+ **Example**: react on both `transactional` decorators on methods or classes
559
+
560
+ ```python
561
+ @advice
562
+ class TransactionAdvice:
563
+ def __init__(self):
564
+ pass
565
+
566
+ @around(methods().decorated_with(transactional), classes().decorated_with(transactional))
567
+ def establish_transaction(self, invocation: Invocation):
568
+ ...
569
+ ```
570
+
571
+ 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!
572
+
573
+ ## Advice Lifecycle and visibility.
574
+
575
+ Advices are always part of a specific environment, and only modify methods of objects managed by exactly this environment.
576
+
577
+ 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.
578
+
579
+ # Threading
580
+
581
+ A handy decorator `@synchronized` in combination with the respective advice is implemented that automatically synchronizes methods with a `RLock` associated with the instance.
582
+
583
+ **Example**:
584
+ ```python
585
+ @injectable()
586
+ class Foo:
587
+ def __init__(self):
588
+ pass
589
+
590
+ @synchronized()
591
+ def execute_synchronized(self):
592
+ ...
593
+ ```
594
+
595
+ # Configuration
596
+
597
+ It is possible to inject configuration values, by decorating methods with `@inject-value(<name>)` given a configuration key.
598
+
599
+ ```python
600
+ @injectable()
601
+ class Foo:
602
+ def __init__(self):
603
+ pass
604
+
605
+ @inject_value("HOME")
606
+ def inject_home(self, os: str):
607
+ ...
608
+ ```
609
+
610
+ If required type coercion will be applied.
611
+
612
+ Configuration values are managed centrally using a `ConfigurationManager`, which aggregates values from various configuration sources that are defined as follows.
613
+
614
+ ```python
615
+ class ConfigurationSource(ABC):
616
+ def __init__(self):
617
+ pass
618
+
619
+ ...
620
+
621
+ @abstractmethod
622
+ def load(self) -> dict:
623
+ ```
624
+
625
+ The `load` method is able to return a tree-like structure by returning a `dict`.
626
+
627
+ Configuration variables are retrieved with the method
628
+
629
+ ```python
630
+ def get(self, path: str, type: Type[T], default : Optional[T]=None) -> T:
631
+ ```
632
+
633
+ - `path`
634
+ a '.' separated path
635
+ - `type`
636
+ the desired type
637
+ - `default`
638
+ a default, if no value is registered
639
+
640
+ Sources can be added dynamically by registering them.
641
+
642
+ **Example**:
643
+ ```python
644
+ @injectable()
645
+ class SampleConfigurationSource(ConfigurationSource):
646
+ def __init__(self):
647
+ super().__init__()
648
+
649
+ def load(self) -> dict:
650
+ return {
651
+ "a": 1,
652
+ "b": {
653
+ "d": "2",
654
+ "e": 3,
655
+ "f": 4
656
+ }
657
+ }
658
+ ```
659
+
660
+ Two specific source are already implemented:
661
+ - `EnvConfigurationSource`
662
+ reads the os environment variables
663
+ - `YamlConfigurationSource`
664
+ reads a specific yaml file
665
+
666
+ Typically you create the required configuration sources in an environment class, e.g.
667
+
668
+ ```python
669
+ @module()
670
+ class SampleModule:
671
+ # constructor
672
+
673
+ def __init__(self):
674
+ pass
675
+
676
+ @create()
677
+ def create_env_source(self) -> EnvConfigurationSource:
678
+ return EnvConfigurationSource()
679
+
680
+ @create()
681
+ def create_yaml_source(self) -> YamlConfigurationSource:
682
+ return YamlConfigurationSource("config.yaml")
683
+ ```
684
+
685
+ # Reflection
686
+
687
+ As the library heavily relies on type introspection of classes and methods, a utility class `TypeDescriptor` is available that covers type information on classes.
688
+
689
+ After being instantiated with
690
+
691
+ ```python
692
+ TypeDescriptor.for_type(<type>)
693
+ ```
694
+
695
+ it offers the methods
696
+ - `get_methods(local=False)`
697
+ return a list of either local or overall methods
698
+ - `get_method(name: str, local=False)`
699
+ return a single either local or overall method
700
+ - `has_decorator(decorator: Callable) -> bool`
701
+ return `True`, if the class is decorated with the specified decorator
702
+ - `get_decorator(decorator) -> Optional[DecoratorDescriptor]`
703
+ return a descriptor covering the decorator. In addition to the callable, it also stores the supplied args in the `args` property
704
+
705
+ The returned method descriptors provide:
706
+ - `param_types`
707
+ list of arg types
708
+ - `return_type`
709
+ the return type
710
+ - `has_decorator(decorator: Callable) -> bool`
711
+ return `True`, if the method is decorated with the specified decorator
712
+ - `get_decorator(decorator: Callable) -> Optional[DecoratorDescriptor]`
713
+ return a descriptor covering the decorator. In addition to the callable, it also stores the supplied args in the `args` property
714
+
715
+ The management of decorators in turn relies on another utility class `Decorators` that caches decorators.
716
+
717
+ Whenver you define a custom decorator, you will need to register it accordingly.
718
+
719
+ **Example**:
720
+ ```python
721
+ def transactional(scope):
722
+ def decorator(func):
723
+ Decorators.add(func, transactional, scope) # also add _all_ parameters in order to cache them
724
+ return func
725
+
726
+ return decorator
727
+ ```
728
+
729
+ # Exceptions
730
+
731
+ The class `ExceptionManager` is used to collect dynamic handlers for specific exceptions and is able to dispatch to the concrete functions
732
+ given a specific exception.
733
+
734
+ The handlers are declared by annoting a class with `@exception_handler` and decorating specific methods with `@handle`
735
+
736
+ **Example**:
737
+ ```python
738
+ class DerivedException(Exception):
739
+ def __init__(self):
740
+ pass
741
+
742
+ @module()
743
+ class SampleModule:
744
+ # constructor
745
+
746
+ def __init__(self):
747
+ pass
748
+
749
+ @create()
750
+ def create_exception_manager(self) -> ExceptionManager:
751
+ return ExceptionManager()
752
+
753
+ @injectable()
754
+ @exception_handler()
755
+ class TestExceptionHandler:
756
+ def __init__(self):
757
+ pass
758
+
759
+ @handle()
760
+ def handle_derived_exception(self, exception: DerivedException):
761
+ ExceptionManager.proceed()
762
+
763
+ @handle()
764
+ def handle_exception(self, exception: Exception):
765
+ pass
766
+
767
+ @handle()
768
+ def handle_base_exception(self, exception: BaseException):
769
+ pass
770
+
771
+
772
+ @advice
773
+ class ExceptionAdvice:
774
+ def __init__(self, exceptionManager: ExceptionManager):
775
+ self.exceptionManager = exceptionManager
776
+
777
+ @error(methods().of_type(Service))
778
+ def handle_error(self, invocation: Invocation):
779
+ self.exceptionManager.handle(invocation.exception)
780
+
781
+ environment = Environment(SampleEnvironment)
782
+
783
+ environment.get(ExceptionManager).handle(DerivedException())
784
+ ```
785
+
786
+ The exception maanger will first call the most appropriate method.
787
+ Any `ExceptionManager.proceed()` will in turn call the next most applicable method ( if available).
788
+
789
+ Together with a simple around advice we can now add exception handling to any method:
790
+
791
+ **Example**:
792
+ ```python
793
+ @injectable()
794
+ class Service:
795
+ def __init__(self):
796
+ pass
797
+
798
+ def throw(self):
799
+ raise DerivedException()
800
+
801
+ @advice
802
+ class ExceptionAdvice:
803
+ def __init__(self, exceptionManager: ExceptionManager):
804
+ self.exceptionManager = exceptionManager
805
+
806
+ @error(methods().of_type(Service))
807
+ def handle_error(self, invocation: Invocation):
808
+ self.exceptionManager.handle(invocation.exception)
809
+ ```
810
+
811
+ # Version History
812
+
813
+ **1.0.1**
814
+
815
+ - some internal refactorings
816
+
817
+ **1.1.0**
818
+
819
+ - added `@on_running()` callback
820
+ - added `thread` scope
821
+
822
+ **1.2.0**
823
+
824
+ - added `YamlConfigurationSource`
825
+
826
+ **1.3.0**
827
+
828
+ - added `@conditional`
829
+ - added support for `async` advices
830
+
831
+
832
+ **1.4.0**
833
+
834
+ - bugfixes
835
+ - added `@ExceptionManager`
836
+
837
+ **1.4.1**
838
+
839
+ - mkdocs