aspyx 1.3.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/di/di.py CHANGED
@@ -5,14 +5,16 @@ from __future__ import annotations
5
5
 
6
6
  import inspect
7
7
  import logging
8
+ import importlib
9
+ import pkgutil
10
+ import sys
8
11
 
9
12
  from abc import abstractmethod, ABC
10
13
  from enum import Enum
11
14
  import threading
12
- from operator import truediv
13
15
  from typing import Type, Dict, TypeVar, Generic, Optional, cast, Callable, TypedDict
14
16
 
15
- from aspyx.di.util import StringBuilder
17
+ from aspyx.util import StringBuilder
16
18
  from aspyx.reflection import Decorators, TypeDescriptor, DecoratorDescriptor
17
19
 
18
20
  T = TypeVar("T")
@@ -44,9 +46,9 @@ class DIRegistrationException(DIException):
44
46
 
45
47
  class ProviderCollisionException(DIRegistrationException):
46
48
  def __init__(self, message: str, *providers: AbstractInstanceProvider):
47
- super().__init__(message)
49
+ super().__init__(message)
48
50
 
49
- self.providers = providers
51
+ self.providers = providers
50
52
 
51
53
  def __str__(self):
52
54
  return f"[{self.args[0]} {self.providers[1].location()} collides with {self.providers[0].location()}"
@@ -159,6 +161,13 @@ class SingletonScopeInstanceProvider(InstanceProvider):
159
161
  def create(self, environment: Environment, *args):
160
162
  return SingletonScope()
161
163
 
164
+ class EnvironmentScopeInstanceProvider(InstanceProvider):
165
+ def __init__(self):
166
+ super().__init__(SingletonScopeInstanceProvider, SingletonScope, False, "request") # TODO?
167
+
168
+ def create(self, environment: Environment, *args):
169
+ return EnvironmentScope()
170
+
162
171
  class RequestScopeInstanceProvider(InstanceProvider):
163
172
  def __init__(self):
164
173
  super().__init__(RequestScopeInstanceProvider, RequestScope, False, "singleton")
@@ -166,7 +175,6 @@ class RequestScopeInstanceProvider(InstanceProvider):
166
175
  def create(self, environment: Environment, *args):
167
176
  return RequestScope()
168
177
 
169
-
170
178
  class AmbiguousProvider(AbstractInstanceProvider):
