aspyx 1.4.1__py3-none-any.whl → 1.5.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 CHANGED
@@ -0,0 +1 @@
1
+ #
aspyx/di/__init__.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """
2
2
  This module provides dependency injection and aop capabilities for Python applications.
3
3
  """
4
- from .di import conditional, requires_class, requires_feature, DIException, AbstractCallableProcessor, LifecycleCallable, Lifecycle, Providers, Environment, ClassInstanceProvider, injectable, factory, module, inject, order, create, on_init, on_running, on_destroy, inject_environment, Factory, PostProcessor
4
+ from .di import InstanceProvider, conditional, requires_class, requires_feature, DIException, AbstractCallableProcessor, LifecycleCallable, Lifecycle, Providers, Environment, ClassInstanceProvider, injectable, factory, module, inject, order, create, on_init, on_running, on_destroy, inject_environment, Factory, PostProcessor
5
5
 
6
6
  # import something from the subpackages, so that the decorators are executed
7
7
 
@@ -21,7 +21,7 @@ __all__ = [
21
21
  "inject",
22
22
  "create",
23
23
  "order",
24
-
24
+ "InstanceProvider",
25
25
  "on_init",
26
26
  "on_running",
27
27
  "on_destroy",
aspyx/di/di.py CHANGED
@@ -316,13 +316,32 @@ class EnvironmentInstanceProvider(AbstractInstanceProvider):
316
316
 
317
317
  self.environment = environment
318
318
  self.provider = provider
319
- self.dependencies = []
319
+ self.dependencies : list[AbstractInstanceProvider] = []
320
320
  self.scope_instance = Scopes.get(provider.get_scope(), environment)
321
321
 
322
+ # public
323
+
324
+ def print_tree(self, prefix=""):
325
+ children = self.dependencies
326
+ last_index = len(children) - 1
327
+ print(prefix + "+- " + self.report())
328
+
329
+ for i, child in enumerate(children):
330
+ if i == last_index:
331
+ # Last child
332
+ child_prefix = prefix + " "
333
+ else:
334
+ # Not last child
335
+ child_prefix = prefix + "| "
336
+
337
+
338
+ cast(EnvironmentInstanceProvider, child).print_tree(child_prefix)
339
+
322
340
  # implement
323
341
 
324
342
  def resolve(self, context: Providers.ResolveContext):
325
343
  context.add(self)
344
+ context.push(self)
326
345
 
327
346
  if not context.is_resolved(self):
328
347
  context.provider_dependencies[self] = [] #?
@@ -332,7 +351,7 @@ class EnvironmentInstanceProvider(AbstractInstanceProvider):
332
351
  for type in type_and_params[0]:
333
352
  if params > 0:
334
353
  params -= 1
335
- self.dependencies.append(context.get_provider(type))
354
+ self.dependencies.append(context.get_provider(type)) # try/catch TODO
336
355
 
337
356
  provider = context.add_provider_dependency(self, type)
338
357
  if provider is not None:
@@ -341,6 +360,8 @@ class EnvironmentInstanceProvider(AbstractInstanceProvider):
341
360
  else:
342
361
  context.add(*context.get_provider_dependencies(self))
343
362
 
363
+ context.pop()
364
+
344
365
  def get_module(self) -> str:
345
366
  return self.provider.get_module()
346
367
 
@@ -403,7 +424,11 @@ class ClassInstanceProvider(InstanceProvider):
403
424
  for method in TypeDescriptor.for_type(self.type).get_methods():
404
425
  if method.has_decorator(inject):
405
426
  for param in method.param_types:
427
+ if not Providers.is_registered(param):
428
+ raise DIRegistrationException(f"{self.type.__name__}.{method.method.__name__} declares an unknown parameter type {param.__name__}")
429
+
406
430
  types.append(param)
431
+ # done
407
432
 
408
433
  return types, self.params
409
434
 
@@ -431,28 +456,28 @@ class FunctionInstanceProvider(InstanceProvider):
431
456
 
432
457
  # constructor
433
458
 
434
- def __init__(self, clazz : Type, method, return_type : Type[T], eager = True, scope = "singleton"):
435
- super().__init__(clazz, return_type, eager, scope)
459
+ def __init__(self, clazz : Type, method: TypeDescriptor.MethodDescriptor, eager = True, scope = "singleton"):
460
+ super().__init__(clazz, method.return_type, eager, scope)
436
461
 
437
- self.method = method
462
+ self.method : TypeDescriptor.MethodDescriptor = method
438
463
 
439
464
  # implement
440
465
 
441
466
  def get_dependencies(self) -> (list[Type],int):
442
- return [self.host], 1
467
+ return [self.host, *self.method.param_types], 1 + len(self.method.param_types)
443
468
 
444
469
  def create(self, environment: Environment, *args):
445
470
  Environment.logger.debug("%s create class %s", self, self.type.__qualname__)
446
471
 
447
- instance = self.method(*args) # args[0]=self
472
+ instance = self.method.method(*args) # args[0]=self
448
473
 
449
474
  return environment.created(instance)
450
475
 
451
476
  def report(self) -> str:
452
- return f"{self.host.__name__}.{self.method.__name__}"
477
+ return f"{self.host.__name__}.{self.method.get_name()}({', '.join(t.__name__ for t in self.method.param_types)}) -> {self.type.__qualname__}"
453
478
 
454
479
  def __str__(self):
455
- return f"FunctionInstanceProvider({self.host.__name__}.{self.method.__name__} -> {self.type.__name__})"
480
+ return f"FunctionInstanceProvider({self.host.__name__}.{self.method.get_name()}({', '.join(t.__name__ for t in self.method.param_types)}) -> {self.type.__name__})"
456
481
 
457
482
  class FactoryInstanceProvider(InstanceProvider):
458
483
  """
@@ -483,7 +508,7 @@ class FactoryInstanceProvider(InstanceProvider):
483
508
  return environment.created(args[0].create())
484
509
 
485
510
  def report(self) -> str:
486
- return f"{self.host.__name__}.create"
511
+ return f"{self.host.__name__}.create() -> {self.type.__name__} "
487
512
 
488
513
  def __str__(self):
489
514
  return f"FactoryInstanceProvider({self.host.__name__} -> {self.type.__name__})"
@@ -553,7 +578,8 @@ class Providers:
553
578
  __slots__ = [
554
579
  "dependencies",
555
580
  "providers",
556
- "provider_dependencies"
581
+ "provider_dependencies",
582
+ "path"
557
583
  ]
558
584
 
559
585
  # constructor
@@ -561,6 +587,7 @@ class Providers:
561
587
  def __init__(self, providers: Dict[Type, EnvironmentInstanceProvider]):
562
588
  self.dependencies : list[EnvironmentInstanceProvider] = []
563
589
  self.providers = providers
590
+ self.path = []
564
591
  self.provider_dependencies : dict[EnvironmentInstanceProvider, list[EnvironmentInstanceProvider]] = {}
565
592
 
566
593
  # public
@@ -586,7 +613,14 @@ class Providers:
586
613
 
587
614
  return provider
588
615
 
616
+ def push(self, provider):
617
+ self.path.append(provider)
618
+
619
+ def pop(self):
620
+ self.path.pop()
621
+
589
622
  def next(self):
623
+ self.path.clear()
590
624
  self.dependencies.clear()
591
625
 
592
626
  def get_provider(self, type: Type) -> EnvironmentInstanceProvider:
@@ -607,7 +641,7 @@ class Providers:
607
641
  cycle = ""
608
642
 
609
643
  first = True
610
- for p in self.dependencies:
644
+ for p in self.path:
611
645
  if not first:
612
646
  cycle += " -> "
613
647
 
@@ -638,6 +672,10 @@ class Providers:
638
672
  else:
639
673
  candidates.append(provider)
640
674
 
675
+ @classmethod
676
+ def is_registered(cls,type: Type) -> bool:
677
+ return Providers.providers.get(type, None) is not None
678
+
641
679
  # add factories lazily
642
680
 
643
681
  @classmethod
@@ -652,7 +690,7 @@ class Providers:
652
690
  cache: Dict[Type,AbstractInstanceProvider] = {}
653
691
 
654
692
  context: ConditionContext = {
655
- "requires_feature": lambda feature : environment.has_feature(feature),
693
+ "requires_feature": environment.has_feature,
656
694
  "requires_class": lambda clazz : cache.get(clazz, None) is not None # ? only works if the class is in the cache already?
657
695
  }
658
696
 
@@ -708,10 +746,13 @@ class Providers:
708
746
  if type is provider.get_type():
709
747
  raise ProviderCollisionException(f"type {type.__name__} already registered", existing_provider, provider)
710
748
 
711
- if isinstance(existing_provider, AmbiguousProvider):
712
- cast(AmbiguousProvider, existing_provider).add_provider(provider)
713
- else:
714
- cache[type] = AmbiguousProvider(type, existing_provider, provider)
749
+ if existing_provider.get_type() is not type:
750
+ # only overwrite if the existing provider is not specific
751
+
752
+ if isinstance(existing_provider, AmbiguousProvider):
753
+ cast(AmbiguousProvider, existing_provider).add_provider(provider)
754
+ else:
755
+ cache[type] = AmbiguousProvider(type, existing_provider, provider)
715
756
 
716
757
  # recursion
717
758
 
@@ -763,8 +804,7 @@ def register_factories(cls: Type):
763
804
  if return_type is None:
764
805
  raise DIRegistrationException(f"{cls.__name__}.{method.method.__name__} expected to have a return type")
765
806
 
766
- Providers.register(FunctionInstanceProvider(cls, method.method, return_type, create_decorator.args[0],
767
- create_decorator.args[1]))
807
+ Providers.register(FunctionInstanceProvider(cls, method, create_decorator.args[0], create_decorator.args[1]))
768
808
  def order(prio = 0):
769
809
  def decorator(cls):
770
810
  Decorators.add(cls, order, prio)
@@ -939,12 +979,12 @@ class Environment:
939
979
  class Foo:
940
980
  def __init__(self):
941
981
 
942
- @environment()
943
- class SimpleEnvironment:
982
+ @module()
983
+ class Module:
944
984
  def __init__(self):
945
985
  pass
946
986
 
947
- environment = Environment(SimpleEnvironment)
987
+ environment = Environment(Module)
948
988
 
949
989
  foo = environment.get(Foo) # will create an instance of Foo
950
990
  ```
@@ -1026,9 +1066,33 @@ class Environment:
1026
1066
 
1027
1067
  def get_type_package(type: Type):
1028
1068
  module_name = type.__module__
1029
- module = sys.modules[module_name]
1069
+ module = sys.modules.get(module_name)
1070
+
1071
+ if not module:
1072
+ raise ImportError(f"Module {module_name} not found")
1073
+
1074
+ # Try to get the package
1075
+
1076
+ package = getattr(module, '__package__', None)
1030
1077
 
1031
- return module.__package__
1078
+ # Fallback: if module is __main__, try to infer from the module name if possible
1079
+
1080
+ if not package:
1081
+ if module_name == '__main__':
1082
+ # Try to resolve real name via __file__
1083
+ path = getattr(module, '__file__', None)
1084
+ if path:
1085
+ Environment.logger.warning(
1086
+ "Module is __main__; consider running via -m to preserve package context")
1087
+ return ''
1088
+
1089
+ # Try to infer package name from module name
1090
+
1091
+ parts = module_name.split('.')
1092
+ if len(parts) > 1:
1093
+ return '.'.join(parts[:-1])
1094
+
1095
+ return package or ''
1032
1096
 
1033
1097
  def import_package(name: str):
1034
1098
  """Import a package and all its submodules recursively."""
@@ -1106,6 +1170,9 @@ class Environment:
1106
1170
  # construct eager objects for local providers
1107
1171
 
1108
1172
  for provider in set(self.providers.values()):
1173
+ if isinstance(provider, EnvironmentInstanceProvider):
1174
+ provider.print_tree()
1175
+
1109
1176
  if provider.is_eager():
1110
1177
  provider.create(self)
1111
1178
 
@@ -155,7 +155,7 @@ class ExceptionManager:
155
155
 
156
156
  # chain
157
157
 
158
- for i in range(0, len(chain) - 2):
158
+ for i in range(0, len(chain) - 1):
159
159
  chain[i].next = chain[i + 1]
160
160
 
161
161
  if len(chain) > 0:
aspyx/reflection/proxy.py CHANGED
@@ -1,7 +1,8 @@
1
1
  """
2
2
  Dynamic proxies for method interception and delegation.
3
3
  """
4
- from typing import Generic, TypeVar, Type
4
+ import inspect
5
+ from typing import Generic, TypeVar, Type, Callable
5
6
 
6
7
  T = TypeVar("T")
7
8
 
@@ -28,9 +29,18 @@ class DynamicProxy(Generic[T]):
28
29
  # inner class
29
30
 
30
31
  class Invocation:
31
- def __init__(self, type: Type[T], name: str, *args, **kwargs):
32
+ __slots__ = [
33
+ "type",
34
+ "method",
35
+ "args",
36
+ "kwargs",
37
+ ]
38
+
39
+ # constructor
40
+
41
+ def __init__(self, type: Type[T], method: Callable, *args, **kwargs):
32
42
  self.type = type
33
- self.name = name
43
+ self.method = method
34
44
  self.args = args
35
45
  self.kwargs = kwargs
36
46
 
@@ -38,12 +48,20 @@ class DynamicProxy(Generic[T]):
38
48
  def invoke(self, invocation: 'DynamicProxy.Invocation'):
39
49
  pass
40
50
 
51
+ async def invoke_async(self, invocation: 'DynamicProxy.Invocation'):
52
+ return self.invoke(invocation)
53
+
41
54
  # class methods
42
55
 
43
56
  @classmethod
44
57
  def create(cls, type: Type[T], invocation_handler: 'DynamicProxy.InvocationHandler') -> T:
45
58
  return DynamicProxy(type, invocation_handler)
46
59
 
60
+ __slots__ = [
61
+ "type",
62
+ "invocation_handler"
63
+ ]
64
+
47
65
  # constructor
48
66
 
49
67
  def __init__(self, type: Type[T], invocation_handler: 'DynamicProxy.InvocationHandler'):
@@ -53,7 +71,16 @@ class DynamicProxy(Generic[T]):
53
71
  # public
54
72
 
55
73
  def __getattr__(self, name):
56
- def wrapper(*args, **kwargs):
57
- return self.invocation_handler.invoke(DynamicProxy.Invocation(self.type, name, *args, **kwargs))
74
+ method = getattr(self.type, name)
75
+
76
+ if inspect.iscoroutinefunction(method):
77
+ async def async_wrapper(*args, **kwargs):
78
+ return await self.invocation_handler.invoke_async(DynamicProxy.Invocation(self.type, method, *args, **kwargs))
79
+
80
+ return async_wrapper
81
+
82
+ else:
83
+ def sync_wrapper(*args, **kwargs):
84
+ return self.invocation_handler.invoke(DynamicProxy.Invocation(self.type, method, *args, **kwargs))
58
85
 
59
- return wrapper
86
+ return sync_wrapper
@@ -5,8 +5,10 @@ including their methods, decorators, and type hints. It supports caching for per
5
5
  from __future__ import annotations
6
6
 
7
7
  import inspect
8
- from inspect import signature, getmembers
8
+ from inspect import signature
9
9
  import threading
10
+ from types import FunctionType
11
+
10
12
  from typing import Callable, get_type_hints, Type, Dict, Optional
11
13
  from weakref import WeakKeyDictionary
12
14
 
@@ -25,7 +27,7 @@ class DecoratorDescriptor:
25
27
  self.args = args
26
28
 
27
29
  def __str__(self):
28
- return f"@{self.decorator.__name__}({','.join(self.args)})"
30
+ return f"@{self.decorator.__name__}({', '.join(map(str, self.args))})"
29
31
 
30
32
  class Decorators:
31
33
  """
@@ -59,6 +61,14 @@ class Decorators:
59
61
  """
60
62
  return any(decorator.decorator is callable for decorator in Decorators.get(func_or_class))
61
63
 
64
+ @classmethod
65
+ def get_decorator(cls, func_or_class, callable: Callable) -> DecoratorDescriptor:
66
+ return next((decorator for decorator in Decorators.get_all(func_or_class) if decorator.decorator is callable), None)
67
+
68
+ @classmethod
69
+ def get_all(cls, func_or_class) -> list[DecoratorDescriptor]:
70
+ return getattr(func_or_class, '__decorators__', [])
71
+
62
72
  @classmethod
63
73
  def get(cls, func_or_class) -> list[DecoratorDescriptor]:
64
74
  """
@@ -67,9 +77,14 @@ class Decorators:
67
77
  func_or_class: the function or class
68
78
 
69
79
  Returns:
70
- list[DecoratorDescriptor]: ths list
80
+ list[DecoratorDescriptor]: the list
71
81
  """
72
- return getattr(func_or_class, '__decorators__', [])
82
+ if inspect.ismethod(func_or_class):
83
+ func_or_class = func_or_class.__func__ # unwrap bound method
84
+
85
+ #return getattr(func_or_class, '__decorators__', []) will return inherited as well
86
+ return func_or_class.__dict__.get('__decorators__', [])
87
+
73
88
 
74
89
  class TypeDescriptor:
75
90
  """
@@ -130,6 +145,9 @@ class TypeDescriptor:
130
145
  """
131
146
  return inspect.iscoroutinefunction(self.method)
132
147
 
148
+ def get_decorators(self) -> list[DecoratorDescriptor]:
149
+ return self.decorators
150
+
133
151
  def get_decorator(self, decorator: Callable) -> Optional[DecoratorDescriptor]:
134
152
  """
135
153
  return the DecoratorDescriptor - if any - associated with the passed Callable
@@ -212,10 +230,16 @@ class TypeDescriptor:
212
230
  # internal
213
231
 
214
232
  def _get_local_members(self, cls):
233
+ #return [
234
+ # (name, value)
235
+ # for name, value in getmembers(cls, predicate=inspect.isfunction)
236
+ # if name in cls.__dict__
237
+ #]
238
+
215
239
  return [
216
- (name, value)
217
- for name, value in getmembers(cls, predicate=inspect.isfunction)
218
- if name in cls.__dict__
240
+ (name, attr)
241
+ for name, attr in cls.__dict__.items()
242
+ if isinstance(attr, FunctionType)
219
243
  ]
220
244
 
221
245
  # public
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.4
2
+ Name: aspyx
3
+ Version: 1.5.0
4
+ Summary: A DI and AOP library for Python
5
+ Author-email: Andreas Ernst <andreas.ernst7@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 Andreas Ernst
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ License-File: LICENSE
28
+ Requires-Python: >=3.9
29
+ Requires-Dist: python-dotenv~=1.1.0
30
+ Requires-Dist: pyyaml~=6.0.2
31
+ Description-Content-Type: text/markdown
32
+
33
+ aspyx
@@ -1,6 +1,6 @@
1
- aspyx/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- aspyx/di/__init__.py,sha256=BDOloIhmCIUJWC7l4PLtuiWS1LlWyitIofLCLcyXYpQ,1097
3
- aspyx/di/di.py,sha256=ozZanDcrmluzghQBqqD_vbav3Civ-V4R8XXwlBAM3MI,44068
1
+ aspyx/__init__.py,sha256=MsSFjiLMLJZ7QhUPpVBWKiyDnCzryquRyr329NoCACI,2
2
+ aspyx/di/__init__.py,sha256=AGVU2VBWQyBxSssvbk_GOKrYWIYtcmSoIlupz-Oqxi4,1138
3
+ aspyx/di/di.py,sha256=MN9I-KIAgBQYakctEZQH3W1_jjsc7pgGyeLt7eJr0yY,46539
4
4
  aspyx/di/aop/__init__.py,sha256=rn6LSpzFtUOlgaBATyhLRWBzFmZ6XoVKA9B8SgQzYEI,746
5
5
  aspyx/di/aop/aop.py,sha256=Cn-fqFW6PznVDM38fPX7mqlSpjGKMsgpJRBSYBv59xY,18403
6
6
  aspyx/di/configuration/__init__.py,sha256=flM9A79J2wfA5I8goQbxs4tTqYustR9tn_9s0YO2WJQ,484
@@ -10,16 +10,15 @@ aspyx/di/configuration/yaml_configuration_source.py,sha256=NDl3SeoLMNVlzHgfP-Ysv
10
10
  aspyx/di/threading/__init__.py,sha256=qrWdaq7MewQ2UmZy4J0Dn6BhY-ahfiG3xsv-EHqoqSE,191
11
11
  aspyx/di/threading/synchronized.py,sha256=BQ9PjMQUJsF5r-qWaDgvqg3AvFm_R9QZdKB49EkoelQ,1263
12
12
  aspyx/exception/__init__.py,sha256=OZwv-C3ZHD0Eg1rohCQMj575WLJ7lfYuk6PZD6sh1MA,211
13
- aspyx/exception/exception_manager.py,sha256=ihQ8Hs_EAUi-4xtVOn6kNZVblJjznpscLQ4vKXfWq7s,5228
13
+ aspyx/exception/exception_manager.py,sha256=tv0nb0b2CFPiYWK6wwH9yI8hSc--9Xz9_xQVBwU42wc,5228
14
14
  aspyx/reflection/__init__.py,sha256=r2sNJrfHDpuqaIYu4fTYsoo046gpgn4VTd7bsS3mQJY,282
15
- aspyx/reflection/proxy.py,sha256=kaVPeEGuerdYgcgchMe99c8xykDskRSYR-w4OF83Ofo,1868
16
- aspyx/reflection/reflection.py,sha256=AgChenUzK9elcqOc_BfMiszTQuXrsy0NHYkqy9Jsl0E,8066
15
+ aspyx/reflection/proxy.py,sha256=9zqzmK2HGGx7LxdiBw8MfKRNT8H03h_0I6Y972eKFH8,2582
16
+ aspyx/reflection/reflection.py,sha256=HYzbzExG7jzdHG_AggrRxy9yte6Cl192dPsJBRl785Y,8939
17
17
  aspyx/threading/__init__.py,sha256=3clmbCDP37GPan3dWtxTQvpg0Ti4aFzruAbUClkHGi0,147
18
18
  aspyx/threading/thread_local.py,sha256=86dNtbA4k2B-rNUUnZgn3_pU0DAojgLrRnh8RL6zf1E,1196
19
19
  aspyx/util/__init__.py,sha256=8H2yKkXu3nkRGeTerb8ialzKGfvzUx44XUWFUYcYuQM,125
20
20
  aspyx/util/stringbuilder.py,sha256=a-0T4YEXSJFUuQ3ztKN1ZPARkh8dIGMSkNEEJHRN7dc,856
21
- aspyx-1.4.1.dist-info/licenses/LICENSE,sha256=n4jfx_MNj7cBtPhhI7MCoB_K35cj1icP9yJ4Rh4vlvY,1070
22
- aspyx-1.4.1.dist-info/METADATA,sha256=cEYH9P1Mng4RjRQID-bUp-5X6fbxUHhQWf3WDUwkPI0,26564
23
- aspyx-1.4.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
24
- aspyx-1.4.1.dist-info/top_level.txt,sha256=A_ZwhBY_ybIgjZlztd44eaOrWqkJAndiqjGlbJ3tR_I,6
25
- aspyx-1.4.1.dist-info/RECORD,,
21
+ aspyx-1.5.0.dist-info/METADATA,sha256=dm6A8H-_qsn1NDzfFFG2AYclm7Bj6BbLdgtbuVRR6HY,1540
22
+ aspyx-1.5.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
+ aspyx-1.5.0.dist-info/licenses/LICENSE,sha256=n4jfx_MNj7cBtPhhI7MCoB_K35cj1icP9yJ4Rh4vlvY,1070
24
+ aspyx-1.5.0.dist-info/RECORD,,
@@ -1,5 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: hatchling 1.27.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
-
@@ -1,845 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: aspyx
3
- Version: 1.4.1
4
- Summary: A DI and AOP library for Python
5
- Author-email: Andreas Ernst <andreas.ernst7@gmail.com>
6
- License: MIT License
7
-
8
- Copyright (c) 2025 Andreas Ernst
9
-
10
- Permission is hereby granted, free of charge, to any person obtaining a copy
11
- of this software and associated documentation files (the "Software"), to deal
12
- in the Software without restriction, including without limitation the rights
13
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
- copies of the Software, and to permit persons to whom the Software is
15
- furnished to do so, subject to the following conditions:
16
-
17
- The above copyright notice and this permission notice shall be included in all
18
- copies or substantial portions of the Software.
19
-
20
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
- SOFTWARE.
27
-
28
- Requires-Python: >=3.9
29
- Description-Content-Type: text/markdown
30
- License-File: LICENSE
31
- Provides-Extra: dev
32
- Requires-Dist: mkdocstrings-python; extra == "dev"
33
- Dynamic: license-file
34
-
35
- # aspyx
36
-
37
- ![Pylint](https://github.com/coolsamson7/aspyx/actions/workflows/pylint.yml/badge.svg)
38
- ![Build Status](https://github.com/coolsamson7/aspyx/actions/workflows/ci.yml/badge.svg)
39
- ![Python Versions](https://img.shields.io/badge/python-3.9%20|%203.10%20|%203.11%20|%203.12-blue)
40
- ![License](https://img.shields.io/github/license/coolsamson7/aspyx)
41
- ![coverage](https://img.shields.io/badge/coverage-94%25-brightgreen)
42
- [![PyPI](https://img.shields.io/pypi/v/aspyx)](https://pypi.org/project/aspyx/)
43
- [![Docs](https://img.shields.io/badge/docs-online-blue?logo=github)](https://coolsamson7.github.io/aspyx/index/introduction)
44
-
45
- ![image](https://github.com/user-attachments/assets/e808210a-b1a4-4fd0-93f1-b5f9845fa520)
46
-
47
- ## Table of Contents
48
-
49
- - [Motivation](#motivation)
50
- - [Overview](#overview)
51
- - [Installation](#installation)
52
- - [Registration](#registration)
53
- - [Class](#class)
54
- - [Class Factory](#class-factory)
55
- - [Method](#method)
56
- - [Conditional](#conditional)
57
- - [Environment](#environment)
58
- - [Definition](#definition)
59
- - [Retrieval](#retrieval)
60
- - [Instantiation logic](#instantiation-logic)
61
- - [Injection Methods](#injection-methods)
62
- - [Lifecycle Methods](#lifecycle-methods)
63
- - [Post Processors](#post-processors)
64
- - [Custom scopes](#custom-scopes)
65
- - [AOP](#aop)
66
- - [Threading](#threading)
67
- - [Configuration](#configuration)
68
- - [Reflection](#reflection)
69
- - [Exceptions](#exceptions)
70
- - [Version History](#version-history)
71
-
72
- # Motivation
73
-
74
- While working on AI-related projects in Python, I was looking for a dependency injection (DI) framework. After evaluating existing options, my impression was that the most either lacked key features — such as integrated AOP — or had APIs that felt overly technical and complex, which made me develop a library on my own with the following goals
75
-
76
- - bring both di and AOP features together in a lightweight library ( still only about 2T loc),
77
- - be as minimal invasive as possible,
78
- - offering mechanisms to easily extend and customize features without touching the core,
79
- - while still offering a _simple_ and _readable_ api that doesnt overwhelm developers and only requires a minimum initial learning curve
80
-
81
- The AOP integration, in particular, makes a lot of sense because:
82
-
83
- - Aspects typically require context, which is naturally provided through DI,
84
- - And they should only apply to objects managed by the container, rather than acting globally.
85
-
86
- # Overview
87
-
88
- Aspyx is a lightweight - still only about 2T LOC- Python library that provides both Dependency Injection (DI) and Aspect-Oriented Programming (AOP) support.
89
-
90
- The following DI features are supported
91
- - constructor and setter injection
92
- - injection of configuration variables
93
- - possibility to define custom injections
94
- - post processors
95
- - support for factory classes and methods
96
- - support for eager and lazy construction
97
- - support for scopes "singleton", "request" and "thread"
98
- - possibility to add custom scopes
99
- - conditional registration of classes and factories ( aka profiles in spring )
100
- - lifecycle events methods `on_init`, `on_destroy`, `on_running`
101
- - Automatic discovery and bundling of injectable objects based on their module location, including support for recursive imports
102
- - Instantiation of one or possible more isolated container instances — called environments — each managing the lifecycle of a related set of objects,
103
- - Support for hierarchical environments, enabling structured scoping and layered object management.
104
-
105
- With respect to AOP:
106
- - support for before, around, after and error aspects
107
- - simple fluent interface to specify which methods are targeted by an aspect
108
- - sync and async method support
109
-
110
- The library is thread-safe and heavily performance optimized as most of the runtime information is precomputed and cached!
111
-
112
- Let's look at a simple example
113
-
114
- ```python
115
- from aspyx.di import injectable, on_init, on_destroy, module, Environment
116
-
117
-
118
- @injectable()
119
- class Foo:
120
- def __init__(self):
121
- pass
122
-
123
- def hello(self, msg: str):
124
- print(f"hello {msg}")
125
-
126
-
127
- @injectable() # eager and singleton by default
128
- class Bar:
129
- def __init__(self, foo: Foo): # will inject the Foo dependency
130
- self.foo = foo
131
-
132
- @on_init() # a lifecycle callback called after the constructor and all possible injections
133
- def init(self):
134
- ...
135
-
136
-
137
- # this class will discover and manage all - specifically decorated - classes and factories that are part of the own module
138
-
139
- @module()
140
- class SampleModule:
141
- def __init__(self):
142
- pass
143
-
144
-
145
- # create environment
146
-
147
- environment = Environment(SampleModule)
148
-
149
- # fetch an instance
150
-
151
- bar = env.get(Bar)
152
-
153
- bar.foo.hello("world")
154
- ```
155
-
156
- The concepts should be pretty familiar as well as the names as they are inspired by both Spring and Angular.
157
-
158
- Let's add some aspects...
159
-
160
- ```python
161
-
162
- @advice
163
- class SampleAdvice:
164
- def __init__(self): # could inject additional stuff
165
- pass
166
-
167
- @before(methods().named("hello").of_type(Foo))
168
- def call_before(self, invocation: Invocation):
169
- ...
170
-
171
- @error(methods().named("hello").of_type(Foo))
172
- def call_error(self, invocation: Invocation):
173
- ... # exception accessible in invocation.exception
174
-
175
- @around(methods().named("hello"))
176
- def call_around(self, invocation: Invocation):
177
- ...
178
- return invocation.proceed()
179
- ```
180
-
181
- While features like DI and AOP are often associated with enterprise applcations, this example hopefully demonstrates that they work just as well in small- to medium-sized projects—without introducing significant overhead—while still providing powerful tools for achieving clean architecture, resulting in maintainable and easily testable code.
182
-
183
- Let's look at the details
184
-
185
- # Installation
186
-
187
- Just install from PyPI with
188
-
189
- `pip install aspyx`
190
-
191
- The library is tested with all Python version >= 3.9
192
-
193
- # Registration
194
-
195
- Different mechanisms are available that make classes eligible for injection
196
-
197
- ## Class
198
-
199
- Any class annotated with `@injectable` is eligible for injection
200
-
201
- **Example**:
202
-
203
- ```python
204
- @injectable()
205
- class Foo:
206
- def __init__(self):
207
- pass
208
- ```
209
- ⚠️ **Attention:** Please make sure, that the class defines a local constructor, as this is _required_ to determine injected instances.
210
- All referenced types will be injected by the environment.
211
-
212
- Only eligible types are allowed, of course!
213
-
214
- The decorator accepts the keyword arguments
215
- - `eager : boolean`
216
- if `True`, the container will create the instances automatically while booting the environment. This is the default.
217
- - `scope: str`
218
- the name of a - registered - scope which will determine how often instances will be created.
219
-
220
- The following scopes are implemented out of the box:
221
- - `singleton`
222
- objects are created once inside an environment and cached. This is the default.
223
- - `request`
224
- objects are created on every injection request
225
- - `thread`
226
- objects are created and cached with respect to the current thread.
227
-
228
- Other scopes - e.g. session related scopes - can be defined dynamically. Please check the corresponding chapter.
229
-
230
- ## Class Factory
231
-
232
- Classes that implement the `Factory` base class and are annotated with `@factory` will register the appropriate classes returned by the `create` method.
233
-
234
- **Example**:
235
- ```python
236
- @factory()
237
- class TestFactory(Factory[Foo]):
238
- def __init__(self):
239
- pass
240
-
241
- def create(self) -> Foo:
242
- return Foo()
243
- ```
244
-
245
- As in `@injectable`, the same arguments are possible.
246
-
247
- ## Method
248
-
249
- Any `injectable` can define methods decorated with `@create()`, that will create appropriate instances.
250
-
251
- **Example**:
252
- ```python
253
- @injectable()
254
- class Foo:
255
- def __init__(self):
256
- pass
257
-
258
- @create(scope="request")
259
- def create(self) -> Baz:
260
- return Baz()
261
- ```
262
-
263
- The same arguments as in `@injectable` are possible.
264
-
265
- ## Conditional
266
-
267
- All `@injectable` declarations can be supplemented with
268
-
269
- ```python
270
- @conditional(<condition>, ..., <condition>)
271
- ```
272
-
273
- decorators that act as filters in the context of an environment.
274
-
275
- Valid conditions are created by:
276
- - `requires_class(clazz: Type)`
277
- the injectable is valid, if the specified class is registered as well.
278
- - `requires_feature(feature: str)`
279
- the injectable is valid, if the environment defines the specified feature.
280
-
281
- # Environment
282
-
283
- ## Definition
284
-
285
- An `Environment` is the container that manages the lifecycle of objects.
286
- The set of classes and instances is determined by a
287
- constructor type argument called `module`.
288
-
289
- **Example**:
290
- ```python
291
- @module()
292
- class SampleModule:
293
- def __init__(self):
294
- pass
295
- ```
296
-
297
- A module is a regular injectable class decorated with `@module` that controls the discovery of injectable classes, by filtering classes according to their module location relative to this class.
298
- All eligible classes, that are implemented in the containing module or in any submodule will be managed.
299
-
300
- In a second step the real container - the environment - is created based on a module:
301
-
302
- ```python
303
- environment = Environment(SampleModule, features=["dev"])
304
- ```
305
-
306
- By adding the parameter `features: list[str]`, it is possible to filter injectables by evaluating the corresponding `@conditional` decorators.
307
-
308
- **Example**:
309
- ```python
310
-
311
- @injectable()
312
- @conditional(requires_feature("dev"))
313
- class DevOnly:
314
- def __init__(self):
315
- pass
316
-
317
- @module()
318
- class SampleModule():
319
- def __init__(self):
320
- pass
321
-
322
- environment = Environment(SampleModule, features=["dev"])
323
- ```
324
-
325
-
326
- By adding an `imports: list[Type]` parameter, specifying other module types, it will register the appropriate classes recursively.
327
-
328
- **Example**:
329
- ```python
330
- @module()
331
- class SampleModule(imports=[OtherModule]):
332
- def __init__(self):
333
- pass
334
- ```
335
-
336
- Another possibility is to add a parent environment as an `Environment` constructor parameter
337
-
338
- **Example**:
339
- ```python
340
- rootEnvironment = Environment(RootModule)
341
-
342
- environment = Environment(SampleModule, parent=rootEnvironment)
343
- ```
344
-
345
- The difference is, that in the first case, class instances of imported modules will be created in the scope of the _own_ environment, while in the second case, it will return instances managed by the parent.
346
-
347
- The method
348
-
349
- ```shutdown()```
350
-
351
- is used when a container is not needed anymore. It will call any `on_destroy()` of all created instances.
352
-
353
- ## Retrieval
354
-
355
- ```python
356
- def get(type: Type[T]) -> T
357
- ```
358
-
359
- is used to retrieve object instances. Depending on the respective scope it will return either cached instances or newly instantiated objects.
360
-
361
- The container knows about class hierarchies and is able to `get` base classes, as long as there is only one implementation.
362
-
363
- In case of ambiguities, it will throw an exception.
364
-
365
- Note that a base class are not _required_ to be annotated with `@injectable`, as this would mean, that it could be created on its own as well. ( Which is possible as well, btw. )
366
-
367
- # Instantiation logic
368
-
369
- Constructing a new instance involves a number of steps executed in this order
370
- - Constructor call
371
- the constructor is called with the resolved parameters
372
- - Advice injection
373
- All methods involving aspects are updated
374
- - Lifecycle methods
375
- different decorators can mark methods that should be called during the lifecycle ( here the construction ) of an instance.
376
- These are various injection possibilities as well as an optional final `on_init` call
377
- - PostProcessors
378
- Any custom post processors, that can add side effects or modify the instances
379
-
380
- ## Injection methods
381
-
382
- Different decorators are implemented, that call methods with computed values
383
-
384
- - `@inject`
385
- the method is called with all resolved parameter types ( same as the constructor call)
386
- - `@inject_environment`
387
- the method is called with the creating environment as a single parameter
388
- - `@inject_value()`
389
- the method is called with a resolved configuration value. Check the corresponding chapter
390
-
391
- **Example**:
392
- ```python
393
- @injectable()
394
- class Foo:
395
- def __init__(self):
396
- pass
397
-
398
- @inject_environment()
399
- def initEnvironment(self, env: Environment):
400
- ...
401
-
402
- @inject()
403
- def set(self, baz: Baz) -> None:
404
- ...
405
- ```
406
-
407
- ## Lifecycle methods
408
-
409
- It is possible to mark specific lifecyle methods.
410
- - `@on_init()`
411
- called after the constructor and all other injections.
412
- - `@on_running()`
413
- called after an environment has initialized completely ( e.g. created all eager objects ).
414
- - `@on_destroy()`
415
- called during shutdown of the environment
416
-
417
- ## Post Processors
418
-
419
- As part of the instantiation logic it is possible to define post processors that execute any side effect on newly created instances.
420
-
421
- **Example**:
422
- ```python
423
- @injectable()
424
- class SamplePostProcessor(PostProcessor):
425
- def process(self, instance: object, environment: Environment):
426
- print(f"created a {instance}")
427
- ```
428
-
429
- Any implementing class of `PostProcessor` that is eligible for injection will be called by passing the new instance.
430
-
431
- Note that a post processor will only handle instances _after_ its _own_ registration.
432
-
433
- As injectables within a single file will be handled in the order as they are declared, a post processor will only take effect for all classes after its declaration!
434
-
435
- # Custom scopes
436
-
437
- As explained, available scopes are "singleton" and "request".
438
-
439
- It is easily possible to add custom scopes by inheriting the base-class `Scope`, decorating the class with `@scope(<name>)` and overriding the method `get`
440
-
441
- ```python
442
- def get(self, provider: AbstractInstanceProvider, environment: Environment, argProvider: Callable[[],list]):
443
- ```
444
-
445
- Arguments are:
446
- - `provider` the actual provider that will create an instance
447
- - `environment`the requesting environment
448
- - `argProvider` a function that can be called to compute the required arguments recursively
449
-
450
- **Example**: The simplified code of the singleton provider ( disregarding locking logic )
451
-
452
- ```python
453
- @scope("singleton")
454
- class SingletonScope(Scope):
455
- # constructor
456
-
457
- def __init__(self):
458
- super().__init__()
459
-
460
- self.value = None
461
-
462
- # override
463
-
464
- def get(self, provider: AbstractInstanceProvider, environment: Environment, argProvider: Callable[[],list]):
465
- if self.value is None:
466
- self.value = provider.create(environment, *argProvider())
467
-
468
- return self.value
469
- ```
470
-
471
- # AOP
472
-
473
- It is possible to define different aspects, that will be part of method calling flow. This logic fits nicely in the library, since the DI framework controls the instantiation logic and can handle aspects within a regular post processor.
474
-
475
- On the other hand, advices are also regular DI objects, as they will usually require some kind of - injected - context.
476
-
477
- Advices are regular classes decorated with `@advice` that define aspect methods.
478
-
479
- ```python
480
- @advice
481
- class SampleAdvice:
482
- def __init__(self): # could inject dependencies
483
- pass
484
-
485
- @before(methods().named("hello").of_type(Foo))
486
- def call_before(self, invocation: Invocation):
487
- # arguments: invocation.args and invocation.kwargs
488
- ...
489
-
490
- @after(methods().named("hello").of_type(Foo))
491
- def call_after(self, invocation: Invocation):
492
- # arguments: invocation.args and invocation.kwargs
493
- ...
494
-
495
- @error(methods().named("hello").of_type(Foo))
496
- def call_error(self, invocation: Invocation):
497
- # error: invocation.exception
498
- ...
499
-
500
- @around(methods().named("hello"))
501
- def call_around(self, invocation: Invocation):
502
- try:
503
- ...
504
- return invocation.proceed() # will leave a result in invocation.result or invocation.exception in case of an exception
505
- finally:
506
- ...
507
- ```
508
-
509
- Different aspects - with the appropriate decorator - are possible:
510
- - `before`
511
- methods that will be executed _prior_ to the original method
512
- - `around`
513
- methods that will be executed _around_ to the original method allowing you to add side effects or even modify parameters.
514
- - `after`
515
- methods that will be executed _after_ to the original method
516
- - `error`
517
- methods that will be executed in case of a caught exception
518
-
519
- The different aspects can be supplemented with an `@order(<prio>)` decorator that controls the execution order based on the passed number. Smaller values get executed first.
520
-
521
- All methods are expected to have single `Invocation` parameter, that stores
522
-
523
- - `func` the target function
524
- - `args` the supplied args ( including the `self` instance as the first element)
525
- - `kwargs` the keywords args
526
- - `result` the result ( initially `None`)
527
- - `exception` a possible caught exception ( initially `None`)
528
-
529
- ⚠️ **Note:** It is essential for `around` methods to call `proceed()` on the invocation, which will call the next around method in the chain and finally the original method.
530
-
531
- If the `proceed` is called with parameters, they will replace the original parameters!
532
-
533
- **Example**: Parameter modifications
534
-
535
- ```python
536
- @around(methods().named("say"))
537
- def call_around(self, invocation: Invocation):
538
- return invocation.proceed(invocation.args[0], invocation.args[1] + "!") # 0 is self!
539
- ```
540
-
541
- The argument list to the corresponding decorators control which methods are targeted by the advice.
542
-
543
- A fluent interface is used describe the mapping.
544
- The parameters restrict either methods or classes and are constructed by a call to either `methods()` or `classes()`.
545
-
546
- Both add the fluent methods:
547
- - `of_type(type: Type)`
548
- defines the matching classes
549
- - `named(name: str)`
550
- defines method or class names
551
- - `that_are_async()`
552
- defines async methods
553
- - `matches(re: str)`
554
- defines regular expressions for methods or classes
555
- - `decorated_with(type: Type)`
556
- defines decorators on methods or classes
557
-
558
- The fluent methods `named`, `matches` and `of_type` can be called multiple times!
559
-
560
- **Example**: react on both `transactional` decorators on methods or classes
561
-
562
- ```python
563
- @advice
564
- class TransactionAdvice:
565
- def __init__(self):
566
- pass
567
-
568
- @around(methods().decorated_with(transactional), classes().decorated_with(transactional))
569
- def establish_transaction(self, invocation: Invocation):
570
- ...
571
- ```
572
-
573
- With respect to async methods, you need to make sure, to replace a `proceed()` with a `await proceed_async()` to have the overall chain async!
574
-
575
- ## Advice Lifecycle and visibility.
576
-
577
- Advices are always part of a specific environment, and only modify methods of objects managed by exactly this environment.
578
-
579
- An advice of a parent environment will for example not see classes of inherited environments. What is done instead, is to recreate the advice - more technically speaking, a processor that will collect and apply the advices - in every child environment, and let it operate on the local objects. With this approach different environments are completely isolated from each other with no side effects whatsoever.
580
-
581
- # Threading
582
-
583
- A handy decorator `@synchronized` in combination with the respective advice is implemented that automatically synchronizes methods with a `RLock` associated with the instance.
584
-
585
- **Example**:
586
- ```python
587
- @injectable()
588
- class Foo:
589
- def __init__(self):
590
- pass
591
-
592
- @synchronized()
593
- def execute_synchronized(self):
594
- ...
595
- ```
596
-
597
- # Configuration
598
-
599
- It is possible to inject configuration values, by decorating methods with `@inject-value(<name>)` given a configuration key.
600
-
601
- ```python
602
- @injectable()
603
- class Foo:
604
- def __init__(self):
605
- pass
606
-
607
- @inject_value("HOME")
608
- def inject_home(self, os: str):
609
- ...
610
- ```
611
-
612
- If required type coercion will be applied.
613
-
614
- Configuration values are managed centrally using a `ConfigurationManager`, which aggregates values from various configuration sources that are defined as follows.
615
-
616
- ```python
617
- class ConfigurationSource(ABC):
618
- def __init__(self):
619
- pass
620
-
621
- ...
622
-
623
- @abstractmethod
624
- def load(self) -> dict:
625
- ```
626
-
627
- The `load` method is able to return a tree-like structure by returning a `dict`.
628
-
629
- Configuration variables are retrieved with the method
630
-
631
- ```python
632
- def get(self, path: str, type: Type[T], default : Optional[T]=None) -> T:
633
- ```
634
-
635
- - `path`
636
- a '.' separated path
637
- - `type`
638
- the desired type
639
- - `default`
640
- a default, if no value is registered
641
-
642
- Sources can be added dynamically by registering them.
643
-
644
- **Example**:
645
- ```python
646
- @injectable()
647
- class SampleConfigurationSource(ConfigurationSource):
648
- def __init__(self):
649
- super().__init__()
650
-
651
- def load(self) -> dict:
652
- return {
653
- "a": 1,
654
- "b": {
655
- "d": "2",
656
- "e": 3,
657
- "f": 4
658
- }
659
- }
660
- ```
661
-
662
- Two specific source are already implemented:
663
- - `EnvConfigurationSource`
664
- reads the os environment variables
665
- - `YamlConfigurationSource`
666
- reads a specific yaml file
667
-
668
- Typically you create the required configuration sources in an environment class, e.g.
669
-
670
- ```python
671
- @module()
672
- class SampleModule:
673
- # constructor
674
-
675
- def __init__(self):
676
- pass
677
-
678
- @create()
679
- def create_env_source(self) -> EnvConfigurationSource:
680
- return EnvConfigurationSource()
681
-
682
- @create()
683
- def create_yaml_source(self) -> YamlConfigurationSource:
684
- return YamlConfigurationSource("config.yaml")
685
- ```
686
-
687
- # Reflection
688
-
689
- As the library heavily relies on type introspection of classes and methods, a utility class `TypeDescriptor` is available that covers type information on classes.
690
-
691
- After being instantiated with
692
-
693
- ```python
694
- TypeDescriptor.for_type(<type>)
695
- ```
696
-
697
- it offers the methods
698
- - `get_methods(local=False)`
699
- return a list of either local or overall methods
700
- - `get_method(name: str, local=False)`
701
- return a single either local or overall method
702
- - `has_decorator(decorator: Callable) -> bool`
703
- return `True`, if the class is decorated with the specified decorator
704
- - `get_decorator(decorator) -> Optional[DecoratorDescriptor]`
705
- return a descriptor covering the decorator. In addition to the callable, it also stores the supplied args in the `args` property
706
-
707
- The returned method descriptors provide:
708
- - `param_types`
709
- list of arg types
710
- - `return_type`
711
- the return type
712
- - `has_decorator(decorator: Callable) -> bool`
713
- return `True`, if the method is decorated with the specified decorator
714
- - `get_decorator(decorator: Callable) -> Optional[DecoratorDescriptor]`
715
- return a descriptor covering the decorator. In addition to the callable, it also stores the supplied args in the `args` property
716
-
717
- The management of decorators in turn relies on another utility class `Decorators` that caches decorators.
718
-
719
- Whenver you define a custom decorator, you will need to register it accordingly.
720
-
721
- **Example**:
722
- ```python
723
- def transactional(scope):
724
- def decorator(func):
725
- Decorators.add(func, transactional, scope) # also add _all_ parameters in order to cache them
726
- return func
727
-
728
- return decorator
729
- ```
730
-
731
- # Exceptions
732
-
733
- The class `ExceptionManager` is used to collect dynamic handlers for specific exceptions and is able to dispatch to the concrete functions
734
- given a specific exception.
735
-
736
- The handlers are declared by annoting a class with `@exception_handler` and decorating specific methods with `@handle`
737
-
738
- **Example**:
739
- ```python
740
- class DerivedException(Exception):
741
- def __init__(self):
742
- pass
743
-
744
- @module()
745
- class SampleModule:
746
- # constructor
747
-
748
- def __init__(self):
749
- pass
750
-
751
- @create()
752
- def create_exception_manager(self) -> ExceptionManager:
753
- return ExceptionManager()
754
-
755
- @injectable()
756
- @exception_handler()
757
- class TestExceptionHandler:
758
- def __init__(self):
759
- pass
760
-
761
- @handle()
762
- def handle_derived_exception(self, exception: DerivedException):
763
- ExceptionManager.proceed()
764
-
765
- @handle()
766
- def handle_exception(self, exception: Exception):
767
- pass
768
-
769
- @handle()
770
- def handle_base_exception(self, exception: BaseException):
771
- pass
772
-
773
-
774
- @advice
775
- class ExceptionAdvice:
776
- def __init__(self, exceptionManager: ExceptionManager):
777
- self.exceptionManager = exceptionManager
778
-
779
- @error(methods().of_type(Service))
780
- def handle_error(self, invocation: Invocation):
781
- self.exceptionManager.handle(invocation.exception)
782
-
783
- environment = Environment(SampleEnvironment)
784
-
785
- environment.get(ExceptionManager).handle(DerivedException())
786
- ```
787
-
788
- The exception maanger will first call the most appropriate method.
789
- Any `ExceptionManager.proceed()` will in turn call the next most applicable method ( if available).
790
-
791
- Together with a simple around advice we can now add exception handling to any method:
792
-
793
- **Example**:
794
- ```python
795
- @injectable()
796
- class Service:
797
- def __init__(self):
798
- pass
799
-
800
- def throw(self):
801
- raise DerivedException()
802
-
803
- @advice
804
- class ExceptionAdvice:
805
- def __init__(self, exceptionManager: ExceptionManager):
806
- self.exceptionManager = exceptionManager
807
-
808
- @error(methods().of_type(Service))
809
- def handle_error(self, invocation: Invocation):
810
- self.exceptionManager.handle(invocation.exception)
811
- ```
812
-
813
- # Version History
814
-
815
- **1.0.1**
816
-
817
- - some internal refactorings
818
-
819
- **1.1.0**
820
-
821
- - added `@on_running()` callback
822
- - added `thread` scope
823
-
824
- **1.2.0**
825
-
826
- - added `YamlConfigurationSource`
827
-
828
- **1.3.0**
829
-
830
- - added `@conditional`
831
- - added support for `async` advices
832
-
833
-
834
- **1.4.0**
835
-
836
- - bugfixes
837
- - added `@ExceptionManager`
838
-
839
- **1.4.1**
840
-
841
- - mkdocs
842
-
843
-
844
-
845
-
@@ -1 +0,0 @@
1
- aspyx