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.

@@ -66,10 +66,12 @@ class ConfigurationManager:
66
66
  def get(self, path: str, type: Type[T], default : Optional[T]=None) -> T:
67
67
  """
68
68
  Retrieve a configuration value by path and type, with optional coercion.
69
- Arguments:
69
+
70
+ Args:
70
71
  path (str): The path to the configuration value, e.g. "database.host".
71
72
  type (Type[T]): The expected type.
72
73
  default (Optional[T]): The default value to return if the path is not found.
74
+
73
75
  Returns:
74
76
  T: The configuration value coerced to the specified type, or the default value if not found.
75
77
  """
@@ -119,17 +121,17 @@ class ConfigurationSource(ABC):
119
121
 
120
122
  # decorator
121
123
 
122
- def value(key: str, default=None):
124
+ def inject_value(key: str, default=None):
123
125
  """
124
126
  Decorator to inject a configuration value into a method.
125
127
 
126
- Arguments:
128
+ Args:
127
129
  key (str): The configuration key to inject.
128
130
  default: The default value to use if the key is not found.
129
131
 
130
132
  """
131
133
  def decorator(func):
132
- Decorators.add(func, value, key, default)
134
+ Decorators.add(func, inject_value, key, default)
133
135
 
134
136
  return func
135
137
 
@@ -139,7 +141,7 @@ def value(key: str, default=None):
139
141
  @order(9)
140
142
  class ConfigurationLifecycleCallable(LifecycleCallable):
141
143
  def __init__(self, manager: ConfigurationManager):
142
- super().__init__(value, Lifecycle.ON_INJECT)
144
+ super().__init__(inject_value, Lifecycle.ON_INJECT)
143
145
 
144
146
  self.manager = manager
145
147
 
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()}"
@@ -60,33 +62,76 @@ class DIRuntimeException(DIException):
60
62
 
61
63
  class AbstractInstanceProvider(ABC, Generic[T]):
62
64
  """
63
- Interface for instance providers.
65
+ An AbstractInstanceProvider is responsible to create instances.
64
66
  """
65
67
  @abstractmethod
66
68
  def get_module(self) -> str:
67
- pass
69
+ """
70
+ return the module name of the provider
71
+
72
+ Returns:
73
+ str: the module name of the provider
74
+ """
68
75
 
69
76
  def get_host(self) -> Type[T]:
77
+ """
78
+ return the class which is responsible for creation ( e.g. the injectable class )
79
+
80
+ Returns:
81
+ Type[T]: the class which is responsible for creation
82
+ """
70
83
  return type(self)
71
84
 
72
85
  @abstractmethod
73
86
  def get_type(self) -> Type[T]:
74
- pass
87
+ """
88
+ return the type of the created instance
89
+
90
+ Returns:
91
+ Type[T: the type]
92
+ """
75
93
 
76
94
  @abstractmethod
77
95
  def is_eager(self) -> bool:
78
- pass
96
+ """
97
+ return True, if the provider will eagerly construct instances
98
+
99
+ Returns:
100
+ bool: eager flag
101
+ """
79
102
 
80
103
  @abstractmethod
81
104
  def get_scope(self) -> str:
82
- pass
105
+ """
106
+ return the scope name
107
+
108
+ Returns:
109
+ str: the scope name
110
+ """
111
+
83
112
 
84
113
  def get_dependencies(self) -> (list[Type],int):
114
+ """
115
+ return the types that i depend on ( for constructor or setter injection ).
116
+ The second tuple element is the number of parameters that a construction injection will require
117
+
118
+ Returns:
119
+ (list[Type],int): the type array and the number of parameters
120
+ """
85
121
  return [],1
86
122
 
87
123
  @abstractmethod
88
- def create(self, environment: Environment, *args):
89
- pass
124
+ def create(self, environment: Environment, *args) -> T:
125
+ """
126
+ Create a new instance.
127
+
128
+ Args:
129
+ environment: the Environment
130
+ *args: the required arguments
131
+
132
+ Returns:
133
+ T: the instance
134
+ """
90
135
 
91
136
  def report(self) -> str:
