aspyx 1.0.1__py3-none-any.whl → 1.2.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,8 @@
1
+ """
2
+ This module provides utility functions.
3
+ """
4
+ from .stringbuilder import StringBuilder
5
+
6
+ __all__ = [
7
+ "StringBuilder",
8
+ ]
@@ -0,0 +1,31 @@
1
+ """
2
+ Utility class for Java lovers
3
+ """
4
+ class StringBuilder:
5
+ ___slots__ = ("_parts",)
6
+
7
+ # constructor
8
+
9
+ def __init__(self):
10
+ self._parts = []
11
+
12
+ # public
13
+
14
+ def append(self, s: str) -> "StringBuilder":
15
+ self._parts.append(str(s))
16
+
17
+ return self
18
+
19
+ def extend(self, iterable) -> "StringBuilder":
20
+ for s in iterable:
21
+ self._parts.append(str(s))
22
+
23
+ return self
24
+
25
+ def clear(self):
26
+ self._parts.clear()
27
+
28
+ # object
29
+
30
+ def __str__(self):
31
+ return ''.join(self._parts)
@@ -6,6 +6,7 @@ from __future__ import annotations
6
6
 
7
7
  import inspect
8
8
  from inspect import signature, getmembers
9
+ import threading
9
10
  from typing import Callable, get_type_hints, Type, Dict, Optional
10
11
  from weakref import WeakKeyDictionary
11
12
 
@@ -24,6 +25,9 @@ class DecoratorDescriptor:
24
25
  return f"@{self.decorator.__name__}({','.join(self.args)})"
25
26
 
26
27
  class Decorators:
28
+ """
29
+ Utility class that caches decorators ( Python does not have a feature for this )
30
+ """
27
31
  @classmethod
28
32
  def add(cls, func, decorator, *args):
29
33
  decorators = getattr(func, '__decorators__', None)
@@ -37,9 +41,17 @@ class Decorators:
37
41
  return getattr(func, '__decorators__', [])
38
42
 
39
43
  class TypeDescriptor:
44
+ """
45
+ This class provides a way to introspect Python classes, their methods, decorators, and type hints.
46
+ """
40
47
  # inner class
41
48
 
42
49
  class MethodDescriptor:
50
+ """
51
+ This class represents a method of a class, including its decorators, parameter types, and return type.
52
+ """
53
+ # constructor
54
+
43
55
  def __init__(self, cls, method: Callable):
44
56
  self.clazz = cls
45
57
  self.method = method
@@ -55,14 +67,16 @@ class TypeDescriptor:
55
67
 
56
68
  self.return_type = type_hints.get('return', None)
57
69
 
58
- def get_decorator(self, decorator) -> Optional[DecoratorDescriptor]:
70
+ # public
71
+
72
+ def get_decorator(self, decorator: Callable) -> Optional[DecoratorDescriptor]:
59
73
  for dec in self.decorators:
60
74
  if dec.decorator is decorator:
61
75
  return dec
62
76
 
63
77
  return None
64
78
 
65
- def has_decorator(self, decorator) -> bool:
79
+ def has_decorator(self, decorator: Callable) -> bool:
66
80
  for dec in self.decorators:
67
81
  if dec.decorator is decorator:
68
82
  return True
@@ -75,15 +89,22 @@ class TypeDescriptor:
75
89
  # class properties
76
90
 
77
91
  _cache = WeakKeyDictionary()
92
+ _lock = threading.RLock()
78
93
 
79
94
  # class methods
80
95
 
81
96
  @classmethod
82
97
  def for_type(cls, clazz: Type) -> TypeDescriptor:
98
+ """
99
+ Returns a TypeDescriptor for the given class, using a cache to avoid redundant introspection.
100
+ """
83
101
  descriptor = cls._cache.get(clazz)
84
102
  if descriptor is None:
85
- descriptor = TypeDescriptor(clazz)
86
- cls._cache[clazz] = descriptor
103
+ with cls._lock:
104
+ descriptor = cls._cache.get(clazz)
105
+ if descriptor is None:
106
+ descriptor = TypeDescriptor(clazz)
107
+ cls._cache[clazz] = descriptor
87
108
 
88
109
  return descriptor
89
110
 
@@ -120,14 +141,19 @@ class TypeDescriptor:
120
141
 
121
142
  # public
122
143
 
