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.

@@ -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
 
@@ -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, func, decorator, *args):
33
- decorators = getattr(func, '__decorators__', None)
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(func, '__decorators__', [DecoratorDescriptor(decorator, *args)])
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 get(cls, func) -> list[DecoratorDescriptor]:
41
- return getattr(func, '__decorators__', [])
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,10 @@
1
+ """
2
+ A module with threading related utilities
3
+ """
4
+ from .thread_local import ThreadLocal
5
+
6
+ imports = [ThreadLocal]
7
+
8
+ __all__ = [
9
+ "ThreadLocal",
10
+ ]
@@ -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