92
137
  return str(self)
@@ -159,6 +204,13 @@ class SingletonScopeInstanceProvider(InstanceProvider):
159
204
  def create(self, environment: Environment, *args):
160
205
  return SingletonScope()
161
206
 
207
+ class EnvironmentScopeInstanceProvider(InstanceProvider):
208
+ def __init__(self):
209
+ super().__init__(SingletonScopeInstanceProvider, SingletonScope, False, "request")
210
+
211
+ def create(self, environment: Environment, *args):
212
+ return EnvironmentScope()
213
+
162
214
  class RequestScopeInstanceProvider(InstanceProvider):
163
215
  def __init__(self):
164
216
  super().__init__(RequestScopeInstanceProvider, RequestScope, False, "singleton")
@@ -166,7 +218,6 @@ class RequestScopeInstanceProvider(InstanceProvider):
166
218
  def create(self, environment: Environment, *args):
167
219
  return RequestScope()
168
220
 
169
-
170
221
  class AmbiguousProvider(AbstractInstanceProvider):
171
222
  """
172
223
  An AmbiguousProvider covers all cases, where fetching a class would lead to an ambiguity exception.
@@ -354,7 +405,7 @@ class ClassInstanceProvider(InstanceProvider):
354
405
  for param in method.param_types:
355
406
  types.append(param)
356
407
 
357
- return (types, self.params)
408
+ return types, self.params
358
409
 
359
410
  def create(self, environment: Environment, *args):
360
411
  Environment.logger.debug("%s create class %s", self, self.type.__qualname__)
@@ -441,6 +492,12 @@ class FactoryInstanceProvider(InstanceProvider):
441
492
  class Lifecycle(Enum):
442
493
  """
443
494
  This enum defines the lifecycle phases that can be processed by lifecycle processors.
495
+ Phases are:
496
+
497
+ - ON_INJECT
498
+ - ON_INIT
499
+ - ON_RUNNING
500
+ - ON_DESTROY
444
501
  """
445
502
 
446
503
  __slots__ = []
@@ -591,7 +648,7 @@ class Providers:
591
648
  Providers.check.clear()
592
649
 
593
650
  @classmethod
594
- def filter(cls, environment: Environment) -> Dict[Type,AbstractInstanceProvider]:
651
+ def filter(cls, environment: Environment, provider_filter: Callable) -> Dict[Type,AbstractInstanceProvider]:
595
652
  cache: Dict[Type,AbstractInstanceProvider] = {}
596
653
 
597
654
  context: ConditionContext = {
@@ -610,12 +667,18 @@ class Providers:
610
667
  if result is not None:
611
668
  raise ProviderCollisionException(f"type {clazz.__name__} already registered", result, provider)
612
669
 
613
- else:
614
- result = provider
670
+ result = provider
615
671
 
616
672
  return result
617
673
 
618
674
  def provider_applies(provider: AbstractInstanceProvider) -> bool:
675
+ # is it in the right module?
676
+
677
+ if not provider_filter(provider):
678
+ return False
679
+
680
+ # check conditionals
681
+
619
682
  descriptor = TypeDescriptor.for_type(provider.get_host())
620
683
  if descriptor.has_decorator(conditional):
621
684
  conditions: list[Condition] = [*descriptor.get_decorator(conditional).args]
@@ -658,7 +721,7 @@ class Providers:
658
721
 
659
722
  # filter conditional providers and fill base classes as well
660
723
 
661
- for provider_type, providers in Providers.providers.items():
724
+ for provider_type, _ in Providers.providers.items():
662
725
  matching_provider = filter_type(provider_type)
663
726
  if matching_provider is not None:
664
727
  cache_provider_for_type(matching_provider, provider_type)
@@ -677,7 +740,11 @@ class Providers:
677
740
 
678
741
  # and resolve
679
742
 
680
- provider_context = Providers.ResolveContext(result)
743
+ providers = result
744
+ if environment.parent is not None:
745
+ providers = providers | environment.parent.providers # add parent providers
746
+
747
+ provider_context = Providers.ResolveContext(providers)
681
748
  for provider in mapped.values():
682
749
  provider.resolve(provider_context)
683
750
  provider_context.next() # clear dependencies
@@ -722,6 +789,10 @@ def injectable(eager=True, scope="singleton"):
722
789
  def factory(eager=True, scope="singleton"):
723
790
  """
