aspyx 1.2.0__py3-none-any.whl → 1.3.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.

@@ -0,0 +1,11 @@
1
+ """
2
+ threading utilities
3
+ """
4
+ from .synchronized import synchronized, SynchronizeAdvice
5
+
6
+ imports = [synchronized, SynchronizeAdvice]
7
+
8
+ __all__ = [
9
+ "synchronized",
10
+ "SynchronizeAdvice",
11
+ ]
@@ -0,0 +1,46 @@
1
+ """
2
+ Synchronized decorator and advice
3
+ """
4
+ from __future__ import annotations
5
+
6
+ import threading
7
+ from weakref import WeakKeyDictionary
8
+
9
+ from aspyx.reflection import Decorators
10
+ from aspyx.di.aop import advice, around, methods, Invocation
11
+
12
+ def synchronized():
13
+ """
14
+ decorate methods to synchronize them based on an instance related `RLock`
15
+ """
16
+ def decorator(func):
17
+ Decorators.add(func, synchronized)
18
+ return func #
19
+
20
+ return decorator
21
+
22
+ @advice
23
+ class SynchronizeAdvice():
24
+ __slots__ = ("locks")
25
+
26
+ # constructor
27
+
28
+ def __init__(self):
29
+ self.locks = WeakKeyDictionary()
30
+
31
+ # internal
32
+
33
+ def get_lock(self, instance) -> threading.RLock:
34
+ lock = self.locks.get(instance, None)
35
+ if lock is None:
36
+ lock = threading.RLock()
37
+ self.locks[instance] = lock
38
+
39
+ return lock
40
+
41
+ # around
42
+
43
+ @around(methods().decorated_with(synchronized))
44
+ def synchronize(self, invocation: Invocation):
45
+ with self.get_lock(invocation.args[0]):
46
+ return invocation.proceed()
@@ -17,7 +17,7 @@ class DecoratorDescriptor:
17
17
  "args"
18
18
  ]
19
19
 
20
- def __init__(self, decorator, *args):
20
+ def __init__(self, decorator: Callable, *args):
21
21
  self.decorator = decorator
22
22
  self.args = args
23
23
 
@@ -69,6 +69,9 @@ class TypeDescriptor:
69
69
 
70
70
  # public
71
71
 
72
+ def is_async(self) -> bool:
73
+ return inspect.iscoroutinefunction(self.method)
74
+
72
75
  def get_decorator(self, decorator: Callable) -> Optional[DecoratorDescriptor]:
73
76
  for dec in self.decorators:
74
77
  if dec.decorator is decorator:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aspyx
3
- Version: 1.2.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
 
