aspyx 1.3.0__tar.gz → 1.4.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 (37) hide show
  1. {aspyx-1.3.0/src/aspyx.egg-info → aspyx-1.4.0}/PKG-INFO +180 -58
  2. {aspyx-1.3.0 → aspyx-1.4.0}/README.md +177 -57
  3. {aspyx-1.3.0 → aspyx-1.4.0}/pyproject.toml +6 -1
  4. aspyx-1.4.0/src/aspyx/__init__.py +0 -0
  5. {aspyx-1.3.0 → aspyx-1.4.0}/src/aspyx/di/aop/aop.py +112 -87
  6. {aspyx-1.3.0 → aspyx-1.4.0}/src/aspyx/di/di.py +125 -24
  7. {aspyx-1.3.0 → aspyx-1.4.0}/src/aspyx/di/threading/synchronized.py +8 -5
  8. aspyx-1.4.0/src/aspyx/exception/__init__.py +10 -0
  9. aspyx-1.4.0/src/aspyx/exception/exception_manager.py +168 -0
  10. {aspyx-1.3.0 → aspyx-1.4.0}/src/aspyx/reflection/reflection.py +37 -1
  11. aspyx-1.4.0/src/aspyx/threading/__init__.py +10 -0
  12. aspyx-1.4.0/src/aspyx/threading/thread_local.py +47 -0
  13. {aspyx-1.3.0/src/aspyx/di → aspyx-1.4.0/src/aspyx}/util/stringbuilder.py +11 -0
  14. {aspyx-1.3.0 → aspyx-1.4.0/src/aspyx.egg-info}/PKG-INFO +180 -58
  15. {aspyx-1.3.0 → aspyx-1.4.0}/src/aspyx.egg-info/SOURCES.txt +10 -3
  16. aspyx-1.4.0/src/aspyx.egg-info/requires.txt +3 -0
  17. {aspyx-1.3.0 → aspyx-1.4.0}/tests/test_aop.py +38 -5
  18. {aspyx-1.3.0 → aspyx-1.4.0}/tests/test_di.py +6 -6
  19. aspyx-1.4.0/tests/test_exception_manager.py +74 -0
  20. {aspyx-1.3.0 → aspyx-1.4.0}/LICENSE +0 -0
  21. {aspyx-1.3.0 → aspyx-1.4.0}/setup.cfg +0 -0
  22. {aspyx-1.3.0 → aspyx-1.4.0}/src/aspyx/di/__init__.py +0 -0
  23. {aspyx-1.3.0 → aspyx-1.4.0}/src/aspyx/di/aop/__init__.py +0 -0
  24. {aspyx-1.3.0 → aspyx-1.4.0}/src/aspyx/di/configuration/__init__.py +0 -0
  25. {aspyx-1.3.0 → aspyx-1.4.0}/src/aspyx/di/configuration/configuration.py +0 -0
  26. {aspyx-1.3.0 → aspyx-1.4.0}/src/aspyx/di/configuration/env_configuration_source.py +0 -0
  27. {aspyx-1.3.0 → aspyx-1.4.0}/src/aspyx/di/configuration/yaml_configuration_source.py +0 -0
  28. {aspyx-1.3.0 → aspyx-1.4.0}/src/aspyx/di/threading/__init__.py +0 -0
  29. {aspyx-1.3.0 → aspyx-1.4.0}/src/aspyx/reflection/__init__.py +0 -0
  30. {aspyx-1.3.0 → aspyx-1.4.0}/src/aspyx/reflection/proxy.py +0 -0
  31. {aspyx-1.3.0/src/aspyx/di → aspyx-1.4.0/src/aspyx}/util/__init__.py +0 -0
  32. {aspyx-1.3.0 → aspyx-1.4.0}/src/aspyx.egg-info/dependency_links.txt +0 -0
  33. {aspyx-1.3.0 → aspyx-1.4.0}/src/aspyx.egg-info/top_level.txt +0 -0
  34. {aspyx-1.3.0 → aspyx-1.4.0}/tests/test_configuration.py +0 -0
  35. /aspyx-1.3.0/tests/test_di_cycle.py → /aspyx-1.4.0/tests/test_cycle.py +0 -0
  36. {aspyx-1.3.0 → aspyx-1.4.0}/tests/test_proxy.py +0 -0
  37. {aspyx-1.3.0 → aspyx-1.4.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.0
3
+ Version: 1.4.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
@@ -28,6 +28,8 @@ License: MIT License
28
28
  Requires-Python: >=3.9
29
29
  Description-Content-Type: text/markdown
30
30
  License-File: LICENSE
31
+ Provides-Extra: dev
32
+ Requires-Dist: mkdocstrings-python; extra == "dev"
31
33
  Dynamic: license-file
32
34
 
33
35
  # aspyx
@@ -38,8 +40,9 @@ Dynamic: license-file
38
40
  ![License](https://img.shields.io/github/license/coolsamson7/aspyx)
39
41
  ![coverage](https://img.shields.io/badge/coverage-94%25-brightgreen)
40
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/)
41
44
 
42
- ## Table of Contents
45
+ ## Table of Contents
43
46
 
44
47
  - [Motivation](#motivation)
45
48
  - [Introduction](#introduction)
@@ -58,43 +61,48 @@ Dynamic: license-file
58
61
  - [Post Processors](#post-processors)
59
62
  - [Custom scopes](#custom-scopes)
60
63
  - [AOP](#aop)
64
+ - [Threading](#threading)
61
65
  - [Configuration](#configuration)
62
66
  - [Reflection](#reflection)
67
+ - [Exceptions](#exceptions)
63
68
  - [Version History](#version-history)
64
69
 
65
70
  # Motivation
66
71
 
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".
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
70
73
 
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 :-)
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.
72
80
 
73
81
  # Introduction
74
82
 
75
- Aspyx is a small python libary, that adds support for both dependency injection and aop.
83
+ Aspyx is a lightweight Python library that provides both Dependency Injection (DI) and Aspect-Oriented Programming (AOP) support.
76
84
 
77
- The following di features are supported
85
+ The following DI features are supported
78
86
  - constructor and setter injection
87
+ - injection of configuration variables
79
88
  - possibility to define custom injections
80
89
  - post processors
81
90
  - support for factory classes and methods
82
- - support for eager construction
91
+ - support for eager and lazy construction
83
92
  - support for scopes singleton, request and thread
84
- - possibilty to add custom scopes
85
- - conditional registration of classes and factories
86
- - lifecycle events methods `on_init`, `on_destroy`
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`
87
96
  - 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 )
97
+ - instantiation of - possibly multiple - container instances - so called environments - that manage the lifecycle of related objects
90
98
  - hierarchical environments
91
99
 
92
- With respect to aop:
100
+ With respect to AOP:
93
101
  - support for before, around, after and error aspects
102
+ - simple fluent interface to specify which methods are targeted by an aspect
94
103
  - sync and async method support
95
- - `synchronized` decorator that adds locking to methods
96
104
 
97
- The library is thread-safe!
105
+ The library is thread-safe and heavily performance optimized as most of the runtime information is precomputed and cached!
98
106
 
99
107
  Let's look at a simple example
100
108
 
@@ -106,7 +114,7 @@ class Foo:
106
114
  def __init__(self):
107
115
  pass
108
116
 
109
- def hello(msg: str):
117
+ def hello(self, msg: str):
110
118
  print(f"hello {msg}")
111
119
 
112
120
  @injectable() # eager and singleton by default
@@ -127,16 +135,18 @@ class SampleEnvironment:
127
135
  def __init__(self):
128
136
  pass
129
137
 
130
- # go, forrest
138
+ # create environment
131
139
 
132
140
  environment = Environment(SampleEnvironment)
133
141
 
142
+ # fetch an instance
143
+
134
144
  bar = env.get(Bar)
135
145
 
136
146
  bar.foo.hello("world")
137
147
  ```
138
148
 
139
- The concepts should be pretty familiar as well as the names which are a combination of Spring and Angular names :-)
149
+ The concepts should be pretty familiar as well as the names as they are inspired by both Spring and Angular.
140
150
 
141
151
  Let's add some aspects...
142
152
 
@@ -149,17 +159,15 @@ class SampleAdvice:
149
159
 
150
160
  @before(methods().named("hello").of_type(Foo))
151
161
  def call_before(self, invocation: Invocation):
152
- print("before Foo.hello(...)")
162
+ ...
153
163
 
154
164
  @error(methods().named("hello").of_type(Foo))
155
165
  def call_error(self, invocation: Invocation):
156
- print("error Foo.hello(...)")
157
- print(invocation.exception)
166
+ ... # exception accessible in invocation.exception
158
167
 
159
168
  @around(methods().named("hello"))
160
169
  def call_around(self, invocation: Invocation):
161
- print("around Foo.hello()")
162
-
170
+ ...
163
171
  return invocation.proceed()
164
172
  ```
165
173
 
@@ -196,8 +204,8 @@ class Foo:
196
204
  def __init__(self):
197
205
  pass
198
206
  ```
199
- Please make sure, that the class defines a local constructor, as this is required to determine injected instances.
200
- All referenced types will be injected by the environemnt.
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.
201
209
 
202
210
  Only eligible types are allowed, of course!
203
211
 
@@ -285,6 +293,8 @@ environment = Environment(SampleEnvironment)
285
293
  ```
286
294
 
287
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
+
288
298
 
289
299
  By adding the parameter `features: list[str]`, it is possible to filter injectables by evaluating the corresponding `@conditional` decorators.
290
300
 
@@ -298,7 +308,7 @@ class DevOnly:
298
308
  pass
299
309
 
300
310
  @environment()
301
- class SampleEnvironmen()):
311
+ class SampleEnvironmen():
302
312
  def __init__(self):
303
313
  pass
304
314
 
@@ -311,7 +321,7 @@ By adding an `imports: list[Type]` parameter, specifying other environment types
311
321
  **Example**:
312
322
  ```python
313
323
  @environment()
314
- class SampleEnvironmen(imports=[OtherEnvironment])):
324
+ class SampleEnvironmen(imports=[OtherEnvironment]):
315
325
  def __init__(self):
316
326
  pass
317
327
  ```
@@ -344,7 +354,7 @@ The container knows about class hierarchies and is able to `get` base classes, a
344
354
 
345
355
  In case of ambiguities, it will throw an exception.
346
356
 
347
- Please be aware, 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. )
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. )
348
358
 
349
359
  # Instantiation logic
350
360
 
@@ -357,7 +367,7 @@ Constructing a new instance involves a number of steps executed in this order
357
367
  different decorators can mark methods that should be called during the lifecycle ( here the construction ) of an instance.
358
368
  These are various injection possibilities as well as an optional final `on_init` call
359
369
  - PostProcessors
360
- Any custom post processors, that can add isde effects or modify the instances
370
+ Any custom post processors, that can add side effects or modify the instances
361
371
 
362
372
  ## Injection methods
363
373
 
@@ -392,7 +402,7 @@ It is possible to mark specific lifecyle methods.
392
402
  - `@on_init()`
393
403
  called after the constructor and all other injections.
394
404
  - `@on_running()`
395
- called an environment has initialized all eager objects.
405
+ called after an environment has initialized completely ( e.g. created all eager objects ).
396
406
  - `@on_destroy()`
397
407
  called during shutdown of the environment
398
408
 
@@ -410,7 +420,7 @@ class SamplePostProcessor(PostProcessor):
410
420
 
411
421
  Any implementing class of `PostProcessor` that is eligible for injection will be called by passing the new instance.
412
422
 
413
- Please be aware, that a post processor will only handle instances _after_ its _own_ registration.
423
+ Note that a post processor will only handle instances _after_ its _own_ registration.
414
424
 
415
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!
416
426
 
@@ -427,7 +437,7 @@ def get(self, provider: AbstractInstanceProvider, environment: Environment, argP
427
437
  Arguments are:
428
438
  - `provider` the actual provider that will create an instance
429
439
  - `environment`the requesting environment
430
- - `argPovider` a function that can be called to compute the required arguments recursively
440
+ - `argProvider` a function that can be called to compute the required arguments recursively
431
441
 
432
442
  **Example**: The simplified code of the singleton provider ( disregarding locking logic )
433
443
 
@@ -452,52 +462,62 @@ class SingletonScope(Scope):
452
462
 
453
463
  # AOP
454
464
 
455
- 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.
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.
456
466
 
457
467
  Advice classes need to be part of classes that add a `@advice()` decorator and can define methods that add aspects.
458
468
 
459
469
  ```python
460
- @advice()
470
+ @advice
461
471
  class SampleAdvice:
462
472
  def __init__(self): # could inject dependencies
463
473
  pass
464
474
 
465
475
  @before(methods().named("hello").of_type(Foo))
466
476
  def call_before(self, invocation: Invocation):
467
- # arguments: invocation.args
468
- print("before Foo.hello(...)")
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
+ ...
469
484
 
470
485
  @error(methods().named("hello").of_type(Foo))
471
486
  def call_error(self, invocation: Invocation):
472
- print("error Foo.hello(...)")
473
- print(invocation.exception)
487
+ # error: invocation.exception
488
+ ...
474
489
 
475
490
  @around(methods().named("hello"))
476
491
  def call_around(self, invocation: Invocation):
477
- print("around Foo.hello()")
478
-
479
- return invocation.proceed() # will leave a result in invocation.result or invocation.exception in case of an exception
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
+ ...
480
497
  ```
481
498
 
482
499
  Different aspects - with the appropriate decorator - are possible:
483
500
  - `before`
484
501
  methods that will be executed _prior_ to the original method
485
502
  - `around`
486
- methods that will be executed _around_ to the original method giving it the possibility to add side effects or even change the parameters.
503
+ methods that will be executed _around_ to the original method allowing you to add side effects or even modify parameters.
487
504
  - `after`
488
505
  methods that will be executed _after_ to the original method
489
506
  - `error`
490
507
  methods that will be executed in case of a caught exception
491
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
+
492
511
  All methods are expected to have single `Invocation` parameter, that stores
493
512
 
494
513
  - `func` the target function
495
- - `args` the suppliued args
514
+ - `args` the supplied args ( including the `self` instance as the first element)
496
515
  - `kwargs` the keywords args
497
516
  - `result` the result ( initially `None`)
498
- - `exception` a possible caught excpetion ( 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.
499
520
 
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.
501
521
  If the `proceed` is called with parameters, they will replace the original parameters!
502
522
 
503
523
  **Example**: Parameter modifications
@@ -505,9 +525,7 @@ If the `proceed` is called with parameters, they will replace the original param
505
525
  ```python
506
526
  @around(methods().named("say"))
507
527
  def call_around(self, invocation: Invocation):
508
- args = [invocation.args[0],invocation.args[1] + "!"] # 0 is self!
509
-
510
- return invocation.proceed(*args)
528
+ return invocation.proceed(invocation.args[0], invocation.args[1] + "!") # 0 is self!
511
529
  ```
512
530
 
513
531
  The argument list to the corresponding decorators control which methods are targeted by the advice.
@@ -532,7 +550,7 @@ The fluent methods `named`, `matches` and `of_type` can be called multiple times
532
550
  **Example**: react on both `transactional` decorators on methods or classes
533
551
 
534
552
  ```python
535
- @injectable()
553
+ @advice
536
554
  class TransactionAdvice:
537
555
  def __init__(self):
538
556
  pass
@@ -542,9 +560,23 @@ class TransactionAdvice:
542
560
  ...
543
561
  ```
544
562
 
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!
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
546
566
 
547
- A handy decorator `@synchronized` is implemented that automatically synchronizes methods with a `RLock` associated with the instance.
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
+ ```
548
580
 
549
581
  # Configuration
550
582
 
@@ -561,9 +593,9 @@ class Foo:
561
593
  ...
562
594
  ```
563
595
 
564
- If required a coercion will be executed.
596
+ If required type coercion will be applied.
565
597
 
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.
598
+ Configuration values are managed centrally using a `ConfigurationManager`, which aggregates values from various configuration sources that are defined as follows.
567
599
 
568
600
  ```python
569
601
  class ConfigurationSource(ABC):
@@ -640,7 +672,7 @@ class SampleEnvironment:
640
672
 
641
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.
642
674
 
643
- After beeing instatiated with
675
+ After being instantiated with
644
676
 
645
677
  ```python
646
678
  TypeDescriptor.for_type(<type>)
@@ -656,7 +688,7 @@ it offers the methods
656
688
  - `get_decorator(decorator) -> Optional[DecoratorDescriptor]`
657
689
  return a descriptor covering the decorator. In addition to the callable, it also stores the supplied args in the `args` property
658
690
 
659
- The returned method descriptors offer:
691
+ The returned method descriptors provide:
660
692
  - `param_types`
661
693
  list of arg types
662
694
  - `return_type`
@@ -680,6 +712,87 @@ def transactional(scope):
680
712
  return decorator
681
713
  ```
682
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
+ ```
683
796
 
684
797
  # Version History
685
798
 
@@ -696,7 +809,16 @@ def transactional(scope):
696
809
 
697
810
  - added `YamlConfigurationSource`
698
811
 
699
- **1.2.1**
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`
700
822
 
701
823
 
702
824