123
- def get_decorator(self, decorator) -> Optional[DecoratorDescriptor]:
144
+ def get_decorator(self, decorator: Callable) -> Optional[DecoratorDescriptor]:
145
+ """
146
+ Returns the first decorator of the given type, or None if not found.
147
+ """
124
148
  for dec in self.decorators:
125
149
  if dec.decorator is decorator:
126
150
  return dec
127
151
 
128
152
  return None
129
153
 
130
- def has_decorator(self, decorator) -> bool:
154
+ def has_decorator(self, decorator: Callable) -> bool:
155
+ """
156
+ Checks if the class has a decorator of the given type."""
131
157
  for dec in self.decorators:
132
158
  if dec.decorator is decorator:
133
159
  return True
@@ -135,12 +161,20 @@ class TypeDescriptor:
135
161
  return False
136
162
 
137
163
  def get_methods(self, local = False) -> list[TypeDescriptor.MethodDescriptor]:
164
+ """
165
+ Returns a list of MethodDescriptor objects for the class.
166
+ If local is True, only returns methods defined in the class itself, otherwise includes inherited methods.
167
+ """
138
168
  if local:
139
169
  return list(self.local_methods.values())
140
170
  else:
141
171
  return list(self.methods.values())
142
172
 
143
173
  def get_method(self, name: str, local = False) -> Optional[TypeDescriptor.MethodDescriptor]:
174
+ """
175
+ Returns a MethodDescriptor for the method with the given name.
176
+ If local is True, only searches for methods defined in the class itself, otherwise includes inherited methods.
177
+ """
144
178
  if local:
145
179
  return self.local_methods.get(name, None)
146
180
  else:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aspyx
3
- Version: 1.0.1
3
+ Version: 1.2.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
@@ -34,8 +34,9 @@ Dynamic: license-file
34
34
 