@@ -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)`
@@ -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.
@@ -618,6 +696,8 @@ def transactional(scope):
618
696
 
619
697
  - added `YamlConfigurationSource`
620
698
 
699
+ **1.2.1**
700
+
621
701
 
622
702
 
623
703
 
@@ -0,0 +1,20 @@
1
+ aspyx/di/__init__.py,sha256=OfETLGaquTbFHhhDRzzGtnSu-KkMj68aDdEaVU19KoI,1107
2
+ aspyx/di/di.py,sha256=OmvcEd2Arsyo0n9Vh40LM_-jLFdOg-bKwahcEINJ7GA,38363
3
+ aspyx/di/aop/__init__.py,sha256=nOABex49zSyMZ2w1ezwX3Q3yrOcQRSDjDtSj0DwKVbQ,233
4
+ aspyx/di/aop/aop.py,sha256=GPkNE6CgQZUPeMlDdmE5TiksNLSgPb2A8D2IXRZmJ_M,16887
5
+ aspyx/di/configuration/__init__.py,sha256=mweJ3tZX1YJfY1d4ra-i0TWEcF3EwXBpGbHrKg1Kc6E,380
6
+ aspyx/di/configuration/configuration.py,sha256=KfPjrlUhhmEOUxdJiXePt5RGxKc8JczkWqlEBjpWQTg,4362
7
+ aspyx/di/configuration/env_configuration_source.py,sha256=FXPvREzq2ZER6_GG5xdpx154TQQDxZVf7LW7cvaylAk,1446
8
+ aspyx/di/configuration/yaml_configuration_source.py,sha256=NDl3SeoLMNVlzHgfP-Ysvhco1tRew_zFnBL5gGy2WRk,550
9
+ aspyx/di/threading/__init__.py,sha256=qrWdaq7MewQ2UmZy4J0Dn6BhY-ahfiG3xsv-EHqoqSE,191
10
+ aspyx/di/threading/synchronized.py,sha256=awFm8T5_lPw0An-H4-jdTUumPmX348WiYw1x8xyz4qs,1053
11
+ aspyx/di/util/__init__.py,sha256=8H2yKkXu3nkRGeTerb8ialzKGfvzUx44XUWFUYcYuQM,125
12
+ aspyx/di/util/stringbuilder.py,sha256=JywkLxZfaQUUWjSB5wvqA6a6Cfs3sW1jbaZ1z4U0-CQ,540
13
+ aspyx/reflection/__init__.py,sha256=r2sNJrfHDpuqaIYu4fTYsoo046gpgn4VTd7bsS3mQJY,282
14
+ aspyx/reflection/proxy.py,sha256=zJ6Psd6zWfFABdrKOf4cULt3gibyqCRdcR6z8WKIkzE,1982
15
+ aspyx/reflection/reflection.py,sha256=0kQXQcyOCj29vy5Ls4dbAlvOKsY7FRyHndiygZzg7HY,5826
16
+ aspyx-1.3.0.dist-info/licenses/LICENSE,sha256=n4jfx_MNj7cBtPhhI7MCoB_K35cj1icP9yJ4Rh4vlvY,1070
17
+ aspyx-1.3.0.dist-info/METADATA,sha256=lxqBe-dsW1oe6HgBOfoRIUk_q-qFObZSNpO4e8Xi40U,21691
18
+ aspyx-1.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
19
+ aspyx-1.3.0.dist-info/top_level.txt,sha256=A_ZwhBY_ybIgjZlztd44eaOrWqkJAndiqjGlbJ3tR_I,6
20
+ aspyx-1.3.0.dist-info/RECORD,,
@@ -1,18 +0,0 @@
1
- aspyx/di/__init__.py,sha256=xdb2lsKh00uGMFCWYavhUEMGH15OSeAhUC-iSosqHqU,935
2
- aspyx/di/di.py,sha256=evfhTznmPNRDjamthSPisMpDhGZJdNmEREUhdJ9nsTE,35571
3
- aspyx/di/aop/__init__.py,sha256=nOABex49zSyMZ2w1ezwX3Q3yrOcQRSDjDtSj0DwKVbQ,233
4
- aspyx/di/aop/aop.py,sha256=3GKN6sGlsZbJ7_WSxQvHZNFYouAfU4Eq6H5cBBB_e_4,14455
5
- aspyx/di/configuration/__init__.py,sha256=mweJ3tZX1YJfY1d4ra-i0TWEcF3EwXBpGbHrKg1Kc6E,380
6
- aspyx/di/configuration/configuration.py,sha256=KfPjrlUhhmEOUxdJiXePt5RGxKc8JczkWqlEBjpWQTg,4362
7
- aspyx/di/configuration/env_configuration_source.py,sha256=Xh8g3AuQdgk89nG6GKA4iKILXaqHecD0KqMW2w91hXs,1445
8
- aspyx/di/configuration/yaml_configuration_source.py,sha256=LM-5J6IRxBBbBAjDeGIFsmiT-61WuGv1sU_zXWNzhkI,549
9
- aspyx/di/util/__init__.py,sha256=8H2yKkXu3nkRGeTerb8ialzKGfvzUx44XUWFUYcYuQM,125
10
- aspyx/di/util/stringbuilder.py,sha256=JywkLxZfaQUUWjSB5wvqA6a6Cfs3sW1jbaZ1z4U0-CQ,540
11
- aspyx/reflection/__init__.py,sha256=r2sNJrfHDpuqaIYu4fTYsoo046gpgn4VTd7bsS3mQJY,282
12
- aspyx/reflection/proxy.py,sha256=zJ6Psd6zWfFABdrKOf4cULt3gibyqCRdcR6z8WKIkzE,1982
13
- aspyx/reflection/reflection.py,sha256=2oYJKYysH3ULmTzbXdjGBCB1iaLbWh3IPKSnorVLshU,5719
14
- aspyx-1.2.0.dist-info/licenses/LICENSE,sha256=n4jfx_MNj7cBtPhhI7MCoB_K35cj1icP9yJ4Rh4vlvY,1070
15
- aspyx-1.2.0.dist-info/METADATA,sha256=wXSu9Clxg4ZZCoiRAhiZzAR1Wcqtjv4NZie4XilJNSs,19009
16
- aspyx-1.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
- aspyx-1.2.0.dist-info/top_level.txt,sha256=A_ZwhBY_ybIgjZlztd44eaOrWqkJAndiqjGlbJ3tR_I,6
18
- aspyx-1.2.0.dist-info/RECORD,,
File without changes