aspyx 1.3.0__py3-none-any.whl → 1.4.1__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/__init__.py +0 -0
- aspyx/di/__init__.py +2 -2
- aspyx/di/aop/__init__.py +16 -2
- aspyx/di/aop/aop.py +169 -102
- aspyx/di/configuration/__init__.py +3 -3
- aspyx/di/configuration/configuration.py +7 -5
- aspyx/di/di.py +224 -46
- aspyx/di/threading/synchronized.py +8 -5
- aspyx/exception/__init__.py +10 -0
- aspyx/exception/exception_manager.py +185 -0
- aspyx/reflection/proxy.py +2 -4
- aspyx/reflection/reflection.py +81 -5
- aspyx/threading/__init__.py +10 -0
- aspyx/threading/thread_local.py +51 -0
- aspyx/{di/util → util}/stringbuilder.py +15 -0
- {aspyx-1.3.0.dist-info → aspyx-1.4.1.dist-info}/METADATA +235 -93
- aspyx-1.4.1.dist-info/RECORD +25 -0
- aspyx-1.3.0.dist-info/RECORD +0 -20
- /aspyx/{di/util → util}/__init__.py +0 -0
- {aspyx-1.3.0.dist-info → aspyx-1.4.1.dist-info}/WHEEL +0 -0
- {aspyx-1.3.0.dist-info → aspyx-1.4.1.dist-info}/licenses/LICENSE +0 -0
- {aspyx-1.3.0.dist-info → aspyx-1.4.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exception handling code
|
|
3
|
+
"""
|
|
4
|
+
from threading import RLock
|
|
5
|
+
from typing import Any, Callable, Dict, Optional, Type
|
|
6
|
+
|
|
7
|
+
from aspyx.di import injectable, Environment, inject_environment, on_running
|
|
8
|
+
from aspyx.reflection import Decorators, TypeDescriptor
|
|
9
|
+
from aspyx.threading import ThreadLocal
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def exception_handler():
|
|
13
|
+
"""
|
|
14
|
+
This annotation is used to mark classes that container handlers for exceptions
|
|
15
|
+
"""
|
|
16
|
+
def decorator(cls):
|
|
17
|
+
Decorators.add(cls, exception_handler)
|
|
18
|
+
|
|
19
|
+
ExceptionManager.exception_handler_classes.append(cls)
|
|
20
|
+
|
|
21
|
+
return cls
|
|
22
|
+
|
|
23
|
+
return decorator
|
|
24
|
+
|
|
25
|
+
def handle():
|
|
26
|
+
"""
|
|
27
|
+
Any method annotated with @handle will be registered as an exception handler method.
|
|
28
|
+
"""
|
|
29
|
+
def decorator(func):
|
|
30
|
+
Decorators.add(func, handle)
|
|
31
|
+
return func
|
|
32
|
+
|
|
33
|
+
return decorator
|
|
34
|
+
|
|
35
|
+
class Handler:
|
|
36
|
+
# constructor
|
|
37
|
+
|
|
38
|
+
def __init__(self, type_: Type, instance: Any, handler: Callable):
|
|
39
|
+
self.type = type_
|
|
40
|
+
self.instance = instance
|
|
41
|
+
self.handler = handler
|
|
42
|
+
|
|
43
|
+
def handle(self, exception: BaseException) -> BaseException:
|
|
44
|
+
result = self.handler(self.instance, exception)
|
|
45
|
+
|
|
46
|
+
if result is not None:
|
|
47
|
+
return result
|
|
48
|
+
else:
|
|
49
|
+
return exception
|
|
50
|
+
|
|
51
|
+
class Chain:
|
|
52
|
+
# constructor
|
|
53
|
+
|
|
54
|
+
def __init__(self, handler: Handler, next: Optional[Handler] = None):
|
|
55
|
+
self.handler = handler
|
|
56
|
+
self.next = next
|
|
57
|
+
|
|
58
|
+
# public
|
|
59
|
+
|
|
60
|
+
def handle(self, exception: BaseException) -> BaseException:
|
|
61
|
+
return self.handler.handle(exception)
|
|
62
|
+
|
|
63
|
+
class Invocation:
|
|
64
|
+
def __init__(self, exception: BaseException, chain: Chain):
|
|
65
|
+
self.exception = exception
|
|
66
|
+
self.chain = chain
|
|
67
|
+
self.current = self.chain
|
|
68
|
+
|
|
69
|
+
@injectable()
|
|
70
|
+
class ExceptionManager:
|
|
71
|
+
"""
|
|
72
|
+
An exception manager collects all registered handlers and is able to handle an exception
|
|
73
|
+
by dispatching it to the most applicable handler ( according to mro )
|
|
74
|
+
"""
|
|
75
|
+
# static data
|
|
76
|
+
|
|
77
|
+
exception_handler_classes = []
|
|
78
|
+
|
|
79
|
+
invocation = ThreadLocal[Invocation]()
|
|
80
|
+
|
|
81
|
+
# class methods
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def proceed(cls) -> BaseException:
|
|
85
|
+
"""
|
|
86
|
+
proceed with the next most applicable handler
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
BaseException: the resulting exception
|
|
90
|
+
|
|
91
|
+
"""
|
|
92
|
+
invocation = cls.invocation.get()
|
|
93
|
+
|
|
94
|
+
invocation.current = invocation.current.next
|
|
95
|
+
if invocation.current is not None:
|
|
96
|
+
return invocation.current.handle(invocation.exception)
|
|
97
|
+
else:
|
|
98
|
+
return invocation.exception
|
|
99
|
+
|
|
100
|
+
# constructor
|
|
101
|
+
|
|
102
|
+
def __init__(self):
|
|
103
|
+
self.environment : Optional[Environment] = None
|
|
104
|
+
self.handler : list[Handler] = []
|
|
105
|
+
self.cache: Dict[Type, Chain] = {}
|
|
106
|
+
self.lock = RLock()
|
|
107
|
+
|
|
108
|
+
# internal
|
|
109
|
+
|
|
110
|
+
@inject_environment()
|
|
111
|
+
def set_environment(self, environment: Environment):
|
|
112
|
+
self.environment = environment
|
|
113
|
+
|
|
114
|
+
@on_running()
|
|
115
|
+
def setup(self):
|
|
116
|
+
for handler_class in self.exception_handler_classes:
|
|
117
|
+
type_descriptor = TypeDescriptor.for_type(handler_class)
|
|
118
|
+
instance = self.environment.get(handler_class)
|
|
119
|
+
|
|
120
|
+
# analyze methods
|
|
121
|
+
|
|
122
|
+
for method in type_descriptor.get_methods():
|
|
123
|
+
if method.has_decorator(handle):
|
|
124
|
+
if len(method.param_types) == 1:
|
|
125
|
+
exception_type = method.param_types[0]
|
|
126
|
+
|
|
127
|
+
self.handler.append(Handler(
|
|
128
|
+
exception_type,
|
|
129
|
+
instance,
|
|
130
|
+
method.method,
|
|
131
|
+
))
|
|
132
|
+
else:
|
|
133
|
+
print(f"handler {method.method} expected to have one parameter")
|
|
134
|
+
|
|
135
|
+
def get_handlers(self, clazz: Type) -> Optional[Chain]:
|
|
136
|
+
chain = self.cache.get(clazz, None)
|
|
137
|
+
if chain is None:
|
|
138
|
+
with self.lock:
|
|
139
|
+
chain = self.cache.get(clazz, None)
|
|
140
|
+
if chain is None:
|
|
141
|
+
chain = self.compute_handlers(clazz)
|
|
142
|
+
self.cache[clazz] = chain
|
|
143
|
+
|
|
144
|
+
return chain
|
|
145
|
+
|
|
146
|
+
def compute_handlers(self, clazz: Type) -> Optional[Chain]:
|
|
147
|
+
mro = clazz.mro()
|
|
148
|
+
|
|
149
|
+
chain = []
|
|
150
|
+
|
|
151
|
+
for type in mro:
|
|
152
|
+
handler = next((handler for handler in self.handler if handler.type is type), None)
|
|
153
|
+
if handler:
|
|
154
|
+
chain.append(Chain(handler))
|
|
155
|
+
|
|
156
|
+
# chain
|
|
157
|
+
|
|
158
|
+
for i in range(0, len(chain) - 2):
|
|
159
|
+
chain[i].next = chain[i + 1]
|
|
160
|
+
|
|
161
|
+
if len(chain) > 0:
|
|
162
|
+
return chain[0]
|
|
163
|
+
else:
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
def handle(self, exception: BaseException) -> BaseException:
|
|
167
|
+
"""
|
|
168
|
+
handle an exception by invoking the most applicable handler (according to mro)
|
|
169
|
+
and return a possible modified exception as a result.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
exception (BaseException): the exception
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
BaseException: the resulting exception
|
|
176
|
+
"""
|
|
177
|
+
chain = self.get_handlers(type(exception))
|
|
178
|
+
if chain is not None:
|
|
179
|
+
self.invocation.set(Invocation(exception, chain))
|
|
180
|
+
try:
|
|
181
|
+
return chain.handle(exception)
|
|
182
|
+
finally:
|
|
183
|
+
self.invocation.clear()
|
|
184
|
+
else:
|
|
185
|
+
return exception # hmmm?
|
aspyx/reflection/proxy.py
CHANGED
|
@@ -14,6 +14,7 @@ class DynamicProxy(Generic[T]):
|
|
|
14
14
|
by intercepting method calls at runtime and handling them as needed.
|
|
15
15
|
|
|
16
16
|
Usage:
|
|
17
|
+
```python
|
|
17
18
|
class MyHandler(DynamicProxy.InvocationHandler):
|
|
18
19
|
def invoke(self, invocation):
|
|
19
20
|
print(f"Intercepted: {invocation.name}")
|
|
@@ -22,10 +23,7 @@ class DynamicProxy(Generic[T]):
|
|
|
22
23
|
|
|
23
24
|
proxy = DynamicProxy.create(SomeClass, MyHandler())
|
|
24
25
|
proxy.some_method(args) # Will be intercepted by MyHandler.invoke
|
|
25
|
-
|
|
26
|
-
Attributes:
|
|
27
|
-
type: The proxied class type.
|
|
28
|
-
invocation_handler: The handler that processes intercepted method calls.
|
|
26
|
+
```
|
|
29
27
|
"""
|
|
30
28
|
# inner class
|
|
31
29
|
|
aspyx/reflection/reflection.py
CHANGED
|
@@ -12,6 +12,9 @@ from weakref import WeakKeyDictionary
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class DecoratorDescriptor:
|
|
15
|
+
"""
|
|
16
|
+
A DecoratorDescriptor covers the decorator - a callable - and the passed arguments
|
|
17
|
+
"""
|
|
15
18
|
__slots__ = [
|
|
16
19
|
"decorator",
|
|
17
20
|
"args"
|
|
@@ -29,16 +32,44 @@ class Decorators:
|
|
|
29
32
|
Utility class that caches decorators ( Python does not have a feature for this )
|
|
30
33
|
"""
|
|
31
34
|
@classmethod
|
|
32
|
-
def add(cls,
|
|
33
|
-
|
|
35
|
+
def add(cls, func_or_class, decorator: Callable, *args):
|
|
36
|
+
"""
|
|
37
|
+
Remember the decorator
|
|
38
|
+
Args:
|
|
39
|
+
func_or_class: a function or class
|
|
40
|
+
decorator: the decorator
|
|
41
|
+
*args: any arguments supplied to the decorator
|
|
42
|
+
"""
|
|
43
|
+
decorators = getattr(func_or_class, '__decorators__', None)
|
|
34
44
|
if decorators is None:
|
|
35
|
-
setattr(
|
|
45
|
+
setattr(func_or_class, '__decorators__', [DecoratorDescriptor(decorator, *args)])
|
|
36
46
|
else:
|
|
37
47
|
decorators.append(DecoratorDescriptor(decorator, *args))
|
|
38
48
|
|
|
39
49
|
@classmethod
|
|
40
|
-
def
|
|
41
|
-
|
|
50
|
+
def has_decorator(cls, func_or_class, callable: Callable) -> bool:
|
|
51
|
+
"""
|
|
52
|
+
Return True, if the function or class is decorated with the decorator
|
|
53
|
+
Args:
|
|
54
|
+
func_or_class: a function or class
|
|
55
|
+
callable: the decorator
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
bool: the result
|
|
59
|
+
"""
|
|
60
|
+
return any(decorator.decorator is callable for decorator in Decorators.get(func_or_class))
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def get(cls, func_or_class) -> list[DecoratorDescriptor]:
|
|
64
|
+
"""
|
|
65
|
+
return the list of decorators associated with the given function or class
|
|
66
|
+
Args:
|
|
67
|
+
func_or_class: the function or class
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
list[DecoratorDescriptor]: ths list
|
|
71
|
+
"""
|
|
72
|
+
return getattr(func_or_class, '__decorators__', [])
|
|
42
73
|
|
|
43
74
|
class TypeDescriptor:
|
|
44
75
|
"""
|
|
@@ -69,10 +100,46 @@ class TypeDescriptor:
|
|
|
69
100
|
|
|
70
101
|
# public
|
|
71
102
|
|
|
103
|
+
def get_name(self) -> str:
|
|
104
|
+
"""
|
|
105
|
+
return the method name
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
str: the method name
|
|
109
|
+
"""
|
|
110
|
+
return self.method.__name__
|
|
111
|
+
|
|
112
|
+
def get_doc(self, default = "") -> str:
|
|
113
|
+
"""
|
|
114
|
+
return the method docstring
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
default: the default if no docstring is found
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
str: the docstring
|
|
121
|
+
"""
|
|
122
|
+
return self.method.__doc__ or default
|
|
123
|
+
|
|
72
124
|
def is_async(self) -> bool:
|
|
125
|
+
"""
|
|
126
|
+
return true if the method is asynchronous
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
bool: async flag
|
|
130
|
+
"""
|
|
73
131
|
return inspect.iscoroutinefunction(self.method)
|
|
74
132
|
|
|
75
133
|
def get_decorator(self, decorator: Callable) -> Optional[DecoratorDescriptor]:
|
|
134
|
+
"""
|
|
135
|
+
return the DecoratorDescriptor - if any - associated with the passed Callable
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
decorator: the decorator
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Optional[DecoratorDescriptor]: the DecoratorDescriptor or None
|
|
142
|
+
"""
|
|
76
143
|
for dec in self.decorators:
|
|
77
144
|
if dec.decorator is decorator:
|
|
78
145
|
return dec
|
|
@@ -80,6 +147,15 @@ class TypeDescriptor:
|
|
|
80
147
|
return None
|
|
81
148
|
|
|
82
149
|
def has_decorator(self, decorator: Callable) -> bool:
|
|
150
|
+
"""
|
|
151
|
+
return True if the method is decorated with the decorator
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
decorator: the decorator callable
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
bool: True if the method is decorated with the decorator
|
|
158
|
+
"""
|
|
83
159
|
for dec in self.decorators:
|
|
84
160
|
if dec.decorator is decorator:
|
|
85
161
|
return True
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Some threading related utilities.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import threading
|
|
6
|
+
|
|
7
|
+
from typing import Callable, Optional, TypeVar, Generic
|
|
8
|
+
|
|
9
|
+
T = TypeVar("T")
|
|
10
|
+
class ThreadLocal(Generic[T]):
|
|
11
|
+
"""
|
|
12
|
+
A thread local value holder
|
|
13
|
+
"""
|
|
14
|
+
# constructor
|
|
15
|
+
|
|
16
|
+
def __init__(self, default_factory: Optional[Callable[[], T]] = None):
|
|
17
|
+
self.local = threading.local()
|
|
18
|
+
self.factory = default_factory
|
|
19
|
+
|
|
20
|
+
# public
|
|
21
|
+
|
|
22
|
+
def get(self) -> Optional[T]:
|
|
23
|
+
"""
|
|
24
|
+
return the current value or invoke the optional factory to compute one
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Optional[T]: the value associated with the current thread
|
|
28
|
+
"""
|
|
29
|
+
if not hasattr(self.local, "value"):
|
|
30
|
+
if self.factory is not None:
|
|
31
|
+
self.local.value = self.factory()
|
|
32
|
+
else:
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
return self.local.value
|
|
36
|
+
|
|
37
|
+
def set(self, value: T) -> None:
|
|
38
|
+
"""
|
|
39
|
+
set a value in the current thread
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
value: the value
|
|
43
|
+
"""
|
|
44
|
+
self.local.value = value
|
|
45
|
+
|
|
46
|
+
def clear(self) -> None:
|
|
47
|
+
"""
|
|
48
|
+
clear the value in the current thread
|
|
49
|
+
"""
|
|
50
|
+
if hasattr(self.local, "value"):
|
|
51
|
+
del self.local.value
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
Utility class for Java lovers
|
|
3
3
|
"""
|
|
4
4
|
class StringBuilder:
|
|
5
|
+
"""
|
|
6
|
+
A StringBuilder is used to build a string by multiple append calls.
|
|
7
|
+
"""
|
|
5
8
|
___slots__ = ("_parts",)
|
|
6
9
|
|
|
7
10
|
# constructor
|
|
@@ -12,6 +15,15 @@ class StringBuilder:
|
|
|
12
15
|
# public
|
|
13
16
|
|
|
14
17
|
def append(self, s: str) -> "StringBuilder":
|
|
18
|
+
"""
|
|
19
|
+
append a string to the end of the string builder
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
s (str): the string
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
StringBuilder: self
|
|
26
|
+
"""
|
|
15
27
|
self._parts.append(str(s))
|
|
16
28
|
|
|
17
29
|
return self
|
|
@@ -23,6 +35,9 @@ class StringBuilder:
|
|
|
23
35
|
return self
|
|
24
36
|
|
|
25
37
|
def clear(self):
|
|
38
|
+
"""
|
|
39
|
+
clear the content
|
|
40
|
+
"""
|
|
26
41
|
self._parts.clear()
|
|
27
42
|
|
|
28
43
|
# object
|