aspyx 1.2.0__py3-none-any.whl → 1.4.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/__init__.py +0 -0
- aspyx/di/__init__.py +8 -4
- aspyx/di/aop/aop.py +181 -74
- aspyx/di/configuration/env_configuration_source.py +1 -1
- aspyx/di/configuration/yaml_configuration_source.py +1 -1
- aspyx/di/di.py +389 -198
- aspyx/di/threading/__init__.py +11 -0
- aspyx/di/threading/synchronized.py +49 -0
- aspyx/exception/__init__.py +10 -0
- aspyx/exception/exception_manager.py +168 -0
- aspyx/reflection/reflection.py +41 -2
- aspyx/threading/__init__.py +10 -0
- aspyx/threading/thread_local.py +47 -0
- aspyx/{di/util → util}/stringbuilder.py +11 -0
- {aspyx-1.2.0.dist-info → aspyx-1.4.0.dist-info}/METADATA +254 -52
- aspyx-1.4.0.dist-info/RECORD +25 -0
- aspyx-1.2.0.dist-info/RECORD +0 -18
- /aspyx/{di/util → util}/__init__.py +0 -0
- {aspyx-1.2.0.dist-info → aspyx-1.4.0.dist-info}/WHEEL +0 -0
- {aspyx-1.2.0.dist-info → aspyx-1.4.0.dist-info}/licenses/LICENSE +0 -0
- {aspyx-1.2.0.dist-info → aspyx-1.4.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
# constructor
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
self.locks = WeakKeyDictionary()
|
|
28
|
+
|
|
29
|
+
# internal
|
|
30
|
+
|
|
31
|
+
def get_lock(self, instance) -> threading.RLock:
|
|
32
|
+
lock = self.locks.get(instance, None)
|
|
33
|
+
if lock is None:
|
|
34
|
+
lock = threading.RLock()
|
|
35
|
+
self.locks[instance] = lock
|
|
36
|
+
|
|
37
|
+
return lock
|
|
38
|
+
|
|
39
|
+
# around
|
|
40
|
+
|
|
41
|
+
@around(methods().decorated_with(synchronized))
|
|
42
|
+
def synchronize_sync(self, invocation: Invocation):
|
|
43
|
+
with self.get_lock(invocation.args[0]):
|
|
44
|
+
return invocation.proceed()
|
|
45
|
+
|
|
46
|
+
@around(methods().decorated_with(synchronized).that_are_async())
|
|
47
|
+
async def synchronize_async(self, invocation: Invocation):
|
|
48
|
+
with self.get_lock(invocation.args[0]):
|
|
49
|
+
return await invocation.proceed_async()
|
|
@@ -0,0 +1,168 @@
|
|
|
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 ErrorContext():
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
class Handler:
|
|
39
|
+
# constructor
|
|
40
|
+
|
|
41
|
+
def __init__(self, type_: Type, instance: Any, handler: Callable):
|
|
42
|
+
self.type = type_
|
|
43
|
+
self.instance = instance
|
|
44
|
+
self.handler = handler
|
|
45
|
+
|
|
46
|
+
def handle(self, exception: BaseException):
|
|
47
|
+
self.handler(self.instance, exception)
|
|
48
|
+
|
|
49
|
+
class Chain:
|
|
50
|
+
# constructor
|
|
51
|
+
|
|
52
|
+
def __init__(self, handler: Handler, next: Optional[Handler] = None):
|
|
53
|
+
self.handler = handler
|
|
54
|
+
self.next = next
|
|
55
|
+
|
|
56
|
+
# public
|
|
57
|
+
|
|
58
|
+
def handle(self, exception: BaseException):
|
|
59
|
+
self.handler.handle(exception)
|
|
60
|
+
|
|
61
|
+
class Invocation:
|
|
62
|
+
def __init__(self, exception: Exception, chain: Chain):
|
|
63
|
+
self.exception = exception
|
|
64
|
+
self.chain = chain
|
|
65
|
+
self.current = self.chain
|
|
66
|
+
|
|
67
|
+
@injectable()
|
|
68
|
+
class ExceptionManager:
|
|
69
|
+
"""
|
|
70
|
+
An exception manager collects all registered handlers and is able to handle an exception
|
|
71
|
+
by dispatching it to the most applicable handler ( according to mro )
|
|
72
|
+
"""
|
|
73
|
+
# static data
|
|
74
|
+
|
|
75
|
+
exception_handler_classes = []
|
|
76
|
+
|
|
77
|
+
invocation = ThreadLocal()
|
|
78
|
+
|
|
79
|
+
# class methods
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def proceed(cls):
|
|
83
|
+
invocation = cls.invocation.get()
|
|
84
|
+
|
|
85
|
+
invocation.current = invocation.current.next
|
|
86
|
+
if invocation.current is not None:
|
|
87
|
+
invocation.current.handle(invocation.exception)
|
|
88
|
+
|
|
89
|
+
# constructor
|
|
90
|
+
|
|
91
|
+
def __init__(self):
|
|
92
|
+
self.environment : Optional[Environment] = None
|
|
93
|
+
self.handler : list[Handler] = []
|
|
94
|
+
self.cache: Dict[Type, Chain] = {}
|
|
95
|
+
self.lock = RLock()
|
|
96
|
+
self.current_context: Optional[ErrorContext] = None
|
|
97
|
+
|
|
98
|
+
# internal
|
|
99
|
+
|
|
100
|
+
@inject_environment()
|
|
101
|
+
def set_environment(self, environment: Environment):
|
|
102
|
+
self.environment = environment
|
|
103
|
+
|
|
104
|
+
@on_running()
|
|
105
|
+
def setup(self):
|
|
106
|
+
for handler_class in self.exception_handler_classes:
|
|
107
|
+
type_descriptor = TypeDescriptor.for_type(handler_class)
|
|
108
|
+
instance = self.environment.get(handler_class)
|
|
109
|
+
|
|
110
|
+
# analyze methods
|
|
111
|
+
|
|
112
|
+
for method in type_descriptor.get_methods():
|
|
113
|
+
if method.has_decorator(handle):
|
|
114
|
+
if len(method.param_types) == 1:
|
|
115
|
+
exception_type = method.param_types[0]
|
|
116
|
+
|
|
117
|
+
self.handler.append(Handler(
|
|
118
|
+
exception_type,
|
|
119
|
+
instance,
|
|
120
|
+
method.method,
|
|
121
|
+
))
|
|
122
|
+
else:
|
|
123
|
+
print(f"handler {method.method} expected to have one parameter")
|
|
124
|
+
|
|
125
|
+
def get_handlers(self, clazz: Type) -> Optional[Chain]:
|
|
126
|
+
chain = self.cache.get(clazz, None)
|
|
127
|
+
if chain is None:
|
|
128
|
+
with self.lock:
|
|
129
|
+
chain = self.cache.get(clazz, None)
|
|
130
|
+
if chain is None:
|
|
131
|
+
chain = self.compute_handlers(clazz)
|
|
132
|
+
self.cache[clazz] = chain
|
|
133
|
+
|
|
134
|
+
return chain
|
|
135
|
+
|
|
136
|
+
def compute_handlers(self, clazz: Type) -> Optional[Chain]:
|
|
137
|
+
mro = clazz.mro()
|
|
138
|
+
|
|
139
|
+
chain = []
|
|
140
|
+
|
|
141
|
+
for type in mro:
|
|
142
|
+
handler = next((handler for handler in self.handler if handler.type is type), None)
|
|
143
|
+
if handler:
|
|
144
|
+
chain.append(Chain(handler))
|
|
145
|
+
|
|
146
|
+
# chain
|
|
147
|
+
|
|
148
|
+
for i in range(0, len(chain) - 2):
|
|
149
|
+
chain[i].next = chain[i + 1]
|
|
150
|
+
|
|
151
|
+
if len(chain) > 0:
|
|
152
|
+
return chain[0]
|
|
153
|
+
else:
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
def handle(self, exception: Exception):
|
|
157
|
+
"""
|
|
158
|
+
handle an exception by invoking the most applicable handler ( according to mro )
|
|
159
|
+
:param exception: the exception
|
|
160
|
+
"""
|
|
161
|
+
chain = self.get_handlers(type(exception))
|
|
162
|
+
if chain is not None:
|
|
163
|
+
|
|
164
|
+
self.invocation.set(Invocation(exception, chain))
|
|
165
|
+
try:
|
|
166
|
+
chain.handle(exception)
|
|
167
|
+
finally:
|
|
168
|
+
self.invocation.clear()
|
aspyx/reflection/reflection.py
CHANGED
|
@@ -12,12 +12,15 @@ 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"
|
|
18
21
|
]
|
|
19
22
|
|
|
20
|
-
def __init__(self, decorator, *args):
|
|
23
|
+
def __init__(self, decorator: Callable, *args):
|
|
21
24
|
self.decorator = decorator
|
|
22
25
|
self.args = args
|
|
23
26
|
|
|
@@ -29,13 +32,17 @@ 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, func, decorator, *args):
|
|
35
|
+
def add(cls, func, decorator: Callable, *args):
|
|
33
36
|
decorators = getattr(func, '__decorators__', None)
|
|
34
37
|
if decorators is None:
|
|
35
38
|
setattr(func, '__decorators__', [DecoratorDescriptor(decorator, *args)])
|
|
36
39
|
else:
|
|
37
40
|
decorators.append(DecoratorDescriptor(decorator, *args))
|
|
38
41
|
|
|
42
|
+
@classmethod
|
|
43
|
+
def has_decorator(cls, func, callable: Callable) -> bool:
|
|
44
|
+
return any(decorator.decorator is callable for decorator in Decorators.get(func))
|
|
45
|
+
|
|
39
46
|
@classmethod
|
|
40
47
|
def get(cls, func) -> list[DecoratorDescriptor]:
|
|
41
48
|
return getattr(func, '__decorators__', [])
|
|
@@ -69,7 +76,34 @@ class TypeDescriptor:
|
|
|
69
76
|
|
|
70
77
|
# public
|
|
71
78
|
|
|
79
|
+
def get_name(self) -> str:
|
|
80
|
+
"""
|
|
81
|
+
return the method name
|
|
82
|
+
:return: the method name
|
|
83
|
+
"""
|
|
84
|
+
return self.method.__name__
|
|
85
|
+
|
|
86
|
+
def get_doc(self, default = "") -> str:
|
|
87
|
+
"""
|
|
88
|
+
return the method docstring
|
|
89
|
+
:param default: the default if no docstring is found
|
|
90
|
+
:return: the docstring
|
|
91
|
+
"""
|
|
92
|
+
return self.method.__doc__ or default
|
|
93
|
+
|
|
94
|
+
def is_async(self) -> bool:
|
|
95
|
+
"""
|
|
96
|
+
return true if the method is asynchronous
|
|
97
|
+
:return: async flag
|
|
98
|
+
"""
|
|
99
|
+
return inspect.iscoroutinefunction(self.method)
|
|
100
|
+
|
|
72
101
|
def get_decorator(self, decorator: Callable) -> Optional[DecoratorDescriptor]:
|
|
102
|
+
"""
|
|
103
|
+
return the DecoratorDescriptor - if any - associated with the passed Callable
|
|
104
|
+
:param decorator:
|
|
105
|
+
:return: the DecoratorDescriptor or None
|
|
106
|
+
"""
|
|
73
107
|
for dec in self.decorators:
|
|
74
108
|
if dec.decorator is decorator:
|
|
75
109
|
return dec
|
|
@@ -77,6 +111,11 @@ class TypeDescriptor:
|
|
|
77
111
|
return None
|
|
78
112
|
|
|
79
113
|
def has_decorator(self, decorator: Callable) -> bool:
|
|
114
|
+
"""
|
|
115
|
+
return True if the method is decorated with the decorator
|
|
116
|
+
:param decorator: the decorator callable
|
|
117
|
+
:return: True if the method is decorated with the decorator
|
|
118
|
+
"""
|
|
80
119
|
for dec in self.decorators:
|
|
81
120
|
if dec.decorator is decorator:
|
|
82
121
|
return True
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
:return: the value associated with the current thread
|
|
26
|
+
"""
|
|
27
|
+
if not hasattr(self.local, "value"):
|
|
28
|
+
if self.factory is not None:
|
|
29
|
+
self.local.value = self.factory()
|
|
30
|
+
else:
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
return self.local.value
|
|
34
|
+
|
|
35
|
+
def set(self, value: T) -> None:
|
|
36
|
+
"""
|
|
37
|
+
set a value in the current thread
|
|
38
|
+
:param value: the value
|
|
39
|
+
"""
|
|
40
|
+
self.local.value = value
|
|
41
|
+
|
|
42
|
+
def clear(self) -> None:
|
|
43
|
+
"""
|
|
44
|
+
clear the value in the current thread
|
|
45
|
+
"""
|
|
46
|
+
if hasattr(self.local, "value"):
|
|
47
|
+
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,11 @@ 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
|
+
:param s: the string
|
|
21
|
+
:return: self
|
|
22
|
+
"""
|
|
15
23
|
self._parts.append(str(s))
|
|
16
24
|
|
|
17
25
|
return self
|
|
@@ -23,6 +31,9 @@ class StringBuilder:
|
|
|
23
31
|
return self
|
|
24
32
|
|
|
25
33
|
def clear(self):
|
|
34
|
+
"""
|
|
35
|
+
clear the content
|
|
36
|
+
"""
|
|
26
37
|
self._parts.clear()
|
|
27
38
|
|
|
28
39
|
# object
|