724
791
  Decorator that needs to be used on a class that implements the Factory interface.
792
+
793
+ Args:
794
+ eager (bool): If True, the corresponding object will be created eagerly when the environment is created.
795
+ scope (str): The scope of the factory, e.g. "singleton", "request", "environment".
725
796
  """
726
797
  def decorator(cls):
727
798
  Decorators.add(cls, factory)
@@ -736,6 +807,10 @@ def factory(eager=True, scope="singleton"):
736
807
  def create(eager=True, scope="singleton"):
737
808
  """
738
809
  Any method annotated with @create will be registered as a factory method.
810
+
811
+ Args:
812
+ eager (bool): If True, the corresponding object will be created eagerly when the environment is created.
813
+ scope (str): The scope of the factory, e.g. "singleton", "request", "environment".
739
814
  """
740
815
  def decorator(func):
741
816
  Decorators.add(func, create, eager, scope)
@@ -745,7 +820,7 @@ def create(eager=True, scope="singleton"):
745
820
 
746
821
  def on_init():
747
822
  """
748
- Methods annotated with @on_init will be called when the instance is created."""
823
+ Methods annotated with `@on_init` will be called when the instance is created."""
749
824
  def decorator(func):
750
825
  Decorators.add(func, on_init)
751
826
  return func
@@ -754,7 +829,7 @@ def on_init():
754
829
 
755
830
  def on_running():
756
831
  """
757
- Methods annotated with @on_running will be called when the container up and running."""
832
+ Methods annotated with `@on_running` will be called when the container up and running."""
758
833
  def decorator(func):
759
834
  Decorators.add(func, on_running)
760
835
  return func
@@ -763,7 +838,7 @@ def on_running():
763
838
 
764
839
  def on_destroy():
765
840
  """
766
- Methods annotated with @on_destroy will be called when the instance is destroyed.
841
+ Methods annotated with `@on_destroy` will be called when the instance is destroyed.
767
842
  """
768
843
  def decorator(func):
769
844
  Decorators.add(func, on_destroy)
@@ -771,18 +846,19 @@ def on_destroy():
771
846
 
772
847
  return decorator
773
848
 
774
- def environment(imports: Optional[list[Type]] = None):
849
+ def module(imports: Optional[list[Type]] = None):
775
850
  """
776
- This annotation is used to mark classes that control the set of injectables that will be managed based on their location
777
- relative to the module of the class. All @injectable s and @factory s that are located in the same or any sub-module will
851
+ This annotation is used to mark classes that control the discovery process of injectables based on their location
852
+ relative to the module of the class. All `@injectable`s and `@factory`s that are located in the same or any sub-module will
778
853
  be registered and managed accordingly.
779
- Arguments:
780
- imports (Optional[list[Type]]): Optional list of imported environment types
854
+
855
+ Args:
856
+ imports (Optional[list[Type]]): Optional list of imported module types
781
857
  """
782
858
  def decorator(cls):
783
859
  Providers.register(ClassInstanceProvider(cls, True))
784
860
 
785
- Decorators.add(cls, environment, imports)
861
+ Decorators.add(cls, module, imports)
786
862
  Decorators.add(cls, injectable) # do we need that?
787
863
 
788
864
  return cls
@@ -855,11 +931,28 @@ def conditional(*conditions: Condition):
855
931
  class Environment:
856
932
  """
857
933
  Central class that manages the lifecycle of instances and their dependencies.