171
179
  """
172
180
  An AmbiguousProvider covers all cases, where fetching a class would lead to an ambiguity exception.
@@ -591,7 +599,7 @@ class Providers:
591
599
  Providers.check.clear()
592
600
 
593
601
  @classmethod
594
- def filter(cls, environment: Environment) -> Dict[Type,AbstractInstanceProvider]:
602
+ def filter(cls, environment: Environment, provider_filter: Callable) -> Dict[Type,AbstractInstanceProvider]:
595
603
  cache: Dict[Type,AbstractInstanceProvider] = {}
596
604
 
597
605
  context: ConditionContext = {
@@ -610,12 +618,18 @@ class Providers:
610
618
  if result is not None:
611
619
  raise ProviderCollisionException(f"type {clazz.__name__} already registered", result, provider)
612
620
 
613
- else:
614
- result = provider
621
+ result = provider
615
622
 
616
623
  return result
617
624
 
618
625
  def provider_applies(provider: AbstractInstanceProvider) -> bool:
626
+ # is it in the right module?
627
+
628
+ if not provider_filter(provider):
629
+ return False
630
+
631
+ # check conditionals
632
+
619
633
  descriptor = TypeDescriptor.for_type(provider.get_host())
620
634
  if descriptor.has_decorator(conditional):
621
635
  conditions: list[Condition] = [*descriptor.get_decorator(conditional).args]
@@ -658,7 +672,7 @@ class Providers:
658
672
 
659
673
  # filter conditional providers and fill base classes as well
660
674
 
661
- for provider_type, providers in Providers.providers.items():
675
+ for provider_type, _ in Providers.providers.items():
662
676
  matching_provider = filter_type(provider_type)
663
677
  if matching_provider is not None:
664
678
  cache_provider_for_type(matching_provider, provider_type)
@@ -677,7 +691,11 @@ class Providers:
677
691
 
678
692
  # and resolve
679
693
 
680
- provider_context = Providers.ResolveContext(result)
694
+ providers = result
695
+ if environment.parent is not None:
696
+ providers = providers | environment.parent.providers # add parent providers
697
+
698
+ provider_context = Providers.ResolveContext(providers)
681
699
  for provider in mapped.values():
682
700
  provider.resolve(provider_context)
683
701
  provider_context.next() # clear dependencies
@@ -894,22 +912,35 @@ class Environment:
894
912
 
895
913
  self.features = features
896
914
  self.providers: Dict[Type, AbstractInstanceProvider] = {}
915
+ self.instances = []
897
916
  self.lifecycle_processors: list[LifecycleProcessor] = []
898
917
 
899
918
  if self.parent is not None:
900
- self.providers |= self.parent.providers
901
- self.lifecycle_processors += self.parent.lifecycle_processors
902
- else: #if self.type is Boot:
919
+ # inherit providers from parent
920
+
921
+ for provider_type, inherited_provider in self.parent.providers.items():
922
+ if inherited_provider.get_scope() == "environment":
923
+ # replace with own environment instance provider
924
+ self.providers[provider_type] = EnvironmentInstanceProvider(self, cast(EnvironmentInstanceProvider, inherited_provider).provider)
925
+ else:
926
+ self.providers[provider_type] = inherited_provider
927
+
928
+ # inherit processors as is unless they have an environment scope
929
+
930
+ for processor in self.parent.lifecycle_processors:
931
+ if self.providers[type(processor)].get_scope() != "environment":
932
+ self.lifecycle_processors.append(processor)
933
+ else:
934
+ # create and remember
935
+ self.lifecycle_processors.append(self.get(type(processor)))
936
+ else:
903
937
  self.providers[SingletonScope] = SingletonScopeInstanceProvider()
904
938
  self.providers[RequestScope] = RequestScopeInstanceProvider()
905
-
906
- self.instances = []
939
+ self.providers[EnvironmentScope] = EnvironmentScopeInstanceProvider()
907
940
 
908
941
  Environment.instance = self
909
942
 
910
- # filter conditional providers
911
-
912
- overall_providers = Providers.filter(self)
943
+ prefix_list : list[str] = []
913
944
 
914
945
  loaded = set()
915
946
 
@@ -918,6 +949,36 @@ class Environment:
918
949
 
919
950
  self.providers[type] = provider
920
951
 
952
+ def get_type_package(type: Type):
953
+ module_name = type.__module__
954
+ module = sys.modules[module_name]
955
+
956
+ return module.__package__
957
+
958
+ def import_package(name: str):
959
+ """Import a package and all its submodules recursively."""
960
+ package = importlib.import_module(name)
961
+ results = {name: package}
962
+
963
+ if hasattr(package, '__path__'): # it's a package, not a single file
964
+ for finder, name, ispkg in pkgutil.walk_packages(package.__path__, prefix=package.__name__ + "."):
965
+ try:
966
+ loaded = sys.modules
967
+
968
+ if loaded.get(name, None) is None:
969
+ Environment.logger.debug("import module %s", name)
970
+
971
+ submodule = importlib.import_module(name)
972
+ results[name] = submodule
973
+ else:
974
+ # skip import
975
+ results[name] = loaded[name]
976
+
977
+ except Exception as e:
978
+ Environment.logger.info("failed to import module %s due to %s", name, str(e))
979
+
980
+ return results
981
+
921
982
  def load_environment(env: Type):
922
983
  if env not in loaded:
923
984
  Environment.logger.debug("load environment %s", env.__qualname__)
@@ -930,24 +991,43 @@ class Environment:
930
991
  if decorator is None:
931
992
  raise DIRegistrationException(f"{env.__name__} is not an environment class")
932
993
 
933
- scan = env.__module__
934
- if "." in scan:
935
- scan = scan.rsplit('.', 1)[0]
994
+ # package
995
+
996
+ package_name = get_type_package(env)
936
997
 
937
998
  # recursion
938
999
 
939
1000
  for import_environment in decorator.args[0] or []:
940
1001
  load_environment(import_environment)
941
1002
 
1003
+ # import package
1004
+
1005
+ if package_name is not None and len(package_name) > 0: # files outside of a package return None pr ""
1006
+ import_package(package_name)
1007
+
942
1008
  # filter and load providers according to their module
943
1009
 
944
- for type, provider in overall_providers.items():
945
- if provider.get_module().startswith(scan):
946
- add_provider(type, provider)
1010
+ module_prefix = package_name
1011
+ if len(module_prefix) == 0:
1012
+ module_prefix = env.__module__
1013
+
1014
+ prefix_list.append(module_prefix)
1015
+
947
1016
  # go
948
1017
 
949
1018
  load_environment(env)
950
1019
 
1020
+ # filter according to the prefix list
1021
+
1022
+ def filter_provider(provider: AbstractInstanceProvider) -> bool:
1023
+ for prefix in prefix_list:
1024
+ if provider.get_host().__module__.startswith(prefix):
1025
+ return True
1026
+
1027
+ return False
1028
+
1029
+ self.providers.update(Providers.filter(self, filter_provider))
1030
+
951
1031
  # construct eager objects for local providers
952
1032
 
953
1033
  for provider in set(self.providers.values()):
@@ -959,6 +1039,14 @@ class Environment:
959
1039
  for instance in self.instances:
960
1040
  self.execute_processors(Lifecycle.ON_RUNNING, instance)
961
1041
 
1042
+ def is_registered_type(self, type: Type) -> bool:
1043
+ provider = self.providers.get(type, None)
1044
+ return provider is not None and not isinstance(provider, AmbiguousProvider)
1045
+
1046
+ def registered_types(self, predicate: Callable[[Type], bool]) -> list[Type]:
1047
+ return [provider.get_type() for provider in self.providers.values()
1048
+ if predicate(provider.get_type())]
1049
+
962
1050
  # internal
963
1051
 
964
1052
  def has_feature(self, feature: str) -> bool:
@@ -1297,6 +1385,19 @@ class SingletonScope(Scope):
1297
1385
 
1298
1386
  return self.value
1299
1387
 
1388
+ @scope("environment", register=False)
1389
+ class EnvironmentScope(SingletonScope):
1390
+ # properties
1391
+
1392
+ __slots__ = [
1393
+ ]
1394
+
1395
+ # constructor
1396
+
1397
+ def __init__(self):
1398
+ super().__init__()
1399
+
1400
+
1300
1401
  @scope("thread")
1301
1402
  class ThreadScope(Scope):
1302
1403
  __slots__ = [
@@ -20,9 +20,7 @@ def synchronized():
20
20
  return decorator
21
21
 
22
22
  @advice
23
- class SynchronizeAdvice():
24
- __slots__ = ("locks")
25
-
23
+ class SynchronizeAdvice:
26
24
  # constructor
27
25
 
28
26
  def __init__(self):
@@ -41,6 +39,11 @@ class SynchronizeAdvice():
41
39
  # around
42
40
 
43
41
  @around(methods().decorated_with(synchronized))
44
- def synchronize(self, invocation: Invocation):
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):
45
48
  with self.get_lock(invocation.args[0]):
46
- return invocation.proceed()
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,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,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,10 +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
+
72
94
  def is_async(self) -> bool:
95
+ """
96
+ return true if the method is asynchronous
97
+ :return: async flag
98
+ """
73
99
  return inspect.iscoroutinefunction(self.method)
74
100
 
75
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
+ """
76
107
  for dec in self.decorators:
77
108
  if dec.decorator is decorator:
78
109
  return dec
@@ -80,6 +111,11 @@ class TypeDescriptor:
80
111
  return None
81
112
 
82
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
+ """
83
119
  for dec in self.decorators:
84
120
  if dec.decorator is decorator:
85
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