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.

@@ -0,0 +1,11 @@
1
+ """
2
+ threading utilities
3
+ """
4
+ from .synchronized import synchronized, SynchronizeAdvice
5
+
6
+ imports = [synchronized, SynchronizeAdvice]
7
+
8
+ __all__ = [
9
+ "synchronized",
10
+ "SynchronizeAdvice",
11
+ ]
@@ -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,10 @@
1
+ """
2
+ This module provides utility functions.
3
+ """
4
+ from .exception_manager import exception_handler, handle, ExceptionManager
5
+
6
+ __all__ = [
7
+ "exception_handler",
8
+ "handle",
9
+ "ExceptionManager"
10
+ ]
@@ -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()
@@ -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,10 @@
1
+ """
2
+ threading utilities
3
+ """
4
+ from .thread_local import ThreadLocal
5
+
6
+ imports = [ThreadLocal]
7
+
8
+ __all__ = [
9
+ "ThreadLocal",
10
+ ]
@@ -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