aspyx 0.1.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.
- aspyx/di/__init__.py +29 -0
- aspyx/di/aop/__init__.py +11 -0
- aspyx/di/aop/aop.py +532 -0
- aspyx/di/configuration/__init__.py +8 -0
- aspyx/di/configuration/configuration.py +190 -0
- aspyx/di/di.py +1055 -0
- aspyx/reflection/__init__.py +9 -0
- aspyx/reflection/proxy.py +58 -0
- aspyx/reflection/reflection.py +134 -0
- aspyx-0.1.0.dist-info/METADATA +451 -0
- aspyx-0.1.0.dist-info/RECORD +14 -0
- aspyx-0.1.0.dist-info/WHEEL +5 -0
- aspyx-0.1.0.dist-info/licenses/LICENSE +21 -0
- aspyx-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from typing import Generic, TypeVar, Type
|
|
2
|
+
|
|
3
|
+
T = TypeVar("T")
|
|
4
|
+
|
|
5
|
+
class DynamicProxy(Generic[T]):
|
|
6
|
+
"""
|
|
7
|
+
DynamicProxy enables dynamic method interception and delegation for any class type.
|
|
8
|
+
|
|
9
|
+
It is used to create proxy objects that forward method calls to a custom InvocationHandler.
|
|
10
|
+
This allows for advanced patterns such as aspect-oriented programming, logging, or remote invocation,
|
|
11
|
+
by intercepting method calls at runtime and handling them as needed.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
class MyHandler(DynamicProxy.InvocationHandler):
|
|
15
|
+
def invoke(self, invocation):
|
|
16
|
+
print(f"Intercepted: {invocation.name}")
|
|
17
|
+
# custom logic here
|
|
18
|
+
return ...
|
|
19
|
+
|
|
20
|
+
proxy = DynamicProxy.create(SomeClass, MyHandler())
|
|
21
|
+
proxy.some_method(args) # Will be intercepted by MyHandler.invoke
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
type: The proxied class type.
|
|
25
|
+
invocationHandler: The handler that processes intercepted method calls.
|
|
26
|
+
"""
|
|
27
|
+
# inner class
|
|
28
|
+
|
|
29
|
+
class Invocation:
|
|
30
|
+
def __init__(self, type: Type[T], name: str, *args, **kwargs):
|
|
31
|
+
self.type = type
|
|
32
|
+
self.name = name
|
|
33
|
+
self.args = args
|
|
34
|
+
self.kwargs = kwargs
|
|
35
|
+
|
|
36
|
+
class InvocationHandler:
|
|
37
|
+
def invoke(self, invocation: 'DynamicProxy.Invocation'):
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
# class methods
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def create(cls, type: Type[T], invocationHandler: 'DynamicProxy.InvocationHandler') -> T:
|
|
44
|
+
return DynamicProxy(type, invocationHandler)
|
|
45
|
+
|
|
46
|
+
# constructor
|
|
47
|
+
|
|
48
|
+
def __init__(self, type: Type[T], invocationHandler: 'DynamicProxy.InvocationHandler'):
|
|
49
|
+
self.type = type
|
|
50
|
+
self.invocationHandler = invocationHandler
|
|
51
|
+
|
|
52
|
+
# public
|
|
53
|
+
|
|
54
|
+
def __getattr__(self, name):
|
|
55
|
+
def wrapper(*args, **kwargs):
|
|
56
|
+
return self.invocationHandler.invoke(DynamicProxy.Invocation(self.type, name, *args, **kwargs))
|
|
57
|
+
|
|
58
|
+
return wrapper
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from inspect import signature, getmembers
|
|
5
|
+
from typing import Callable, get_type_hints, Type, Dict, Optional
|
|
6
|
+
from weakref import WeakKeyDictionary
|
|
7
|
+
|
|
8
|
+
class DecoratorDescriptor:
|
|
9
|
+
def __init__(self, decorator, *args):
|
|
10
|
+
self.decorator = decorator
|
|
11
|
+
self.args = args
|
|
12
|
+
|
|
13
|
+
def __str__(self):
|
|
14
|
+
return f"@{self.decorator.__name__}({','.join(self.args)})"
|
|
15
|
+
|
|
16
|
+
class Decorators:
|
|
17
|
+
@classmethod
|
|
18
|
+
def add(cls, func, decorator, *args):
|
|
19
|
+
decorators = getattr(func, '__decorators__', None)
|
|
20
|
+
if decorators is None:
|
|
21
|
+
setattr(func, '__decorators__', [DecoratorDescriptor(decorator, *args)])
|
|
22
|
+
else:
|
|
23
|
+
decorators.append(DecoratorDescriptor(decorator, *args))
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def get(cls, func) -> list[DecoratorDescriptor]:
|
|
27
|
+
return getattr(func, '__decorators__', [])
|
|
28
|
+
|
|
29
|
+
class TypeDescriptor:
|
|
30
|
+
# inner class
|
|
31
|
+
|
|
32
|
+
class MethodDescriptor:
|
|
33
|
+
def __init__(self, cls, method: Callable):
|
|
34
|
+
self.clazz = cls
|
|
35
|
+
self.method = method
|
|
36
|
+
self.decorators: list[DecoratorDescriptor] = Decorators.get(method)
|
|
37
|
+
self.paramTypes : list[Type] = []
|
|
38
|
+
|
|
39
|
+
type_hints = get_type_hints(method)
|
|
40
|
+
sig = signature(method)
|
|
41
|
+
|
|
42
|
+
for name, param in sig.parameters.items():
|
|
43
|
+
if name != 'self':
|
|
44
|
+
self.paramTypes.append(type_hints.get(name, object))
|
|
45
|
+
|
|
46
|
+
self.returnType = type_hints.get('return', None)
|
|
47
|
+
|
|
48
|
+
def get_decorator(self, decorator):
|
|
49
|
+
for dec in self.decorators:
|
|
50
|
+
if dec.decorator == decorator:
|
|
51
|
+
return dec
|
|
52
|
+
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
def has_decorator(self, decorator):
|
|
56
|
+
for dec in self.decorators:
|
|
57
|
+
if dec.decorator == decorator:
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
def __str__(self):
|
|
63
|
+
return f"Method({self.method.__name__})"
|
|
64
|
+
|
|
65
|
+
# class methods
|
|
66
|
+
|
|
67
|
+
# class properties
|
|
68
|
+
|
|
69
|
+
_cache = WeakKeyDictionary()
|
|
70
|
+
|
|
71
|
+
# class methods
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def for_type(cls, clazz: Type) -> TypeDescriptor:
|
|
75
|
+
descriptor = cls._cache.get(clazz)
|
|
76
|
+
if descriptor is None:
|
|
77
|
+
descriptor = TypeDescriptor(clazz)
|
|
78
|
+
cls._cache[clazz] = descriptor
|
|
79
|
+
return descriptor
|
|
80
|
+
|
|
81
|
+
# constructor
|
|
82
|
+
|
|
83
|
+
def __init__(self, cls):
|
|
84
|
+
self.cls = cls
|
|
85
|
+
self.decorators = Decorators.get(cls)
|
|
86
|
+
self.methods: Dict[str, TypeDescriptor.MethodDescriptor] = dict()
|
|
87
|
+
self.localMethods: Dict[str, TypeDescriptor.MethodDescriptor] = dict()
|
|
88
|
+
|
|
89
|
+
# check superclasses
|
|
90
|
+
|
|
91
|
+
self.superTypes = [TypeDescriptor.for_type(x) for x in cls.__bases__ if not x is object]
|
|
92
|
+
|
|
93
|
+
for superType in self.superTypes:
|
|
94
|
+
self.methods = self.methods | superType.methods
|
|
95
|
+
|
|
96
|
+
# methods
|
|
97
|
+
|
|
98
|
+
for name, member in self._get_local_members(cls):
|
|
99
|
+
method = TypeDescriptor.MethodDescriptor(cls, member)
|
|
100
|
+
self.localMethods[name] = method
|
|
101
|
+
self.methods[name] = method
|
|
102
|
+
|
|
103
|
+
# internal
|
|
104
|
+
|
|
105
|
+
#isinstance(attr, classmethod)
|
|
106
|
+
|
|
107
|
+
def _get_local_members(self, cls):
|
|
108
|
+
return [
|
|
109
|
+
(name, value)
|
|
110
|
+
for name, value in getmembers(cls, predicate=inspect.isfunction)
|
|
111
|
+
if name in cls.__dict__
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
# public
|
|
115
|
+
|
|
116
|
+
def get_decorator(self, decorator) -> Optional[DecoratorDescriptor]:
|
|
117
|
+
for dec in self.decorators:
|
|
118
|
+
if dec.decorator is decorator:
|
|
119
|
+
return dec
|
|
120
|
+
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
def has_decorator(self, decorator) -> bool:
|
|
124
|
+
for dec in self.decorators:
|
|
125
|
+
if dec.decorator is decorator:
|
|
126
|
+
return True
|
|
127
|
+
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
def get_local_method(self, name) -> Optional[MethodDescriptor]:
|
|
131
|
+
return self.localMethods.get(name, None)
|
|
132
|
+
|
|
133
|
+
def get_method(self, name) -> Optional[MethodDescriptor]:
|
|
134
|
+
return self.methods.get(name, None)
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aspyx
|
|
3
|
+
Version: 0.1.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.8
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# aspyx
|
|
34
|
+
|
|
35
|
+

|
|
36
|
+

|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
## Table of Contents
|
|
40
|
+
|
|
41
|
+
- [Introduction](#aspyx)
|
|
42
|
+
- [Registration](#registration)
|
|
43
|
+
- [Class](#class)
|
|
44
|
+
- [Class Factory](#class-factory)
|
|
45
|
+
- [Method](#method)
|
|
46
|
+
- [Environment](#environment)
|
|
47
|
+
- [Definition](#definition)
|
|
48
|
+
- [Retrieval](#retrieval)
|
|
49
|
+
- [Lifecycle methods](#lifecycle-methods)
|
|
50
|
+
- [Post Processors](#post-processors)
|
|
51
|
+
- [Custom scopes](#custom-scopes)
|
|
52
|
+
- [AOP](#aop)
|
|
53
|
+
- [Configuration](#configuration)
|
|
54
|
+
|
|
55
|
+
# Introduction
|
|
56
|
+
|
|
57
|
+
Aspyx is a small python libary, that adds support for both dependency injection and aop.
|
|
58
|
+
|
|
59
|
+
The following features are supported
|
|
60
|
+
- constructor injection
|
|
61
|
+
- method injection
|
|
62
|
+
- post processors
|
|
63
|
+
- factory classes and methods
|
|
64
|
+
- support for eager construction
|
|
65
|
+
- support for singleton and reuqest scopes
|
|
66
|
+
- possibilty to add custom scopes
|
|
67
|
+
- lifecycle events methods
|
|
68
|
+
- bundling of injectable object sets by environment classes including recursive imports and inheritance
|
|
69
|
+
- container instances that relate to environment classes and manage the lifecylce of related objects
|
|
70
|
+
- hierarchical environments
|
|
71
|
+
|
|
72
|
+
Let's look at a simple example
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from aspyx.di import injectable, on_init, on_destroy, environment, Environment
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@injectable()
|
|
79
|
+
class Foo:
|
|
80
|
+
def __init__(self):
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
def hello(msg: str):
|
|
84
|
+
print(f"hello {msg}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@injectable() # eager and singleton by default
|
|
88
|
+
class Bar:
|
|
89
|
+
def __init__(self, foo: Foo): # will inject the Foo dependency
|
|
90
|
+
self.foo = foo
|
|
91
|
+
|
|
92
|
+
@on_init() # a lifecycle callback called after the constructor
|
|
93
|
+
def init(self):
|
|
94
|
+
...
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# this class will register all - specifically decorated - classes and factories in the own module
|
|
98
|
+
# In this case Foo and Bar
|
|
99
|
+
|
|
100
|
+
@environment()
|
|
101
|
+
class SampleEnvironment:
|
|
102
|
+
# constructor
|
|
103
|
+
|
|
104
|
+
def __init__(self):
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# go, forrest
|
|
109
|
+
|
|
110
|
+
environment = SampleEnvironment(Configuration)
|
|
111
|
+
|
|
112
|
+
bar = env.get(Bar)
|
|
113
|
+
bar.foo.hello("Andi")
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The concepts should be pretty familiar , as well as the names which are a combination of Spring and Angular names :-)
|
|
117
|
+
|
|
118
|
+
Let's add some aspects...
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
@advice
|
|
122
|
+
class SampleAdvice:
|
|
123
|
+
def __init__(self):
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
@before(methods().named("hello").of_type(Foo))
|
|
127
|
+
def callBefore(self, invocation: Invocation):
|
|
128
|
+
print("before Foo.hello(...)")
|
|
129
|
+
|
|
130
|
+
@error(methods().named("hello").of_type(Foo))
|
|
131
|
+
def callError(self, invocation: Invocation):
|
|
132
|
+
print("error Foo.hello(...)")
|
|
133
|
+
print(invocation.exception)
|
|
134
|
+
|
|
135
|
+
@around(methods().named("hello"))
|
|
136
|
+
def callAround(self, invocation: Invocation):
|
|
137
|
+
print("around Foo.hello()")
|
|
138
|
+
|
|
139
|
+
return invocation.proceed()
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The invocation parameter stores the complete context of the current execution, which are
|
|
143
|
+
- the method
|
|
144
|
+
- args
|
|
145
|
+
- kwargs
|
|
146
|
+
- the result
|
|
147
|
+
- the possible caught error
|
|
148
|
+
|
|
149
|
+
Let's look at the details
|
|
150
|
+
|
|
151
|
+
# Registration
|
|
152
|
+
|
|
153
|
+
Different mechanisms are available that make classes eligible for injection
|
|
154
|
+
|
|
155
|
+
## Class
|
|
156
|
+
|
|
157
|
+
Any class annotated with `@injectable` is eligible for injection
|
|
158
|
+
|
|
159
|
+
**Example**:
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
@injectable()
|
|
163
|
+
class Foo:
|
|
164
|
+
def __init__(self):
|
|
165
|
+
pass
|
|
166
|
+
```
|
|
167
|
+
Please make sure, that the class defines a constructor, as this is required to determine injected instances.
|
|
168
|
+
|
|
169
|
+
The constructor can only define parameter types that are known as well to the container!
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
The decorator accepts the keyword arguments
|
|
173
|
+
- `eager=True` if `True`, the container will create the instances automatically while booting the environment
|
|
174
|
+
- `scope="singleton"` defines how often instances will be created. `singleton` will create it only once - per environment -, while `request` will recreate it on every injection request
|
|
175
|
+
|
|
176
|
+
Other scopes can be defined. Please check the corresponding chapter.
|
|
177
|
+
|
|
178
|
+
## Class Factory
|
|
179
|
+
|
|
180
|
+
Classes that implement the `Factory` base class and are annotated with `@factory` will register the appropriate classes returned by the `create` method.
|
|
181
|
+
|
|
182
|
+
**Example**:
|
|
183
|
+
```python
|
|
184
|
+
@factory()
|
|
185
|
+
class TestFactory(Factory[Foo]):
|
|
186
|
+
def __init__(self):
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
def create(self) -> Foo:
|
|
190
|
+
return Foo()
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
As in `@injectable`, the same arguments are possible.
|
|
194
|
+
|
|
195
|
+
## Method
|
|
196
|
+
|
|
197
|
+
Any `injectable` can define methods decorated with `@create()`, that will create appropriate instances.
|
|
198
|
+
|
|
199
|
+
**Example**:
|
|
200
|
+
```python
|
|
201
|
+
@injectable()
|
|
202
|
+
class Foo:
|
|
203
|
+
def __init__(self):
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
@create(scope="request")
|
|
207
|
+
def create(self) -> Baz:
|
|
208
|
+
return Baz()
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
The same arguments as in `@injectable` are possible.
|
|
212
|
+
|
|
213
|
+
# Environment
|
|
214
|
+
|
|
215
|
+
## Definition
|
|
216
|
+
|
|
217
|
+
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.
|
|
218
|
+
|
|
219
|
+
**Example**:
|
|
220
|
+
```python
|
|
221
|
+
@environment()
|
|
222
|
+
class SampleEnvironment:
|
|
223
|
+
def __init__(self):
|
|
224
|
+
pass
|
|
225
|
+
|
|
226
|
+
environment = Environment(SampleEnvironment)
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The default is that all eligible classes, that are implemented in the containing module or in any submodule will be managed.
|
|
230
|
+
|
|
231
|
+
By adding an `imports: list[Type]` parameter, specifying other environment types, it will register the appropriate classes recursively.
|
|
232
|
+
|
|
233
|
+
**Example**:
|
|
234
|
+
```python
|
|
235
|
+
@environment()
|
|
236
|
+
class SampleEnvironmen(imports=[OtherEnvironment])):
|
|
237
|
+
def __init__(self):
|
|
238
|
+
pass
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Another possibility is to add a parent environment as an `Environment` constructor parameter
|
|
242
|
+
|
|
243
|
+
**Example**:
|
|
244
|
+
```python
|
|
245
|
+
rootEnvironment = Environment(RootEnvironment)
|
|
246
|
+
environment = Environment(SampleEnvironment, parent=rootEnvironment)
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
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.
|
|
250
|
+
|
|
251
|
+
The method
|
|
252
|
+
|
|
253
|
+
```shutdown()```
|
|
254
|
+
|
|
255
|
+
is used when a container is not needed anymore. It will call any `on_destroy()` of all created instances.
|
|
256
|
+
|
|
257
|
+
## Retrieval
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
def get(type: Type[T]) -> T
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
is used to retrieve object instances. Depending on the respective scope it will return either cached instances or newly instantiated objects.
|
|
264
|
+
|
|
265
|
+
The container knows about class hierarchies and is able to `get` base classes, as long as there is only one implementation.
|
|
266
|
+
|
|
267
|
+
In case of ambiguities, it will throw an exception.
|
|
268
|
+
|
|
269
|
+
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. )
|
|
270
|
+
|
|
271
|
+
# Lifecycle methods
|
|
272
|
+
|
|
273
|
+
It is possible to declare methods that will be called from the container
|
|
274
|
+
- `@on_init()`
|
|
275
|
+
called after the constructor and all other injections.
|
|
276
|
+
- `@on_destroy()`
|
|
277
|
+
called after the container has been shut down
|
|
278
|
+
|
|
279
|
+
# Post Processors
|
|
280
|
+
|
|
281
|
+
As part of the instantiation logic it is possible to define post processors that execute any side effect on newly created instances.
|
|
282
|
+
|
|
283
|
+
**Example**:
|
|
284
|
+
```python
|
|
285
|
+
@injectable()
|
|
286
|
+
class SamplePostProcessor(PostProcessor):
|
|
287
|
+
def process(self, instance: object, environment: Environment):
|
|
288
|
+
print(f"created a {instance}")
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Any implementing class of `PostProcessor` that is eligible for injection will be called by passing the new instance.
|
|
292
|
+
|
|
293
|
+
Please be aware, that a post processor will only handle instances _after_ its _own_ registration.
|
|
294
|
+
|
|
295
|
+
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!
|
|
296
|
+
|
|
297
|
+
# Custom scopes
|
|
298
|
+
|
|
299
|
+
As explained, available scopes are "singleton" and "request".
|
|
300
|
+
|
|
301
|
+
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`
|
|
302
|
+
|
|
303
|
+
```python
|
|
304
|
+
def get(self, provider: AbstractInstanceProvider, environment: Environment, argProvider: Callable[[],list]):
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Arguments are:
|
|
308
|
+
- `provider` the actual provider that will create an instance
|
|
309
|
+
- `environment`the requesting environment
|
|
310
|
+
- `argPovider` a function that can be called to compute the required arguments recursively
|
|
311
|
+
|
|
312
|
+
**Example**: The simplified code of the singleton provider ( disregarding locking logic )
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
@scope("singleton")
|
|
316
|
+
class SingletonScope(Scope):
|
|
317
|
+
# constructor
|
|
318
|
+
|
|
319
|
+
def __init__(self):
|
|
320
|
+
super().__init__()
|
|
321
|
+
|
|
322
|
+
self.value = None
|
|
323
|
+
|
|
324
|
+
# override
|
|
325
|
+
|
|
326
|
+
def get(self, provider: AbstractInstanceProvider, environment: Environment, argProvider: Callable[[],list]):
|
|
327
|
+
if self.value is None:
|
|
328
|
+
self.value = provider.create(environment, *argProvider())
|
|
329
|
+
|
|
330
|
+
return self.value
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
# AOP
|
|
334
|
+
|
|
335
|
+
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.
|
|
336
|
+
|
|
337
|
+
Advice classes need to be part of classes that add a `@advice()` decorator and can define methods that add aspects.
|
|
338
|
+
|
|
339
|
+
```python
|
|
340
|
+
@advice()
|
|
341
|
+
class SampleAdvice:
|
|
342
|
+
def __init__(self): # could inject dependencies
|
|
343
|
+
pass
|
|
344
|
+
|
|
345
|
+
@before(methods().named("hello").of_type(Foo))
|
|
346
|
+
def callBefore(self, invocation: Invocation):
|
|
347
|
+
# arguments: invocation.args
|
|
348
|
+
print("before Foo.hello(...)")
|
|
349
|
+
|
|
350
|
+
@error(methods().named("hello").of_type(Foo))
|
|
351
|
+
def callError(self, invocation: Invocation):
|
|
352
|
+
print("error Foo.hello(...)")
|
|
353
|
+
print(invocation.exception)
|
|
354
|
+
|
|
355
|
+
@around(methods().named("hello"))
|
|
356
|
+
def callAround(self, invocation: Invocation):
|
|
357
|
+
print("around Foo.hello()")
|
|
358
|
+
|
|
359
|
+
return invocation.proceed() # will leave a result in invocation.result or invocation.exception in case of an exception
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Different aspects - with the appropriate decorator - are possible:
|
|
363
|
+
- `before`
|
|
364
|
+
methods that will be executed _prior_ to the original method
|
|
365
|
+
- `around`
|
|
366
|
+
methods that will be executed _around_ to the original method giving it the possibility add side effects or even change the parameters.
|
|
367
|
+
- `after`
|
|
368
|
+
methods that will be executed _after_ to the original method
|
|
369
|
+
- `error`
|
|
370
|
+
methods that will be executed in case of a caught exception, which can be retrieved by `invocation.exception`
|
|
371
|
+
|
|
372
|
+
All methods are expected to hava single `Invocation` parameter, that stores, the function, args and kwargs, the return value and possible exceptions.
|
|
373
|
+
|
|
374
|
+
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.
|
|
375
|
+
If the `proceed` is called with parameters, they will replace the original parameters!
|
|
376
|
+
|
|
377
|
+
The arguments to the corresponding decorators control, how aspects are associated with which methods.
|
|
378
|
+
A fluent interface is used describe the mapping.
|
|
379
|
+
The parameters restrict either methods or classes and are constructed by a call to either `methods()` or `classes()`.
|
|
380
|
+
|
|
381
|
+
Both add the fluent methods:
|
|
382
|
+
- `of_type(type: Type)`
|
|
383
|
+
defines the matching classes
|
|
384
|
+
- `named(name: str)`
|
|
385
|
+
defines method or class names
|
|
386
|
+
- `matches(re: str)`
|
|
387
|
+
defines regular expressions for methods or classes
|
|
388
|
+
- `decorated_with(type: Type)`
|
|
389
|
+
defines decorators on methods or classes
|
|
390
|
+
|
|
391
|
+
The fluent methods `named`, `matches` and `of_type` can be called multiple timess!
|
|
392
|
+
|
|
393
|
+
# Configuration
|
|
394
|
+
|
|
395
|
+
It is possible to inject configuration values, by decorating methods with `@value(<name>)` given a configuration key.
|
|
396
|
+
|
|
397
|
+
```python
|
|
398
|
+
@injectable()
|
|
399
|
+
class Foo:
|
|
400
|
+
def __init__(self):
|
|
401
|
+
pass
|
|
402
|
+
|
|
403
|
+
@value("OS")
|
|
404
|
+
def inject_value(self, os: str):
|
|
405
|
+
...
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
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.
|
|
409
|
+
|
|
410
|
+
```python
|
|
411
|
+
class ConfigurationSource(ABC):
|
|
412
|
+
def __init__(self, manager: ConfigurationManager):
|
|
413
|
+
manager._register(self)
|
|
414
|
+
pass
|
|
415
|
+
|
|
416
|
+
@abstractmethod
|
|
417
|
+
def load(self) -> dict:
|
|
418
|
+
pass
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
The `load` method is able to return a tree-like structure by returning a `dict`.
|
|
422
|
+
|
|
423
|
+
As a default environment variables are already supported.
|
|
424
|
+
|
|
425
|
+
Other sources can be added dynamically by just registering them.
|
|
426
|
+
|
|
427
|
+
```python
|
|
428
|
+
@injectable()
|
|
429
|
+
class SampleConfigurationSource(ConfigurationSource):
|
|
430
|
+
# constructor
|
|
431
|
+
|
|
432
|
+
def __init__(self, manager: ConfigurationManager):
|
|
433
|
+
super().__init__(manager)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def load(self) -> dict:
|
|
437
|
+
return {
|
|
438
|
+
"a": 1,
|
|
439
|
+
"b": {
|
|
440
|
+
"d": "2",
|
|
441
|
+
"e": 3,
|
|
442
|
+
"f": 4
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
aspyx/di/__init__.py,sha256=US6i6s94TIpk4eiRFKYm9JB0NnU9F_3RAoIoXa-OnTI,800
|
|
2
|
+
aspyx/di/di.py,sha256=6A_-x6NrPArcKddlntpDj41yNOF0Gvbte1wSosv9UCI,30035
|
|
3
|
+
aspyx/di/aop/__init__.py,sha256=2mv6o38HHoBzF_nx8rsrvuOlUlNgYITmNRcQJVT2Bk8,213
|
|
4
|
+
aspyx/di/aop/aop.py,sha256=y5kwSzWWzKsPQNP3RUhxiNA0q5o_T_eGDE5vScOiPTM,13798
|
|
5
|
+
aspyx/di/configuration/__init__.py,sha256=YgUk1bHVYq1EJ9W4D_CJ9ZM-3kje-vzPNMxBdUm0Vlg,211
|
|
6
|
+
aspyx/di/configuration/configuration.py,sha256=-9QgOnC9HyWk9d3N8UQCRYHNtk4qDXjxUBaHC3RvAYc,5685
|
|
7
|
+
aspyx/reflection/__init__.py,sha256=jZAjw5NDrtXENmlBsQ0u9MobcNkfEuT0Oey8IWNYh34,204
|
|
8
|
+
aspyx/reflection/proxy.py,sha256=Re643BJAgbQjmDXO5JAvj4mu3RTpe_HF3TcFQ1euArg,1910
|
|
9
|
+
aspyx/reflection/reflection.py,sha256=DS2J3CfxZ0BpdzGBOBU9c1H3R0XCTE34Zx5k2OWn8Iw,3925
|
|
10
|
+
aspyx-0.1.0.dist-info/licenses/LICENSE,sha256=n4jfx_MNj7cBtPhhI7MCoB_K35cj1icP9yJ4Rh4vlvY,1070
|
|
11
|
+
aspyx-0.1.0.dist-info/METADATA,sha256=Ch6Qb4hxI_vFRJ09nZgRKSiAoIhH8xERLT9a8qDUgqA,13906
|
|
12
|
+
aspyx-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
13
|
+
aspyx-0.1.0.dist-info/top_level.txt,sha256=A_ZwhBY_ybIgjZlztd44eaOrWqkJAndiqjGlbJ3tR_I,6
|
|
14
|
+
aspyx-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Andreas Ernst
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
aspyx
|