35
35
  ![Pylint](https://github.com/coolsamson7/aspyx/actions/workflows/pylint.yml/badge.svg)
36
36
  ![Build Status](https://github.com/coolsamson7/aspyx/actions/workflows/ci.yml/badge.svg)
37
- ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/aspyx)
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
+ ![coverage](https://img.shields.io/badge/coverage-94%25-brightgreen)
39
40
 
40
41
  ## Table of Contents
41
42
 
@@ -56,18 +57,18 @@ Dynamic: license-file
56
57
  - [AOP](#aop)
57
58
  - [Configuration](#configuration)
58
59
  - [Reflection](#reflection)
60
+ - [Version History](#version-history)
59
61
 
60
62
  # Introduction
61
63
 
62
64
  Aspyx is a small python libary, that adds support for both dependency injection and aop.
63
65
 
64
66
  The following features are supported
65
- - constructor injection
66
- - method injection
67
+ - constructor and setter injection
67
68
  - post processors
68
69
  - factory classes and methods
69
70
  - support for eager construction
70
- - support for singleton and reuqest scopes
71
+ - support for singleton and request scopes
71
72
  - possibilty to add custom scopes
72
73
  - lifecycle events methods
73
74
  - bundling of injectable object sets by environment classes including recursive imports and inheritance
@@ -156,7 +157,7 @@ Let's look at the details
156
157
 
157
158
  `pip install aspyx`
158
159
 
159
- The library is tested with Python version > 3.8
160
+ The library is tested with all Python version > 3.9
160
161
 
161
162
  Ready to go...
162
163
 
@@ -185,8 +186,15 @@ The decorator accepts the keyword arguments
185
186
  - `eager : boolean`
186
187
  if `True`, the container will create the instances automatically while booting the environment. This is the default.
187
188
  - `scope: str`
188
- the name of a scope which will determine how often instances will be created.
189
- `singleton` will create it only once - per environment -, while `request` will recreate it on every injection request. The default is `singleton`
189
+ the name of a - registered - scope which will determine how often instances will be created.
190
+
191
+ The following scopes are implemented out of the box:
192
+ - `singleton`
193
+ objects are created once inside an environment and cached. This is the default.
194
+ - `request`
195
+ objects are created on every injection request
196
+ - `thread`
197
+ objects are created and cached with respect to the current thread.
190
198
 
191
199
  Other scopes - e.g. session related scopes - can be defined dynamically. Please check the corresponding chapter.
192
200
 
@@ -304,7 +312,7 @@ Different decorators are implemented, that call methods with computed values
304
312
  the method is called with all resolved parameter types ( same as the constructor call)
305
313
  - `@inject_environment`
306
314
  the method is called with the creating environment as a single parameter
307
- - `@value()`
315
+ - `@inject_value()`
308
316
  the method is called with a resolved configuration value. Check the corresponding chapter
309
317
 
310
318
  **Example**:
@@ -325,9 +333,11 @@ class Foo:
325
333
 
326
334
  ## Lifecycle methods
327
335
 
328
- It is possible to mark specific lifecle methods.
336
+ It is possible to mark specific lifecyle methods.
329
337
  - `@on_init()`
330
338
  called after the constructor and all other injections.
339
+ - `@on_running()`
340
+ called an environment has initialized all eager objects.
331
341
  - `@on_destroy()`
332
342
  called during shutdown of the environment
333
343
 
@@ -445,7 +455,7 @@ Both add the fluent methods:
445
455
 
446
456
  The fluent methods `named`, `matches` and `of_type` can be called multiple times!
447
457
 
448
- **Example**:
458
+ **Example**: react on both `transactional` decorators on methods or classes
449
459
 
450
460
  ```python
451
461
  @injectable()
@@ -460,7 +470,7 @@ class TransactionAdvice:
460
470
 
461
471
  # Configuration
462
472
 
463
- It is possible to inject configuration values, by decorating methods with `@value(<name>)` given a configuration key.
473
+ It is possible to inject configuration values, by decorating methods with `@inject-value(<name>)` given a configuration key.
464
474
 
465
475
  ```python
466
476
  @injectable()
@@ -468,11 +478,13 @@ class Foo:
468
478
  def __init__(self):
469
479
  pass
470
480
 
471
- @value("OS")
472
- def inject_value(self, os: str):
481
+ @inject_value("HOME")
482
+ def inject_home(self, os: str):
473
483
  ...
474
484
  ```
475
485
 
486
+ If required a coercion will be executed.
487
+
476
488
  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.
477
489
 
478
490
  ```python
@@ -484,14 +496,24 @@ class ConfigurationSource(ABC):
484
496
 
485
497
  @abstractmethod
486
498
  def load(self) -> dict:
487
- pass
488
499
  ```
489
500
 
490
501
  The `load` method is able to return a tree-like structure by returning a `dict`.
491
502
 
492
- As a default environment variables are already supported.
503
+ Configuration variables are retrieved with the method
504
+
505
+ ```python
506
+ def get(self, path: str, type: Type[T], default : Optional[T]=None) -> T:
507
+ ```
508
+
509
+ - `path`
510
+ a '.' separated path
511
+ - `type`
512
+ the desired type
513
+ - `default`
514
+ a default, if no value is registered
493
515
 
494
- Other sources can be added dynamically by just registering them.
516
+ Sources can be added dynamically by registering them.
495
517
 
496
518
  **Example**:
497
519
  ```python
@@ -511,6 +533,31 @@ class SampleConfigurationSource(ConfigurationSource):
511
533
  }
512
534
  ```
513
535
 
536
+ Two specific source are already implemented:
537
+ - `EnvConfigurationSource`
538
+ reads the os environment variables
539
+ - `YamlConfigurationSource`
540
+ reads a specific yaml file
541
+
542
+ Typically you create the required configuration sources in an environment class, e.g.
543
+
544
+ ```python
545
+ @environment()
546
+ class SampleEnvironment:
547
+ # constructor
548
+
549
+ def __init__(self):
550
+ pass
551
+
552
+ @create()
553
+ def create_env_source(self) -> EnvConfigurationSource:
554
+ return EnvConfigurationSource()
555
+
556
+ @create()
557
+ def create_yaml_source(self) -> YamlConfigurationSource:
558
+ return YamlConfigurationSource("config.yaml")
559
+ ```
560
+
514
561
  # Reflection
515
562
 
516
563
  As the library heavily relies on type introspection of classes and methods, a utility class `TypeDescriptor` is available that covers type information on classes.
@@ -522,16 +569,24 @@ TypeDescriptor.for_type(<type>)
522
569
  ```
523
570
 
524
571
  it offers the methods
525
- - `get_methods(local=False)`
526
- - `get_method(name: str, local=False)`
527
- - `has_decorator(decorator) -> bool`
528
- - `get_decorator(decorator)`
572
+ - `get_methods(local=False)`
573
+ return a list of either local or overall methods
574
+ - `get_method(name: str, local=False)`
575
+ return a single either local or overall method
576
+ - `has_decorator(decorator: Callable) -> bool`
577
+ return `True`, if the class is decorated with the specified decorator
578
+ - `get_decorator(decorator) -> Optional[DecoratorDescriptor]`
579
+ return a descriptor covering the decorator. In addition to the callable, it also stores the supplied args in the `args` property
529
580
 
530
581
  The returned method descriptors offer:
531
- - `param_types`
532
- - `return_type`
533
- - `has_decorator(decorator)`
534
- - `get_decorator(decorator)`
582
+ - `param_types`
583
+ list of arg types
584
+ - `return_type`
585
+ the return type
586
+ - `has_decorator(decorator: Callable) -> bool`
587
+ return `True`, if the method is decorated with the specified decorator
588
+ - `get_decorator(decorator: Callable) -> Optional[DecoratorDescriptor]`
589
+ return a descriptor covering the decorator. In addition to the callable, it also stores the supplied args in the `args` property
535
590
 
536
591
  The management of decorators in turn relies on another utility class `Decorators` that caches decorators.
537
592
 
@@ -539,16 +594,29 @@ Whenver you define a custom decorator, you will need to register it accordingly.
539
594
 
540
595
  **Example**:
541
596
  ```python
542
- def transactional():
597
+ def transactional(scope):
543
598
  def decorator(func):
544
- Decorators.add(func, transactional)
599
+ Decorators.add(func, transactional, scope) # also add _all_ parameters in order to cache them
545
600
  return func
546
601
 
547
602
  return decorator
548
603
  ```
549
604
 
550
605
 
606
+ # Version History
607
+
608
+ **1.0.1**
609
+
610
+ - some internal refactorings
611
+
612
+ **1.1.0**
613
+
614
+ - added `@on_running()` callback
615
+ - added `thread` scope
616
+
617
+ **1.2.0**
551
618
 
619
+ - added `YamlConfigurationSource`
552
620
 
553
621
 
554
622
 
@@ -0,0 +1,18 @@
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,,
@@ -1,14 +0,0 @@
1
- aspyx/di/__init__.py,sha256=G13Cz1MMElBKNWsrhgTEIvhmIKj5MX4uqK_Apke4w8I,897
2
- aspyx/di/di.py,sha256=elT3XJokxYxPJ9mx528MAU-pmm9lIZF8_AXKGkjyhBo,31014
3
- aspyx/di/aop/__init__.py,sha256=nOABex49zSyMZ2w1ezwX3Q3yrOcQRSDjDtSj0DwKVbQ,233
4
- aspyx/di/aop/aop.py,sha256=1RUgijk8RsiXWTizfNQX1IfHF7b96kCPdUgahX_J4Io,14457
5
- aspyx/di/configuration/__init__.py,sha256=Zw7h-OlbJD7LyJvzkgyF0EmVqra6oN_Pt0HuUdjTPTA,249
6
- aspyx/di/configuration/configuration.py,sha256=dHYoM3jC43QeDVlT6-mGDj05Sz6-Nm91LNE2TfQT1UU,5740
7
- aspyx/reflection/__init__.py,sha256=r2sNJrfHDpuqaIYu4fTYsoo046gpgn4VTd7bsS3mQJY,282
8
- aspyx/reflection/proxy.py,sha256=zJ6Psd6zWfFABdrKOf4cULt3gibyqCRdcR6z8WKIkzE,1982
9
- aspyx/reflection/reflection.py,sha256=NkBK94kjJ7rQpPsK4mzHzX5TLSlWRM3mbNVzkwOZo4o,4379
10
- aspyx-1.0.1.dist-info/licenses/LICENSE,sha256=n4jfx_MNj7cBtPhhI7MCoB_K35cj1icP9yJ4Rh4vlvY,1070
11
- aspyx-1.0.1.dist-info/METADATA,sha256=69OakKuzM1UK0YTgH-y6-473YWhylxQvt8v993WsqiU,16798
12
- aspyx-1.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
13
- aspyx-1.0.1.dist-info/top_level.txt,sha256=A_ZwhBY_ybIgjZlztd44eaOrWqkJAndiqjGlbJ3tR_I,6
14
- aspyx-1.0.1.dist-info/RECORD,,
File without changes