aspyx 1.4.0__py3-none-any.whl → 1.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of aspyx might be problematic. Click here for more details.

@@ -1,825 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: aspyx
3
- Version: 1.4.0
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
-
28
- Requires-Python: >=3.9
29
- Description-Content-Type: text/markdown
30
- License-File: LICENSE
31
- Provides-Extra: dev
32
- Requires-Dist: mkdocstrings-python; extra == "dev"
33
- Dynamic: license-file
34
-
35
- # aspyx
36
-
37
- ![Pylint](https://github.com/coolsamson7/aspyx/actions/workflows/pylint.yml/badge.svg)
38
- ![Build Status](https://github.com/coolsamson7/aspyx/actions/workflows/ci.yml/badge.svg)
39
- ![Python Versions](https://img.shields.io/badge/python-3.9%20|%203.10%20|%203.11%20|%203.12-blue)
40
- ![License](https://img.shields.io/github/license/coolsamson7/aspyx)
41
- ![coverage](https://img.shields.io/badge/coverage-94%25-brightgreen)
42
- [![PyPI](https://img.shields.io/pypi/v/aspyx)](https://pypi.org/project/aspyx/)
43
- [![Docs](https://img.shields.io/badge/docs-online-blue?logo=github)](https://coolsamson7.github.io/aspyx/)
44
-
45
- ## Table of Contents
46
-
47
- - [Motivation](#motivation)
48
- - [Introduction](#introduction)
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
78
-
79
- Especially the AOP integration definitely makes sense, as aspects on their own also usually require a context, which in a DI world is simply injected.
80
-
81
- # Introduction
82
-
83
- Aspyx is a lightweight Python library that provides both Dependency Injection (DI) and Aspect-Oriented Programming (AOP) support.
84
-
85
- The following DI features are supported
86
- - constructor and setter injection
87
- - injection of configuration variables
88
- - possibility to define custom injections
89
- - post processors
90
- - support for factory classes and methods
91
- - support for eager and lazy construction
92
- - support for scopes singleton, request and thread
93
- - possibility to add custom scopes
94
- - conditional registration of classes and factories ( aka profiles in spring )
95
- - lifecycle events methods `on_init`, `on_destroy`, `on_running`
96
- - bundling of injectable objects according to their module location including recursive imports and inheritance
97
- - instantiation of - possibly multiple - container instances - so called environments - that manage the lifecycle of related objects
98
- - hierarchical environments
99
-
100
- With respect to AOP:
101
- - support for before, around, after and error aspects
102
- - simple fluent interface to specify which methods are targeted by an aspect
103
- - sync and async method support
104
-
105
- The library is thread-safe and heavily performance optimized as most of the runtime information is precomputed and cached!
106
-
107
- Let's look at a simple example
108
-
109
- ```python
110
- from aspyx.di import injectable, on_init, on_destroy, environment, Environment
111
-
112
- @injectable()
113
- class Foo:
114
- def __init__(self):
115
- pass
116
-
117
- def hello(self, msg: str):
118
- print(f"hello {msg}")
119
-
120
- @injectable() # eager and singleton by default
121
- class Bar:
122
- def __init__(self, foo: Foo): # will inject the Foo dependency
123
- self.foo = foo
124
-
125
- @on_init() # a lifecycle callback called after the constructor and all possible injections
126
- def init(self):
127
- ...
128
-
129
-
130
- # this class will register all - specifically decorated - classes and factories in the own module
131
- # In this case Foo and Bar
132
-
133
- @environment()
134
- class SampleEnvironment:
135
- def __init__(self):
136
- pass
137
-
138
- # create environment
139
-
140
- environment = Environment(SampleEnvironment)
141
-
142
- # fetch an instance
143
-
144
- bar = env.get(Bar)
145
-
146
- bar.foo.hello("world")
147
- ```
148
-
149
- The concepts should be pretty familiar as well as the names as they are inspired by both Spring and Angular.
150
-
151
- Let's add some aspects...
152
-
153
- ```python
154
-
155
- @advice
156
- class SampleAdvice:
157
- def __init__(self): # could inject additional stuff
158
- pass
159
-
160
- @before(methods().named("hello").of_type(Foo))
161
- def call_before(self, invocation: Invocation):
162
- ...
163
-
164
- @error(methods().named("hello").of_type(Foo))
165
- def call_error(self, invocation: Invocation):
166
- ... # exception accessible in invocation.exception
167
-
168
- @around(methods().named("hello"))
169
- def call_around(self, invocation: Invocation):
170
- ...
171
- return invocation.proceed()
172
- ```
173
-
174
- The invocation parameter stores the complete context of the current execution, which are
175
- - the method
176
- - args
177
- - kwargs
178
- - the result
179
- - the possible caught error
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. The set of classes and instances is determined by a constructor argument that controls the class registry.
284
-
285
- **Example**:
286
- ```python
287
- @environment()
288
- class SampleEnvironment:
289
- def __init__(self):
290
- pass
291
-
292
- environment = Environment(SampleEnvironment)
293
- ```
294
-
295
- The default is that all eligible classes, that are implemented in the containing module or in any submodule will be managed.
296
- THe container will import the module and its children automatically. No need to add artificial import statements!
297
-
298
-
299
- By adding the parameter `features: list[str]`, it is possible to filter injectables by evaluating the corresponding `@conditional` decorators.
300
-
301
- **Example**:
302
- ```python
303
-
304
- @injectable()
305
- @conditional(requires_feature("dev"))
306
- class DevOnly:
307
- def __init__(self):
308
- pass
309
-
310
- @environment()
311
- class SampleEnvironmen():
312
- def __init__(self):
313
- pass
314
-
315
- environment = Environment(SampleEnvironment, features=["dev"])
316
- ```
317
-
318
-
319
- By adding an `imports: list[Type]` parameter, specifying other environment types, it will register the appropriate classes recursively.
320
-
321
- **Example**:
322
- ```python
323
- @environment()
324
- class SampleEnvironmen(imports=[OtherEnvironment]):
325
- def __init__(self):
326
- pass
327
- ```
328
-
329
- Another possibility is to add a parent environment as an `Environment` constructor parameter
330
-
331
- **Example**:
332
- ```python
333
- rootEnvironment = Environment(RootEnvironment)
334
- environment = Environment(SampleEnvironment, parent=rootEnvironment)
335
- ```
336
-
337
- 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.
338
-
339
- The method
340
-
341
- ```shutdown()```
342
-
343
- is used when a container is not needed anymore. It will call any `on_destroy()` of all created instances.
344
-
345
- ## Retrieval
346
-
347
- ```python
348
- def get(type: Type[T]) -> T
349
- ```
350
-
351
- is used to retrieve object instances. Depending on the respective scope it will return either cached instances or newly instantiated objects.
352
-
353
- The container knows about class hierarchies and is able to `get` base classes, as long as there is only one implementation.
354
-
355
- In case of ambiguities, it will throw an exception.
356
-
357
- 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. )
358
-
359
- # Instantiation logic
360
-
361
- Constructing a new instance involves a number of steps executed in this order
362
- - Constructor call
363
- the constructor is called with the resolved parameters
364
- - Advice injection
365
- All methods involving aspects are updated
366
- - Lifecycle methods
367
- different decorators can mark methods that should be called during the lifecycle ( here the construction ) of an instance.
368
- These are various injection possibilities as well as an optional final `on_init` call
369
- - PostProcessors
370
- Any custom post processors, that can add side effects or modify the instances
371
-
372
- ## Injection methods
373
-
374
- Different decorators are implemented, that call methods with computed values
375
-
376
- - `@inject`
377
- the method is called with all resolved parameter types ( same as the constructor call)
378
- - `@inject_environment`
379
- the method is called with the creating environment as a single parameter
380
- - `@inject_value()`
381
- the method is called with a resolved configuration value. Check the corresponding chapter
382
-
383
- **Example**:
384
- ```python
385
- @injectable()
386
- class Foo:
387
- def __init__(self):
388
- pass
389
-
390
- @inject_environment()
391
- def initEnvironment(self, env: Environment):
392
- ...
393
-
394
- @inject()
395
- def set(self, baz: Baz) -> None:
396
- ...
397
- ```
398
-
399
- ## Lifecycle methods
400
-
401
- It is possible to mark specific lifecyle methods.
402
- - `@on_init()`
403
- called after the constructor and all other injections.
404
- - `@on_running()`
405
- called after an environment has initialized completely ( e.g. created all eager objects ).
406
- - `@on_destroy()`
407
- called during shutdown of the environment
408
-
409
- ## Post Processors
410
-
411
- As part of the instantiation logic it is possible to define post processors that execute any side effect on newly created instances.
412
-
413
- **Example**:
414
- ```python
415
- @injectable()
416
- class SamplePostProcessor(PostProcessor):
417
- def process(self, instance: object, environment: Environment):
418
- print(f"created a {instance}")
419
- ```
420
-
421
- Any implementing class of `PostProcessor` that is eligible for injection will be called by passing the new instance.
422
-
423
- Note that a post processor will only handle instances _after_ its _own_ registration.
424
-
425
- 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!
426
-
427
- # Custom scopes
428
-
429
- As explained, available scopes are "singleton" and "request".
430
-
431
- 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`
432
-
433
- ```python
434
- def get(self, provider: AbstractInstanceProvider, environment: Environment, argProvider: Callable[[],list]):
435
- ```
436
-
437
- Arguments are:
438
- - `provider` the actual provider that will create an instance
439
- - `environment`the requesting environment
440
- - `argProvider` a function that can be called to compute the required arguments recursively
441
-
442
- **Example**: The simplified code of the singleton provider ( disregarding locking logic )
443
-
444
- ```python
445
- @scope("singleton")
446
- class SingletonScope(Scope):
447
- # constructor
448
-
449
- def __init__(self):
450
- super().__init__()
451
-
452
- self.value = None
453
-
454
- # override
455
-
456
- def get(self, provider: AbstractInstanceProvider, environment: Environment, argProvider: Callable[[],list]):
457
- if self.value is None:
458
- self.value = provider.create(environment, *argProvider())
459
-
460
- return self.value
461
- ```
462
-
463
- # AOP
464
-
465
- It is possible to define different aspects, that will be part of method calling flow. This logic fits nicely in the library, since the DI framework controls the instantiation logic and can handle aspects within a regular post processor.
466
-
467
- Advice classes need to be part of classes that add a `@advice()` decorator and can define methods that add aspects.
468
-
469
- ```python
470
- @advice
471
- class SampleAdvice:
472
- def __init__(self): # could inject dependencies
473
- pass
474
-
475
- @before(methods().named("hello").of_type(Foo))
476
- def call_before(self, invocation: Invocation):
477
- # arguments: invocation.args and invocation.kwargs
478
- ...
479
-
480
- @after(methods().named("hello").of_type(Foo))
481
- def call_after(self, invocation: Invocation):
482
- # arguments: invocation.args and invocation.kwargs
483
- ...
484
-
485
- @error(methods().named("hello").of_type(Foo))
486
- def call_error(self, invocation: Invocation):
487
- # error: invocation.exception
488
- ...
489
-
490
- @around(methods().named("hello"))
491
- def call_around(self, invocation: Invocation):
492
- try:
493
- ...
494
- return invocation.proceed() # will leave a result in invocation.result or invocation.exception in case of an exception
495
- finally:
496
- ...
497
- ```
498
-
499
- Different aspects - with the appropriate decorator - are possible:
500
- - `before`
501
- methods that will be executed _prior_ to the original method
502
- - `around`
503
- methods that will be executed _around_ to the original method allowing you to add side effects or even modify parameters.
504
- - `after`
505
- methods that will be executed _after_ to the original method
506
- - `error`
507
- methods that will be executed in case of a caught exception
508
-
509
- 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.
510
-
511
- All methods are expected to have single `Invocation` parameter, that stores
512
-
513
- - `func` the target function
514
- - `args` the supplied args ( including the `self` instance as the first element)
515
- - `kwargs` the keywords args
516
- - `result` the result ( initially `None`)
517
- - `exception` a possible caught exception ( initially `None`)
518
-
519
- ⚠️ **Attention:** It is essential for `around` methods to call `proceed()` on the invocation, which will call the next around method in the chain and finally the original method.
520
-
521
- If the `proceed` is called with parameters, they will replace the original parameters!
522
-
523
- **Example**: Parameter modifications
524
-
525
- ```python
526
- @around(methods().named("say"))
527
- def call_around(self, invocation: Invocation):
528
- return invocation.proceed(invocation.args[0], invocation.args[1] + "!") # 0 is self!
529
- ```
530
-
531
- The argument list to the corresponding decorators control which methods are targeted by the advice.
532
-
533
- A fluent interface is used describe the mapping.
534
- The parameters restrict either methods or classes and are constructed by a call to either `methods()` or `classes()`.
535
-
536
- Both add the fluent methods:
537
- - `of_type(type: Type)`
538
- defines the matching classes
539
- - `named(name: str)`
540
- defines method or class names
541
- - `that_are_async()`
542
- defines async methods
543
- - `matches(re: str)`
544
- defines regular expressions for methods or classes
545
- - `decorated_with(type: Type)`
546
- defines decorators on methods or classes
547
-
548
- The fluent methods `named`, `matches` and `of_type` can be called multiple times!
549
-
550
- **Example**: react on both `transactional` decorators on methods or classes
551
-
552
- ```python
553
- @advice
554
- class TransactionAdvice:
555
- def __init__(self):
556
- pass
557
-
558
- @around(methods().decorated_with(transactional), classes().decorated_with(transactional))
559
- def establish_transaction(self, invocation: Invocation):
560
- ...
561
- ```
562
-
563
- With respect to async methods, you need to make sure, to replace a `proceed()` with a `await proceed_async()` to have the overall chain async!
564
-
565
- # Threading
566
-
567
- A handy decorator `@synchronized` in combination with the respective advice is implemented that automatically synchronizes methods with a `RLock` associated with the instance.
568
-
569
- **Example**:
570
- ```python
571
- @injectable()
572
- class Foo:
573
- def __init__(self):
574
- pass
575
-
576
- @synchronized()
577
- def execute_synchronized(self):
578
- ...
579
- ```
580
-
581
- # Configuration
582
-
583
- It is possible to inject configuration values, by decorating methods with `@inject-value(<name>)` given a configuration key.
584
-
585
- ```python
586
- @injectable()
587
- class Foo:
588
- def __init__(self):
589
- pass
590
-
591
- @inject_value("HOME")
592
- def inject_home(self, os: str):
593
- ...
594
- ```
595
-
596
- If required type coercion will be applied.
597
-
598
- Configuration values are managed centrally using a `ConfigurationManager`, which aggregates values from various configuration sources that are defined as follows.
599
-
600
- ```python
601
- class ConfigurationSource(ABC):
602
- def __init__(self):
603
- pass
604
-
605
- ...
606
-
607
- @abstractmethod
608
- def load(self) -> dict:
609
- ```
610
-
611
- The `load` method is able to return a tree-like structure by returning a `dict`.
612
-
613
- Configuration variables are retrieved with the method
614
-
615
- ```python
616
- def get(self, path: str, type: Type[T], default : Optional[T]=None) -> T:
617
- ```
618
-
619
- - `path`
620
- a '.' separated path
621
- - `type`
622
- the desired type
623
- - `default`
624
- a default, if no value is registered
625
-
626
- Sources can be added dynamically by registering them.
627
-
628
- **Example**:
629
- ```python
630
- @injectable()
631
- class SampleConfigurationSource(ConfigurationSource):
632
- def __init__(self):
633
- super().__init__()
634
-
635
- def load(self) -> dict:
636
- return {
637
- "a": 1,
638
- "b": {
639
- "d": "2",
640
- "e": 3,
641
- "f": 4
642
- }
643
- }
644
- ```
645
-
646
- Two specific source are already implemented:
647
- - `EnvConfigurationSource`
648
- reads the os environment variables
649
- - `YamlConfigurationSource`
650
- reads a specific yaml file
651
-
652
- Typically you create the required configuration sources in an environment class, e.g.
653
-
654
- ```python
655
- @environment()
656
- class SampleEnvironment:
657
- # constructor
658
-
659
- def __init__(self):
660
- pass
661
-
662
- @create()
663
- def create_env_source(self) -> EnvConfigurationSource:
664
- return EnvConfigurationSource()
665
-
666
- @create()
667
- def create_yaml_source(self) -> YamlConfigurationSource:
668
- return YamlConfigurationSource("config.yaml")
669
- ```
670
-
671
- # Reflection
672
-
673
- As the library heavily relies on type introspection of classes and methods, a utility class `TypeDescriptor` is available that covers type information on classes.
674
-
675
- After being instantiated with
676
-
677
- ```python
678
- TypeDescriptor.for_type(<type>)
679
- ```
680
-
681
- it offers the methods
682
- - `get_methods(local=False)`
683
- return a list of either local or overall methods
684
- - `get_method(name: str, local=False)`
685
- return a single either local or overall method
686
- - `has_decorator(decorator: Callable) -> bool`
687
- return `True`, if the class is decorated with the specified decorator
688
- - `get_decorator(decorator) -> Optional[DecoratorDescriptor]`
689
- return a descriptor covering the decorator. In addition to the callable, it also stores the supplied args in the `args` property
690
-
691
- The returned method descriptors provide:
692
- - `param_types`
693
- list of arg types
694
- - `return_type`
695
- the return type
696
- - `has_decorator(decorator: Callable) -> bool`
697
- return `True`, if the method is decorated with the specified decorator
698
- - `get_decorator(decorator: Callable) -> Optional[DecoratorDescriptor]`
699
- return a descriptor covering the decorator. In addition to the callable, it also stores the supplied args in the `args` property
700
-
701
- The management of decorators in turn relies on another utility class `Decorators` that caches decorators.
702
-
703
- Whenver you define a custom decorator, you will need to register it accordingly.
704
-
705
- **Example**:
706
- ```python
707
- def transactional(scope):
708
- def decorator(func):
709
- Decorators.add(func, transactional, scope) # also add _all_ parameters in order to cache them
710
- return func
711
-
712
- return decorator
713
- ```
714
-
715
- # Exceptions
716
-
717
- The class `ExceptionManager` is used to collect dynamic handlers for specific exceptions and is able to dispatch to the concrete functions
718
- given a specific exception.
719
-
720
- The handlers are declared by annoting a class with `@exception_handler` and decorating specific methods with `@handle`
721
-
722
- **Example**:
723
- ```python
724
- class DerivedException(Exception):
725
- def __init__(self):
726
- pass
727
-
728
- @environment()
729
- class SampleEnvironment:
730
- # constructor
731
-
732
- def __init__(self):
733
- pass
734
-
735
- @create()
736
- def create_exception_manager(self) -> ExceptionManager:
737
- return ExceptionManager()
738
-
739
- @injectable()
740
- @exception_handler()
741
- class TestExceptionHandler:
742
- def __init__(self):
743
- pass
744
-
745
- @handle()
746
- def handle_derived_exception(self, exception: DerivedException):
747
- ExceptionManager.proceed()
748
-
749
- @handle()
750
- def handle_exception(self, exception: Exception):
751
- pass
752
-
753
- @handle()
754
- def handle_base_exception(self, exception: BaseException):
755
- pass
756
-
757
-
758
- @advice
759
- class ExceptionAdvice:
760
- def __init__(self, exceptionManager: ExceptionManager):
761
- self.exceptionManager = exceptionManager
762
-
763
- @error(methods().of_type(Service))
764
- def handle_error(self, invocation: Invocation):
765
- self.exceptionManager.handle(invocation.exception)
766
-
767
- environment = Environment(SampleEnvironment)
768
-
769
- environment.get(ExceptionManager).handle(DerivedException())
770
- ```
771
-
772
- The exception maanger will first call the most appropriate method.
773
- Any `ExceptionManager.proceed()` will in turn call the next most applicable method ( if available).
774
-
775
- Together with a simple around advice we can now add exception handling to any method:
776
-
777
- **Example**:
778
- ```python
779
- @injectable()
780
- class Service:
781
- def __init__(self):
782
- pass
783
-
784
- def throw(self):
785
- raise DerivedException()
786
-
787
- @advice
788
- class ExceptionAdvice:
789
- def __init__(self, exceptionManager: ExceptionManager):
790
- self.exceptionManager = exceptionManager
791
-
792
- @error(methods().of_type(Service))
793
- def handle_error(self, invocation: Invocation):
794
- self.exceptionManager.handle(invocation.exception)
795
- ```
796
-
797
- # Version History
798
-
799
- **1.0.1**
800
-
801
- - some internal refactorings
802
-
803
- **1.1.0**
804
-
805
- - added `@on_running()` callback
806
- - added `thread` scope
807
-
808
- **1.2.0**
809
-
810
- - added `YamlConfigurationSource`
811
-
812
- **1.3.0**
813
-
814
- - added `@conditional`
815
- - added support for `async` advices
816
-
817
-
818
- **1.4.0**
819
-
820
- - bugfixes
821
- - added `@ExceptionManager`
822
-
823
-
824
-
825
-