aspyx 0.1.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.

aspyx-0.1.0/LICENSE ADDED
@@ -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.
aspyx-0.1.0/PKG-INFO ADDED
@@ -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
+ ![Pylint](https://github.com/andreasernst/aspyx/actions/workflows/pylint.yml/badge.svg)
36
+ ![Build Status](https://github.com/andreasernst/aspyx/actions/workflows/ci.yml/badge.svg)
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
+