aspyx 1.1.0__py3-none-any.whl → 1.2.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/__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 InjectorException, AbstractCallableProcessor, LifecycleCallable, Lifecycle, Providers, Environment, ClassInstanceProvider, injectable, factory, environment, inject, create, on_init, on_running, on_destroy, inject_environment, Factory, PostProcessor
4
+ from .di import DIException, AbstractCallableProcessor, LifecycleCallable, Lifecycle, Providers, Environment, ClassInstanceProvider, injectable, factory, environment, inject, order, create, on_init, on_running, on_destroy, inject_environment, Factory, PostProcessor
5
5
 
6
6
  # import something from the subpackages, so that teh decorators are executed
7
7
 
@@ -19,6 +19,7 @@ __all__ = [
19
19
  "environment",
20
20
  "inject",
21
21
  "create",
22
+ "order",
22
23
 
23
24
  "on_init",
24
25
  "on_running",
@@ -28,6 +29,6 @@ __all__ = [
28
29
  "PostProcessor",
29
30
  "AbstractCallableProcessor",
30
31
  "LifecycleCallable",
31
- "InjectorException",
32
+ "DIException",
32
33
  "Lifecycle"
33
34
  ]
aspyx/di/aop/aop.py CHANGED
@@ -533,10 +533,10 @@ class AdviceProcessor(PostProcessor):
533
533
  def process(self, instance: object, environment: Environment):
534
534
  join_point_dict = self.advice.join_points4(instance, environment)
535
535
 
536
- for member, join_points in join_point_dict.items():
536
+ for member, joinPoints in join_point_dict.items():
537
537
  Environment.logger.debug("add aspects for %s:%s", type(instance), member.__name__)
538
538
 
539
539
  def wrap(jp):
540
540
  return lambda *args, **kwargs: Invocation(member, jp).call(*args, **kwargs)
541
541
 
542
- setattr(instance, member.__name__, types.MethodType(wrap(join_points), instance))
542
+ setattr(instance, member.__name__, types.MethodType(wrap(joinPoints), instance))
@@ -1,11 +1,14 @@
1
1
  """
2
2
  Configuration value handling
3
3
  """
4
- from .configuration import ConfigurationManager, ConfigurationSource, EnvConfigurationSource, value
4
+ from .configuration import ConfigurationManager, ConfigurationSource, value
5
+ from .env_configuration_source import EnvConfigurationSource
6
+ from .yaml_configuration_source import YamlConfigurationSource
5
7
 
6
8
  __all__ = [
7
9
  "ConfigurationManager",
8
10
  "ConfigurationSource",
9
11
  "EnvConfigurationSource",
12
+ "YamlConfigurationSource",
10
13
  "value"
11
14
  ]
@@ -4,9 +4,7 @@ Configuration handling module.
4
4
  from __future__ import annotations
5
5
 
6
6
  from abc import ABC, abstractmethod
7
- import os
8
7
  from typing import Optional, Type, TypeVar
9
- from dotenv import load_dotenv
10
8
 
11
9
  from aspyx.di import injectable, Environment, LifecycleCallable, Lifecycle
12
10
  from aspyx.di.di import order, inject
@@ -67,7 +65,7 @@ class ConfigurationManager:
67
65
 
68
66
  def get(self, path: str, type: Type[T], default : Optional[T]=None) -> T:
69
67
  """
70
- Get a configuration value by path and type, with optional coercion.
68
+ Retrieve a configuration value by path and type, with optional coercion.
71
69
  Arguments:
72
70
  path (str): The path to the configuration value, e.g. "database.host".
73
71
  type (Type[T]): The expected type.
@@ -119,54 +117,6 @@ class ConfigurationSource(ABC):
119
117
  return the configuration values of this source as a dictionary.
120
118
  """
121
119
 
122
- @injectable()
123
- class EnvConfigurationSource(ConfigurationSource):
124
- """
125
- EnvConfigurationSource loads all environment variables.
126
- """
127
-
128
- __slots__ = []
129
-
130
- # constructor
131
-
132
- def __init__(self):
133
- super().__init__()
134
-
135
- load_dotenv()
136
-
137
- # implement
138
-
139
- def load(self) -> dict:
140
- def merge_dicts(a, b):
141
- """Recursively merges b into a"""
142
- for key, value in b.items():
143
- if isinstance(value, dict) and key in a and isinstance(a[key], dict):
144
- merge_dicts(a[key], value)
145
- else:
146
- a[key] = value
147
- return a
148
-
149
- def explode_key(key, value):
150
- """Explodes keys with '.' or '/' into nested dictionaries"""
151
- parts = key.replace('/', '.').split('.')
152
- d = current = {}
153
- for part in parts[:-1]:
154
- current[part] = {}
155
- current = current[part]
156
- current[parts[-1]] = value
157
- return d
158
-
159
- exploded = {}
160
-
161
- for key, value in os.environ.items():
162
- if '.' in key or '/' in key:
163
- partial = explode_key(key, value)
164
- merge_dicts(exploded, partial)
165
- else:
166
- exploded[key] = value
167
-
168
- return exploded
169
-
170
120
  # decorator
171
121
 
172
122
  def value(key: str, default=None):
@@ -0,0 +1,55 @@
1
+ """
2
+ EnvConfigurationSource - Loads environment variables as configuration source.
3
+ """
4
+ import os
5
+
6
+ from dotenv import load_dotenv
7
+
8
+ from .configuration import ConfigurationSource
9
+
10
+ class EnvConfigurationSource(ConfigurationSource):
11
+ """
12
+ EnvConfigurationSource loads all environment variables.
13
+ """
14
+
15
+ __slots__ = []
16
+
17
+ # constructor
18
+
19
+ def __init__(self):
20
+ super().__init__()
21
+
22
+ # implement
23
+
24
+ def load(self) -> dict:
25
+ def merge_dicts(a, b):
26
+ """Recursively merges b into a"""
27
+ for key, value in b.items():
28
+ if isinstance(value, dict) and key in a and isinstance(a[key], dict):
29
+ merge_dicts(a[key], value)
30
+ else:
31
+ a[key] = value
32
+ return a
33
+
34
+ def explode_key(key, value):
35
+ """Explodes keys with '.' or '/' into nested dictionaries"""
36
+ parts = key.replace('/', '.').split('.')
37
+ d = current = {}
38
+ for part in parts[:-1]:
39
+ current[part] = {}
40
+ current = current[part]
41
+ current[parts[-1]] = value
42
+ return d
43
+
44
+ exploded = {}
45
+
46
+ load_dotenv()
47
+
48
+ for key, value in os.environ.items():
49
+ if '.' in key or '/' in key:
50
+ partial = explode_key(key, value)
51
+ merge_dicts(exploded, partial)
52
+ else:
53
+ exploded[key] = value
54
+
55
+ return exploded
@@ -0,0 +1,26 @@
1
+ """
2
+ YamlConfigurationSource - Loads variables from a YAML configuration file.
3
+ """
4
+ import yaml
5
+
6
+ from .configuration import ConfigurationSource
7
+
8
+ class YamlConfigurationSource(ConfigurationSource):
9
+ """
10
+ YamlConfigurationSource loads variables from a YAML configuration file.
11
+ """
12
+
13
+ __slots__ = ["file"]
14
+
15
+ # constructor
16
+
17
+ def __init__(self, file: str):
18
+ super().__init__()
19
+
20
+ self.file = file
21
+
22
+ # implement
23
+
24
+ def load(self) -> dict:
25
+ with open(self.file, "r") as file:
26
+ return yaml.safe_load(file)
aspyx/di/di.py CHANGED
@@ -1,5 +1,5 @@
1
1
  """
2
- The deoendency injection module provides a framework for managing dependencies and lifecycle of objects in Python applications.
2
+ The dependency injection module provides a framework for managing dependencies and lifecycle of objects in Python applications.
3
3
  """
4
4
  from __future__ import annotations
5
5
 
@@ -7,7 +7,7 @@ import inspect
7
7
  import logging
8
8
 
9
9
  from abc import abstractmethod, ABC
10
- from enum import Enum, auto
10
+ from enum import Enum
11
11
  import threading
12
12
  from typing import Type, Dict, TypeVar, Generic, Optional, cast, Callable
13
13
 
@@ -27,11 +27,21 @@ class Factory(ABC, Generic[T]):
27
27
  def create(self) -> T:
28
28
  pass
29
29
 
30
- class InjectorException(Exception):
30
+ class DIException(Exception):
31
31
  """
32
32
  Exception raised for errors in the injector.
33
33
  """
34
34
 
35
+ class DIRegistrationException(DIException):
36
+ """
37
+ Exception raised during the registration of dependencies.
38
+ """
39
+
40
+ class DIRuntimeException(DIException):
41
+ """
42
+ Exception raised during the runtime.
43
+ """
44
+
35
45
  class AbstractInstanceProvider(ABC, Generic[T]):
36
46
  """
37
47
  Interface for instance providers.
@@ -40,6 +50,9 @@ class AbstractInstanceProvider(ABC, Generic[T]):
40
50
  def get_module(self) -> str:
41
51
  pass
42
52
 
53
+ def get_host(self) -> Type[T]:
54
+ return type(self)
55
+
43
56
  @abstractmethod
44
57
  def get_type(self) -> Type[T]:
45
58
  pass
@@ -64,6 +77,9 @@ class AbstractInstanceProvider(ABC, Generic[T]):
64
77
  def resolve(self, context: Providers.Context):
65
78
  pass
66
79
 
80
+ def check_factories(self):
81
+ pass
82
+
67
83
 
68
84
  class InstanceProvider(AbstractInstanceProvider):
69
85
  """
@@ -93,6 +109,13 @@ class InstanceProvider(AbstractInstanceProvider):
93
109
 
94
110
  # implement AbstractInstanceProvider
95
111
 
112
+ def get_host(self):
113
+ return self.host
114
+
115
+ def check_factories(self):
116
+ #register_factories(self.host)
117
+ pass
118
+
96
119
  def resolve(self, context: Providers.Context):
97
120
  pass
98
121
 
@@ -188,7 +211,7 @@ class AmbiguousProvider(AbstractInstanceProvider):
188
211
  pass
189
212
 
190
213
  def create(self, environment: Environment, *args):
191
- raise InjectorException(f"multiple candidates for type {self.type}")
214
+ raise DIException(f"multiple candidates for type {self.type}")
192
215
 
193
216
  def __str__(self):
194
217
  return f"AmbiguousProvider({self.type})"
@@ -204,7 +227,7 @@ class Scopes:
204
227
  def get(cls, scope: str, environment: Environment):
205
228
  scope_type = Scopes.scopes.get(scope, None)
206
229
  if scope_type is None:
207
- raise InjectorException(f"unknown scope type {scope}")
230
+ raise DIRegistrationException(f"unknown scope type {scope}")
208
231
 
209
232
  return environment.get(scope_type)
210
233
 
@@ -272,7 +295,7 @@ class EnvironmentInstanceProvider(AbstractInstanceProvider):
272
295
  for dependency in self.provider.get_dependencies():
273
296
  instance_provider = providers.get(dependency.get_type(), None)
274
297
  if instance_provider is None:
275
- raise InjectorException(f"missing import for {dependency.get_type()} ")
298
+ raise DIRegistrationException(f"missing import for {dependency.get_type()} ")
276
299
 
277
300
  self.dependencies.append(instance_provider)
278
301
 
@@ -303,6 +326,9 @@ class ClassInstanceProvider(InstanceProvider):
303
326
 
304
327
  # implement
305
328
 
329
+ def check_factories(self):
330
+ register_factories(self.host)
331
+
306
332
  def resolve(self, context: Providers.Context):
307
333
  context.add(self)
308
334
 
@@ -313,7 +339,7 @@ class ClassInstanceProvider(InstanceProvider):
313
339
 
314
340
  init = TypeDescriptor.for_type(self.type).get_method("__init__")
315
341
  if init is None:
316
- raise InjectorException(f"{self.type.__name__} does not implement __init__")
342
+ raise DIRegistrationException(f"{self.type.__name__} does not implement __init__")
317
343
 
318
344
  for param in init.param_types:
319
345
  provider = Providers.get_provider(param)
@@ -412,7 +438,6 @@ class FactoryInstanceProvider(InstanceProvider):
412
438
  provider = Providers.get_provider(self.host)
413
439
  if self.add_dependency(provider):
414
440
  provider.resolve(context)
415
-
416
441
  else: # check if the dependencies crate a cycle
417
442
  context.add(*self.dependencies)
418
443
 
@@ -490,7 +515,7 @@ class Providers:
490
515
  def add(self, *providers: AbstractInstanceProvider):
491
516
  for provider in providers:
492
517
  if next((p for p in self.dependencies if p.get_type() is provider.get_type()), None) is not None:
493
- raise InjectorException(self.cycle_report(provider))
518
+ raise DIRegistrationException(self.cycle_report(provider))
494
519
 
495
520
  self.dependencies.append(provider)
496
521
 
@@ -542,13 +567,20 @@ class Providers:
542
567
  return True
543
568
 
544
569
  def cache_provider_for_type(provider: AbstractInstanceProvider, type: Type):
570
+ def location(provider: AbstractInstanceProvider):
571
+ host = provider.get_host()
572
+ file = inspect.getfile(host)
573
+ line = inspect.getsourcelines(host)[1]
574
+
575
+ return f"{file}:{line}"
576
+
545
577
  existing_provider = Providers.cache.get(type)
546
578
  if existing_provider is None:
547
579
  Providers.cache[type] = provider
548
580
 
549
581
  else:
550
582
  if type is provider.get_type():
551
- raise InjectorException(f"{type} already registered")
583
+ raise DIRegistrationException(f"{type} already registered in {location(existing_provider)}, override in {location(provider)}")
552
584
 
553
585
  if isinstance(existing_provider, AmbiguousProvider):
554
586
  cast(AmbiguousProvider, existing_provider).add_provider(provider)
@@ -573,10 +605,14 @@ class Providers:
573
605
 
574
606
  @classmethod
575
607
  def resolve(cls):
576
- for provider in Providers.check:
577
- provider.resolve(Providers.Context())
578
608
 
579
- Providers.check.clear()
609
+ for check in Providers.check:
610
+ check.check_factories()
611
+
612
+ # in the loop additional providers may be added
613
+ while len(Providers.check) > 0:
614
+ check = Providers.check.pop(0)
615
+ check.resolve(Providers.Context())
580
616
 
581
617
  @classmethod
582
618
  def report(cls):
@@ -587,7 +623,7 @@ class Providers:
587
623
  def get_provider(cls, type: Type) -> AbstractInstanceProvider:
588
624
  provider = Providers.cache.get(type, None)
589
625
  if provider is None:
590
- raise InjectorException(f"{type.__name__} not registered as injectable")
626
+ raise DIException(f"{type.__name__} not registered as injectable")
591
627
 
592
628
  return provider
593
629
 
@@ -597,7 +633,11 @@ def register_factories(cls: Type):
597
633
  for method in descriptor.get_methods():
598
634
  if method.has_decorator(create):
599
635
  create_decorator = method.get_decorator(create)
600
- Providers.register(FunctionInstanceProvider(cls, method.method, method.return_type, create_decorator.args[0],
636
+ return_type = method.return_type
637
+ if return_type is None:
638
+ raise DIRegistrationException(f"{cls.__name__}.{method.method.__name__} expected to have a return type")
639
+
640
+ Providers.register(FunctionInstanceProvider(cls, method.method, return_type, create_decorator.args[0],
601
641
  create_decorator.args[1]))
602
642
  def order(prio = 0):
603
643
  def decorator(cls):
@@ -686,8 +726,6 @@ def environment(imports: Optional[list[Type]] = None):
686
726
  Decorators.add(cls, environment, imports)
687
727
  Decorators.add(cls, injectable) # do we need that?
688
728
 
689
- register_factories(cls)
690
-
691
729
  return cls
692
730
 
693
731
  return decorator
@@ -745,8 +783,8 @@ class Environment:
745
783
 
746
784
  self.type = env
747
785
  self.parent = parent
748
- if self.parent is None and env is not BootEnvironment:
749
- self.parent = BootEnvironment.get_instance() # inherit environment including its manged instances!
786
+ if self.parent is None and env is not Boot:
787
+ self.parent = Boot.get_environment() # inherit environment including its manged instances!
750
788
 
751
789
  self.providers: Dict[Type, AbstractInstanceProvider] = {}
752
790
  self.lifecycle_processors: list[LifecycleProcessor] = []
@@ -772,7 +810,7 @@ class Environment:
772
810
 
773
811
  # bootstrapping hack, they will be overwritten by the "real" providers
774
812
 
775
- if env is BootEnvironment:
813
+ if env is Boot:
776
814
  add_provider(SingletonScope, SingletonScopeInstanceProvider())
777
815
  add_provider(RequestScope, RequestScopeInstanceProvider())
778
816
 
@@ -786,7 +824,7 @@ class Environment:
786
824
 
787
825
  decorator = TypeDescriptor.for_type(env).get_decorator(environment)
788
826
  if decorator is None:
789
- raise InjectorException(f"{env.__name__} is not an environment class")
827
+ raise DIRegistrationException(f"{env.__name__} is not an environment class")
790
828
 
791
829
  scan = env.__module__
792
830
  if "." in scan:
@@ -921,7 +959,7 @@ class Environment:
921
959
 
922
960
  def get(self, type: Type[T]) -> T:
923
961
  """
924
- Return and possibly create a new instance of the given type.
962
+ Create or return a cached instance for the given type.
925
963
 
926
964
  Arguments:
927
965
  type (Type): The desired type
@@ -931,7 +969,7 @@ class Environment:
931
969
  provider = self.providers.get(type, None)
932
970
  if provider is None:
933
971
  Environment.logger.error("%s is not supported", type)
934
- raise InjectorException(f"{type} is not supported")
972
+ raise DIRuntimeException(f"{type} is not supported")
935
973
 
936
974
  return provider.create(self)
937
975
 
@@ -981,6 +1019,7 @@ class AbstractCallableProcessor(LifecycleProcessor):
981
1019
 
982
1020
  # static data
983
1021
 
1022
+ lock = threading.RLock()
984
1023
  callables : Dict[object, LifecycleCallable] = {}
985
1024
  cache : Dict[Type, list[list[AbstractCallableProcessor.MethodCall]]] = {}
986
1025
 
@@ -1018,8 +1057,11 @@ class AbstractCallableProcessor(LifecycleProcessor):
1018
1057
  def callables_for(cls, type: Type) -> list[list[AbstractCallableProcessor.MethodCall]]:
1019
1058
  callables = AbstractCallableProcessor.cache.get(type, None)
1020
1059
  if callables is None:
1021
- callables = AbstractCallableProcessor.compute_callables(type)
1022
- AbstractCallableProcessor.cache[type] = callables
1060
+ with AbstractCallableProcessor.lock:
1061
+ callables = AbstractCallableProcessor.cache.get(type, None)
1062
+ if callables is None:
1063
+ callables = AbstractCallableProcessor.compute_callables(type)
1064
+ AbstractCallableProcessor.cache[type] = callables
1023
1065
 
1024
1066
  return callables
1025
1067
 
@@ -1164,7 +1206,7 @@ class SingletonScope(Scope):
1164
1206
  self.value = provider.create(environment, *arg_provider())
1165
1207
 
1166
1208
  return self.value
1167
-
1209
+
1168
1210
  @scope("thread")
1169
1211
  class ThreadScope(Scope):
1170
1212
  __slots__ = [
@@ -1185,17 +1227,17 @@ class ThreadScope(Scope):
1185
1227
  # internal class that is required to import technical instance providers
1186
1228
 
1187
1229
  @environment()
1188
- class BootEnvironment:
1230
+ class Boot:
1189
1231
  # class
1190
1232
 
1191
1233
  environment = None
1192
1234
 
1193
1235
  @classmethod
1194
- def get_instance(cls):
1195
- if BootEnvironment.environment is None:
1196
- BootEnvironment.environment = Environment(BootEnvironment)
1236
+ def get_environment(cls):
1237
+ if Boot.environment is None:
1238
+ Boot.environment = Environment(Boot)
1197
1239
 
1198
- return BootEnvironment.environment
1240
+ return Boot.environment
1199
1241
 
1200
1242
  # properties
1201
1243
 
@@ -25,6 +25,9 @@ class DecoratorDescriptor:
25
25
  return f"@{self.decorator.__name__}({','.join(self.args)})"
26
26
 
27
27
  class Decorators:
28
+ """
29
+ Utility class that caches decorators ( Python does not have a feature for this )
30
+ """
28
31
  @classmethod
29
32
  def add(cls, func, decorator, *args):
30
33
  decorators = getattr(func, '__decorators__', None)
@@ -38,9 +41,17 @@ class Decorators:
38
41
  return getattr(func, '__decorators__', [])
39
42
 
40
43
  class TypeDescriptor:
44
+ """
45
+ This class provides a way to introspect Python classes, their methods, decorators, and type hints.
46
+ """
41
47
  # inner class
42
48
 
43
49
  class MethodDescriptor:
50
+ """
51
+ This class represents a method of a class, including its decorators, parameter types, and return type.
52
+ """
53
+ # constructor
54
+
44
55
  def __init__(self, cls, method: Callable):
45
56
  self.clazz = cls
46
57
  self.method = method
@@ -56,6 +67,8 @@ class TypeDescriptor:
56
67
 
57
68
  self.return_type = type_hints.get('return', None)
58
69
 
70
+ # public
71
+
59
72
  def get_decorator(self, decorator: Callable) -> Optional[DecoratorDescriptor]:
60
73
  for dec in self.decorators:
61
74
  if dec.decorator is decorator:
@@ -82,6 +95,9 @@ class TypeDescriptor:
82
95
 
83
96
  @classmethod
84
97
  def for_type(cls, clazz: Type) -> TypeDescriptor:
98
+ """
99
+ Returns a TypeDescriptor for the given class, using a cache to avoid redundant introspection.
100
+ """
85
101
  descriptor = cls._cache.get(clazz)
86
102
  if descriptor is None:
87
103
  with cls._lock:
@@ -126,6 +142,9 @@ class TypeDescriptor:
126
142
  # public
127
143
 
128
144
  def get_decorator(self, decorator: Callable) -> Optional[DecoratorDescriptor]:
145
+ """
146
+ Returns the first decorator of the given type, or None if not found.
147
+ """
129
148
  for dec in self.decorators:
130
149
  if dec.decorator is decorator:
131
150
  return dec
@@ -133,6 +152,8 @@ class TypeDescriptor:
133
152
  return None
134
153
 
135
154
  def has_decorator(self, decorator: Callable) -> bool:
155
+ """
156
+ Checks if the class has a decorator of the given type."""
136
157
  for dec in self.decorators:
137
158
  if dec.decorator is decorator:
138
159
  return True
@@ -140,12 +161,20 @@ class TypeDescriptor:
140
161
  return False
141
162
 
142
163
  def get_methods(self, local = False) -> list[TypeDescriptor.MethodDescriptor]:
164
+ """
165
+ Returns a list of MethodDescriptor objects for the class.
166
+ If local is True, only returns methods defined in the class itself, otherwise includes inherited methods.
167
+ """
143
168
  if local:
144
169
  return list(self.local_methods.values())
145
170
  else:
146
171
  return list(self.methods.values())
147
172
 
148
173
  def get_method(self, name: str, local = False) -> Optional[TypeDescriptor.MethodDescriptor]:
174
+ """
175
+ Returns a MethodDescriptor for the method with the given name.
176
+ If local is True, only searches for methods defined in the class itself, otherwise includes inherited methods.
177
+ """
149
178
  if local:
150
179
  return self.local_methods.get(name, None)
151
180
  else:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aspyx
3
- Version: 1.1.0
3
+ Version: 1.2.0
4
4
  Summary: A DI and AOP library for Python
5
5
  Author-email: Andreas Ernst <andreas.ernst7@gmail.com>
6
6
  License: MIT License
@@ -192,9 +192,9 @@ The decorator accepts the keyword arguments
192
192
  - `singleton`
193
193
  objects are created once inside an environment and cached. This is the default.
194
194
  - `request`
195
- obejcts are created on every injection request
195
+ objects are created on every injection request
196
196
  - `thread`
197
- objects are cerated and cached with respect to the current thread.
197
+ objects are created and cached with respect to the current thread.
198
198
 
199
199
  Other scopes - e.g. session related scopes - can be defined dynamically. Please check the corresponding chapter.
200
200
 
@@ -455,7 +455,7 @@ Both add the fluent methods:
455
455
 
456
456
  The fluent methods `named`, `matches` and `of_type` can be called multiple times!
457
457
 
458
- **Example**:
458
+ **Example**: react on both `transactional` decorators on methods or classes
459
459
 
460
460
  ```python
461
461
  @injectable()
@@ -478,11 +478,13 @@ class Foo:
478
478
  def __init__(self):
479
479
  pass
480
480
 
481
- @value("OS")
482
- def inject_value(self, os: str):
481
+ @inject_value("HOME")
482
+ def inject_home(self, os: str):
483
483
  ...
484
484
  ```
485
485
 
486
+ If required a coercion will be executed.
487
+
486
488
  This concept relies on a central object `ConfigurationManager` that stores the overall configuration values as provided by so called configuration sources that are defined as follows.
487
489
 
488
490
  ```python
@@ -494,14 +496,24 @@ class ConfigurationSource(ABC):
494
496
 
495
497
  @abstractmethod
496
498
  def load(self) -> dict:
497
- pass
498
499
  ```
499
500
 
500
501
  The `load` method is able to return a tree-like structure by returning a `dict`.
501
502
 
502
- As a default environment variables are already supported.
503
+ Configuration variables are retrieved with the method
504
+
505
+ ```python
506
+ def get(self, path: str, type: Type[T], default : Optional[T]=None) -> T:
507
+ ```
508
+
509
+ - `path`
510
+ a '.' separated path
511
+ - `type`
512
+ the desired type
513
+ - `default`
514
+ a default, if no value is registered
503
515
 
504
- Other sources can be added dynamically by just registering them.
516
+ Sources can be added dynamically by registering them.
505
517
 
506
518
  **Example**:
507
519
  ```python
@@ -521,6 +533,31 @@ class SampleConfigurationSource(ConfigurationSource):
521
533
  }
522
534
  ```
523
535
 
536
+ Two specific source are already implemented:
537
+ - `EnvConfigurationSource`
538
+ reads the os environment variables
539
+ - `YamlConfigurationSource`
540
+ reads a specific yaml file
541
+
542
+ Typically you create the required configuration sources in an environment class, e.g.
543
+
544
+ ```python
545
+ @environment()
546
+ class SampleEnvironment:
547
+ # constructor
548
+
549
+ def __init__(self):
550
+ pass
551
+
552
+ @create()
553
+ def create_env_source(self) -> EnvConfigurationSource:
554
+ return EnvConfigurationSource()
555
+
556
+ @create()
557
+ def create_yaml_source(self) -> YamlConfigurationSource:
558
+ return YamlConfigurationSource("config.yaml")
559
+ ```
560
+
524
561
  # Reflection
525
562
 
526
563
  As the library heavily relies on type introspection of classes and methods, a utility class `TypeDescriptor` is available that covers type information on classes.
@@ -537,7 +574,7 @@ it offers the methods
537
574
  - `get_method(name: str, local=False)`
538
575
  return a single either local or overall method
539
576
  - `has_decorator(decorator: Callable) -> bool`
540
- return `True`, if the class is decorated with the specified decrator
577
+ return `True`, if the class is decorated with the specified decorator
541
578
  - `get_decorator(decorator) -> Optional[DecoratorDescriptor]`
542
579
  return a descriptor covering the decorator. In addition to the callable, it also stores the supplied args in the `args` property
543
580
 
@@ -545,9 +582,9 @@ The returned method descriptors offer:
545
582
  - `param_types`
546
583
  list of arg types
547
584
  - `return_type`
548
- the retur type
585
+ the return type
549
586
  - `has_decorator(decorator: Callable) -> bool`
550
- return `True`, if the method is decorated with the specified decrator
587
+ return `True`, if the method is decorated with the specified decorator
551
588
  - `get_decorator(decorator: Callable) -> Optional[DecoratorDescriptor]`
552
589
  return a descriptor covering the decorator. In addition to the callable, it also stores the supplied args in the `args` property
553
590
 
@@ -557,9 +594,9 @@ Whenver you define a custom decorator, you will need to register it accordingly.
557
594
 
558
595
  **Example**:
559
596
  ```python
560
- def transactional():
597
+ def transactional(scope):
561
598
  def decorator(func):
562
- Decorators.add(func, transactional)
599
+ Decorators.add(func, transactional, scope) # also add _all_ parameters in order to cache them
563
600
  return func
564
601
 
565
602
  return decorator
@@ -577,6 +614,10 @@ def transactional():
577
614
  - added `@on_running()` callback
578
615
  - added `thread` scope
579
616
 
617
+ **1.2.0**
618
+
619
+ - added `YamlConfigurationSource`
620
+
580
621
 
581
622
 
582
623
 
@@ -0,0 +1,18 @@
1
+ aspyx/di/__init__.py,sha256=xdb2lsKh00uGMFCWYavhUEMGH15OSeAhUC-iSosqHqU,935
2
+ aspyx/di/di.py,sha256=evfhTznmPNRDjamthSPisMpDhGZJdNmEREUhdJ9nsTE,35571
3
+ aspyx/di/aop/__init__.py,sha256=nOABex49zSyMZ2w1ezwX3Q3yrOcQRSDjDtSj0DwKVbQ,233
4
+ aspyx/di/aop/aop.py,sha256=3GKN6sGlsZbJ7_WSxQvHZNFYouAfU4Eq6H5cBBB_e_4,14455
5
+ aspyx/di/configuration/__init__.py,sha256=mweJ3tZX1YJfY1d4ra-i0TWEcF3EwXBpGbHrKg1Kc6E,380
6
+ aspyx/di/configuration/configuration.py,sha256=KfPjrlUhhmEOUxdJiXePt5RGxKc8JczkWqlEBjpWQTg,4362
7
+ aspyx/di/configuration/env_configuration_source.py,sha256=Xh8g3AuQdgk89nG6GKA4iKILXaqHecD0KqMW2w91hXs,1445
8
+ aspyx/di/configuration/yaml_configuration_source.py,sha256=LM-5J6IRxBBbBAjDeGIFsmiT-61WuGv1sU_zXWNzhkI,549
9
+ aspyx/di/util/__init__.py,sha256=8H2yKkXu3nkRGeTerb8ialzKGfvzUx44XUWFUYcYuQM,125
10
+ aspyx/di/util/stringbuilder.py,sha256=JywkLxZfaQUUWjSB5wvqA6a6Cfs3sW1jbaZ1z4U0-CQ,540
11
+ aspyx/reflection/__init__.py,sha256=r2sNJrfHDpuqaIYu4fTYsoo046gpgn4VTd7bsS3mQJY,282
12
+ aspyx/reflection/proxy.py,sha256=zJ6Psd6zWfFABdrKOf4cULt3gibyqCRdcR6z8WKIkzE,1982
13
+ aspyx/reflection/reflection.py,sha256=2oYJKYysH3ULmTzbXdjGBCB1iaLbWh3IPKSnorVLshU,5719
14
+ aspyx-1.2.0.dist-info/licenses/LICENSE,sha256=n4jfx_MNj7cBtPhhI7MCoB_K35cj1icP9yJ4Rh4vlvY,1070
15
+ aspyx-1.2.0.dist-info/METADATA,sha256=wXSu9Clxg4ZZCoiRAhiZzAR1Wcqtjv4NZie4XilJNSs,19009
16
+ aspyx-1.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
+ aspyx-1.2.0.dist-info/top_level.txt,sha256=A_ZwhBY_ybIgjZlztd44eaOrWqkJAndiqjGlbJ3tR_I,6
18
+ aspyx-1.2.0.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- aspyx/di/__init__.py,sha256=U2-4JnCmSO_IXCnN1jeig9nuWfJN__z9yRzl1WexQJk,927
2
- aspyx/di/di.py,sha256=VVLOXhTKJer2cV0rNEOqMLyMigsNMEQ7xoc-6M7a8wU,34296
3
- aspyx/di/aop/__init__.py,sha256=nOABex49zSyMZ2w1ezwX3Q3yrOcQRSDjDtSj0DwKVbQ,233
4
- aspyx/di/aop/aop.py,sha256=1RUgijk8RsiXWTizfNQX1IfHF7b96kCPdUgahX_J4Io,14457
5
- aspyx/di/configuration/__init__.py,sha256=Zw7h-OlbJD7LyJvzkgyF0EmVqra6oN_Pt0HuUdjTPTA,249
6
- aspyx/di/configuration/configuration.py,sha256=jJJSHRG2wOIvle23Xc8CQ4WM7lp_xCL8-ENVZO603uQ,5682
7
- aspyx/di/util/__init__.py,sha256=8H2yKkXu3nkRGeTerb8ialzKGfvzUx44XUWFUYcYuQM,125
8
- aspyx/di/util/stringbuilder.py,sha256=JywkLxZfaQUUWjSB5wvqA6a6Cfs3sW1jbaZ1z4U0-CQ,540
9
- aspyx/reflection/__init__.py,sha256=r2sNJrfHDpuqaIYu4fTYsoo046gpgn4VTd7bsS3mQJY,282
10
- aspyx/reflection/proxy.py,sha256=zJ6Psd6zWfFABdrKOf4cULt3gibyqCRdcR6z8WKIkzE,1982
11
- aspyx/reflection/reflection.py,sha256=HfPFd_FehTGmgF6NA-IJ_aHePE6GXM1ag7K3h8YIXjI,4600
12
- aspyx-1.1.0.dist-info/licenses/LICENSE,sha256=n4jfx_MNj7cBtPhhI7MCoB_K35cj1icP9yJ4Rh4vlvY,1070
13
- aspyx-1.1.0.dist-info/METADATA,sha256=rdeICMYyQSJmSZDJZrsdw6l5ZVrQrxvAPS_z47ObM-Y,17989
14
- aspyx-1.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
15
- aspyx-1.1.0.dist-info/top_level.txt,sha256=A_ZwhBY_ybIgjZlztd44eaOrWqkJAndiqjGlbJ3tR_I,6
16
- aspyx-1.1.0.dist-info/RECORD,,
File without changes