934
+
935
+ Usage:
936
+
937
+ ```python
938
+ @injectable()
939
+ class Foo:
940
+ def __init__(self):
941
+
942
+ @environment()
943
+ class SimpleEnvironment:
944
+ def __init__(self):
945
+ pass
946
+
947
+ environment = Environment(SimpleEnvironment)
948
+
949
+ foo = environment.get(Foo) # will create an instance of Foo
950
+ ```
858
951
  """
859
952
 
860
953
  # static data
861
954
 
862
- logger = logging.getLogger(__name__) # __name__ = module name
955
+ logger = logging.getLogger("aspyx.di") # __name__ = module name
863
956
 
864
957
  instance : 'Environment' = None
865
958
 
@@ -894,22 +987,35 @@ class Environment:
894
987
 
895
988
  self.features = features
896
989
  self.providers: Dict[Type, AbstractInstanceProvider] = {}
990
+ self.instances = []
897
991
  self.lifecycle_processors: list[LifecycleProcessor] = []
898
992
 
899
993
  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:
994
+ # inherit providers from parent
995
+
996
+ for provider_type, inherited_provider in self.parent.providers.items():
997
+ if inherited_provider.get_scope() == "environment":
998
+ # replace with own environment instance provider
999
+ self.providers[provider_type] = EnvironmentInstanceProvider(self, cast(EnvironmentInstanceProvider, inherited_provider).provider)
1000
+ else:
1001
+ self.providers[provider_type] = inherited_provider
1002
+
1003
+ # inherit processors as is unless they have an environment scope
1004
+
1005
+ for processor in self.parent.lifecycle_processors:
1006
+ if self.providers[type(processor)].get_scope() != "environment":
1007
+ self.lifecycle_processors.append(processor)
1008
+ else:
1009
+ # create and remember
1010
+ self.lifecycle_processors.append(self.get(type(processor)))
1011
+ else:
903
1012
  self.providers[SingletonScope] = SingletonScopeInstanceProvider()
904
1013
  self.providers[RequestScope] = RequestScopeInstanceProvider()
905
-
906
- self.instances = []
1014
+ self.providers[EnvironmentScope] = EnvironmentScopeInstanceProvider()
907
1015
 
908
1016
  Environment.instance = self
909
1017
 
910
- # filter conditional providers
911
-
912
- overall_providers = Providers.filter(self)
1018
+ prefix_list : list[str] = []
913
1019
 
914
1020
  loaded = set()
915
1021
 
@@ -918,6 +1024,36 @@ class Environment:
918
1024
 
919
1025
  self.providers[type] = provider
920
1026
 
1027
+ def get_type_package(type: Type):
1028
+ module_name = type.__module__
1029
+ module = sys.modules[module_name]
1030
+
1031
+ return module.__package__
1032
+
1033
+ def import_package(name: str):
1034
+ """Import a package and all its submodules recursively."""
1035
+ package = importlib.import_module(name)
1036
+ results = {name: package}
1037
+
1038
+ if hasattr(package, '__path__'): # it's a package, not a single file
1039
+ for finder, name, ispkg in pkgutil.walk_packages(package.__path__, prefix=package.__name__ + "."):
1040
+ try:
1041
+ loaded = sys.modules
1042
+
1043
+ if loaded.get(name, None) is None:
1044
+ Environment.logger.debug("import module %s", name)
1045
+
1046
+ submodule = importlib.import_module(name)
1047
+ results[name] = submodule
1048
+ else:
1049
+ # skip import
1050
+ results[name] = loaded[name]
1051
+
1052
+ except Exception as e:
1053
+ Environment.logger.info("failed to import module %s due to %s", name, str(e))
1054
+
1055
+ return results
1056
+
921
1057
  def load_environment(env: Type):
922
1058
  if env not in loaded:
923
1059
  Environment.logger.debug("load environment %s", env.__qualname__)
@@ -926,28 +1062,47 @@ class Environment:
926
1062
 
927
1063
  # sanity check
928
1064
 
929
- decorator = TypeDescriptor.for_type(env).get_decorator(environment)
1065
+ decorator = TypeDescriptor.for_type(env).get_decorator(module)
930
1066
  if decorator is None:
931
1067
  raise DIRegistrationException(f"{env.__name__} is not an environment class")
932
1068
 
933
- scan = env.__module__
934
- if "." in scan:
935
- scan = scan.rsplit('.', 1)[0]
1069
+ # package
1070
+
1071
+ package_name = get_type_package(env)
936
1072
 
937
1073
  # recursion
938
1074
 
939
1075
  for import_environment in decorator.args[0] or []:
940
1076
  load_environment(import_environment)
941
1077
 
1078
+ # import package
1079
+
1080
+ if package_name is not None and len(package_name) > 0: # files outside of a package return None pr ""
1081
+ import_package(package_name)
1082
+
942
1083
  # filter and load providers according to their module
943
1084
 
944
- for type, provider in overall_providers.items():
945
- if provider.get_module().startswith(scan):
946
- add_provider(type, provider)
1085
+ module_prefix = package_name
1086
+ if len(module_prefix) == 0:
1087
+ module_prefix = env.__module__
1088
+
1089
+ prefix_list.append(module_prefix)
1090
+
947
1091
  # go
948
1092
 
949
1093
  load_environment(env)
950
1094
 
1095
+ # filter according to the prefix list
1096
+
1097
+ def filter_provider(provider: AbstractInstanceProvider) -> bool:
1098
+ for prefix in prefix_list:
1099
+ if provider.get_host().__module__.startswith(prefix):
1100
+ return True
1101
+
1102
+ return False
1103
+
1104
+ self.providers.update(Providers.filter(self, filter_provider))
1105
+
951
1106
  # construct eager objects for local providers
952
1107
 
953
1108
  for provider in set(self.providers.values()):
@@ -959,6 +1114,14 @@ class Environment:
959
1114
  for instance in self.instances:
960
1115
  self.execute_processors(Lifecycle.ON_RUNNING, instance)
961
1116
 
1117
+ def is_registered_type(self, type: Type) -> bool:
1118
+ provider = self.providers.get(type, None)
1119
+ return provider is not None and not isinstance(provider, AmbiguousProvider)
1120
+
1121
+ def registered_types(self, predicate: Callable[[Type], bool]) -> list[Type]:
1122
+ return [provider.get_type() for provider in self.providers.values()
1123
+ if predicate(provider.get_type())]
1124
+
962
1125
  # internal
963
1126
 
964
1127
  def has_feature(self, feature: str) -> bool:
@@ -1048,10 +1211,11 @@ class Environment:
1048
1211
  """
1049
1212
  Create or return a cached instance for the given type.
1050
1213
 
1051
- Arguments:
1214
+ Args:
1052
1215
  type (Type): The desired type
1053
1216
 
1054
- Returns: The requested instance
1217
+ Returns:
1218
+ T: The requested instance
1055
1219
  """
1056
1220
  provider = self.providers.get(type, None)
1057
1221
  if provider is None:
@@ -1297,6 +1461,19 @@ class SingletonScope(Scope):
1297
1461
 
1298
1462
  return self.value
1299
1463
 
1464
+ @scope("environment", register=False)
1465
+ class EnvironmentScope(SingletonScope):
1466
+ # properties
1467
+
1468
+ __slots__ = [
1469
+ ]
1470
+
1471
+ # constructor
1472
+
1473
+ def __init__(self):
1474
+ super().__init__()
1475
+
1476
+
1300
1477
  @scope("thread")
1301
1478
  class ThreadScope(Scope):
1302
1479
  __slots__ = [
@@ -1312,11 +1489,12 @@ class ThreadScope(Scope):
1312
1489
  def get(self, provider: AbstractInstanceProvider, environment: Environment, arg_provider: Callable[[], list]):
1313
1490
  if not hasattr(self._local, "value"):
1314
1491
  self._local.value = provider.create(environment, *arg_provider())
1492
+
1315
1493
  return self._local.value
1316
1494
 
1317
1495
  # internal class that is required to import technical instance providers
1318
1496
 
1319
- @environment()
1497
+ @module()
1320
1498
  class Boot:
1321
1499
  # class
1322
1500
 
@@ -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 exception handling functions.
3
+ """
4
+ from .exception_manager import exception_handler, handle, ExceptionManager
5
+
6
+ __all__ = [
7
+ "exception_handler",
8
+ "handle",
9
+ "ExceptionManager"
10
+ ]