aspyx 1.1.0__tar.gz → 1.2.0__tar.gz
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-1.1.0/src/aspyx.egg-info → aspyx-1.2.0}/PKG-INFO +55 -14
- {aspyx-1.1.0 → aspyx-1.2.0}/README.md +54 -13
- {aspyx-1.1.0 → aspyx-1.2.0}/pyproject.toml +1 -1
- {aspyx-1.1.0 → aspyx-1.2.0}/src/aspyx/di/__init__.py +3 -2
- {aspyx-1.1.0 → aspyx-1.2.0}/src/aspyx/di/aop/aop.py +2 -2
- {aspyx-1.1.0 → aspyx-1.2.0}/src/aspyx/di/configuration/__init__.py +4 -1
- {aspyx-1.1.0 → aspyx-1.2.0}/src/aspyx/di/configuration/configuration.py +1 -51
- aspyx-1.2.0/src/aspyx/di/configuration/env_configuration_source.py +55 -0
- aspyx-1.2.0/src/aspyx/di/configuration/yaml_configuration_source.py +26 -0
- {aspyx-1.1.0 → aspyx-1.2.0}/src/aspyx/di/di.py +73 -31
- {aspyx-1.1.0 → aspyx-1.2.0}/src/aspyx/reflection/reflection.py +29 -0
- {aspyx-1.1.0 → aspyx-1.2.0/src/aspyx.egg-info}/PKG-INFO +55 -14
- {aspyx-1.1.0 → aspyx-1.2.0}/src/aspyx.egg-info/SOURCES.txt +2 -0
- {aspyx-1.1.0 → aspyx-1.2.0}/tests/test_configuration.py +31 -2
- {aspyx-1.1.0 → aspyx-1.2.0}/tests/test_di.py +13 -7
- {aspyx-1.1.0 → aspyx-1.2.0}/tests/test_di_cycle.py +2 -2
- {aspyx-1.1.0 → aspyx-1.2.0}/LICENSE +0 -0
- {aspyx-1.1.0 → aspyx-1.2.0}/setup.cfg +0 -0
- {aspyx-1.1.0 → aspyx-1.2.0}/src/aspyx/di/aop/__init__.py +0 -0
- {aspyx-1.1.0 → aspyx-1.2.0}/src/aspyx/di/util/__init__.py +0 -0
- {aspyx-1.1.0 → aspyx-1.2.0}/src/aspyx/di/util/stringbuilder.py +0 -0
- {aspyx-1.1.0 → aspyx-1.2.0}/src/aspyx/reflection/__init__.py +0 -0
- {aspyx-1.1.0 → aspyx-1.2.0}/src/aspyx/reflection/proxy.py +0 -0
- {aspyx-1.1.0 → aspyx-1.2.0}/src/aspyx.egg-info/dependency_links.txt +0 -0
- {aspyx-1.1.0 → aspyx-1.2.0}/src/aspyx.egg-info/top_level.txt +0 -0
- {aspyx-1.1.0 → aspyx-1.2.0}/tests/test_aop.py +0 -0
- {aspyx-1.1.0 → aspyx-1.2.0}/tests/test_proxy.py +0 -0
- {aspyx-1.1.0 → aspyx-1.2.0}/tests/test_reflection.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aspyx
|
|
3
|
-
Version: 1.
|
|
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
|
-
|
|
195
|
+
objects are created on every injection request
|
|
196
196
|
- `thread`
|
|
197
|
-
objects are
|
|
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
|
-
@
|
|
482
|
-
def
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
585
|
+
the return type
|
|
549
586
|
- `has_decorator(decorator: Callable) -> bool`
|
|
550
|
-
return `True`, if the method is decorated with the specified
|
|
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
|
|
|
@@ -160,9 +160,9 @@ The decorator accepts the keyword arguments
|
|
|
160
160
|
- `singleton`
|
|
161
161
|
objects are created once inside an environment and cached. This is the default.
|
|
162
162
|
- `request`
|
|
163
|
-
|
|
163
|
+
objects are created on every injection request
|
|
164
164
|
- `thread`
|
|
165
|
-
objects are
|
|
165
|
+
objects are created and cached with respect to the current thread.
|
|
166
166
|
|
|
167
167
|
Other scopes - e.g. session related scopes - can be defined dynamically. Please check the corresponding chapter.
|
|
168
168
|
|
|
@@ -423,7 +423,7 @@ Both add the fluent methods:
|
|
|
423
423
|
|
|
424
424
|
The fluent methods `named`, `matches` and `of_type` can be called multiple times!
|
|
425
425
|
|
|
426
|
-
**Example**:
|
|
426
|
+
**Example**: react on both `transactional` decorators on methods or classes
|
|
427
427
|
|
|
428
428
|
```python
|
|
429
429
|
@injectable()
|
|
@@ -446,11 +446,13 @@ class Foo:
|
|
|
446
446
|
def __init__(self):
|
|
447
447
|
pass
|
|
448
448
|
|
|
449
|
-
@
|
|
450
|
-
def
|
|
449
|
+
@inject_value("HOME")
|
|
450
|
+
def inject_home(self, os: str):
|
|
451
451
|
...
|
|
452
452
|
```
|
|
453
453
|
|
|
454
|
+
If required a coercion will be executed.
|
|
455
|
+
|
|
454
456
|
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.
|
|
455
457
|
|
|
456
458
|
```python
|
|
@@ -462,14 +464,24 @@ class ConfigurationSource(ABC):
|
|
|
462
464
|
|
|
463
465
|
@abstractmethod
|
|
464
466
|
def load(self) -> dict:
|
|
465
|
-
pass
|
|
466
467
|
```
|
|
467
468
|
|
|
468
469
|
The `load` method is able to return a tree-like structure by returning a `dict`.
|
|
469
470
|
|
|
470
|
-
|
|
471
|
+
Configuration variables are retrieved with the method
|
|
472
|
+
|
|
473
|
+
```python
|
|
474
|
+
def get(self, path: str, type: Type[T], default : Optional[T]=None) -> T:
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
- `path`
|
|
478
|
+
a '.' separated path
|
|
479
|
+
- `type`
|
|
480
|
+
the desired type
|
|
481
|
+
- `default`
|
|
482
|
+
a default, if no value is registered
|
|
471
483
|
|
|
472
|
-
|
|
484
|
+
Sources can be added dynamically by registering them.
|
|
473
485
|
|
|
474
486
|
**Example**:
|
|
475
487
|
```python
|
|
@@ -489,6 +501,31 @@ class SampleConfigurationSource(ConfigurationSource):
|
|
|
489
501
|
}
|
|
490
502
|
```
|
|
491
503
|
|
|
504
|
+
Two specific source are already implemented:
|
|
505
|
+
- `EnvConfigurationSource`
|
|
506
|
+
reads the os environment variables
|
|
507
|
+
- `YamlConfigurationSource`
|
|
508
|
+
reads a specific yaml file
|
|
509
|
+
|
|
510
|
+
Typically you create the required configuration sources in an environment class, e.g.
|
|
511
|
+
|
|
512
|
+
```python
|
|
513
|
+
@environment()
|
|
514
|
+
class SampleEnvironment:
|
|
515
|
+
# constructor
|
|
516
|
+
|
|
517
|
+
def __init__(self):
|
|
518
|
+
pass
|
|
519
|
+
|
|
520
|
+
@create()
|
|
521
|
+
def create_env_source(self) -> EnvConfigurationSource:
|
|
522
|
+
return EnvConfigurationSource()
|
|
523
|
+
|
|
524
|
+
@create()
|
|
525
|
+
def create_yaml_source(self) -> YamlConfigurationSource:
|
|
526
|
+
return YamlConfigurationSource("config.yaml")
|
|
527
|
+
```
|
|
528
|
+
|
|
492
529
|
# Reflection
|
|
493
530
|
|
|
494
531
|
As the library heavily relies on type introspection of classes and methods, a utility class `TypeDescriptor` is available that covers type information on classes.
|
|
@@ -505,7 +542,7 @@ it offers the methods
|
|
|
505
542
|
- `get_method(name: str, local=False)`
|
|
506
543
|
return a single either local or overall method
|
|
507
544
|
- `has_decorator(decorator: Callable) -> bool`
|
|
508
|
-
return `True`, if the class is decorated with the specified
|
|
545
|
+
return `True`, if the class is decorated with the specified decorator
|
|
509
546
|
- `get_decorator(decorator) -> Optional[DecoratorDescriptor]`
|
|
510
547
|
return a descriptor covering the decorator. In addition to the callable, it also stores the supplied args in the `args` property
|
|
511
548
|
|
|
@@ -513,9 +550,9 @@ The returned method descriptors offer:
|
|
|
513
550
|
- `param_types`
|
|
514
551
|
list of arg types
|
|
515
552
|
- `return_type`
|
|
516
|
-
the
|
|
553
|
+
the return type
|
|
517
554
|
- `has_decorator(decorator: Callable) -> bool`
|
|
518
|
-
return `True`, if the method is decorated with the specified
|
|
555
|
+
return `True`, if the method is decorated with the specified decorator
|
|
519
556
|
- `get_decorator(decorator: Callable) -> Optional[DecoratorDescriptor]`
|
|
520
557
|
return a descriptor covering the decorator. In addition to the callable, it also stores the supplied args in the `args` property
|
|
521
558
|
|
|
@@ -525,9 +562,9 @@ Whenver you define a custom decorator, you will need to register it accordingly.
|
|
|
525
562
|
|
|
526
563
|
**Example**:
|
|
527
564
|
```python
|
|
528
|
-
def transactional():
|
|
565
|
+
def transactional(scope):
|
|
529
566
|
def decorator(func):
|
|
530
|
-
Decorators.add(func, transactional)
|
|
567
|
+
Decorators.add(func, transactional, scope) # also add _all_ parameters in order to cache them
|
|
531
568
|
return func
|
|
532
569
|
|
|
533
570
|
return decorator
|
|
@@ -545,6 +582,10 @@ def transactional():
|
|
|
545
582
|
- added `@on_running()` callback
|
|
546
583
|
- added `thread` scope
|
|
547
584
|
|
|
585
|
+
**1.2.0**
|
|
586
|
+
|
|
587
|
+
- added `YamlConfigurationSource`
|
|
588
|
+
|
|
548
589
|
|
|
549
590
|
|
|
550
591
|
|
|
@@ -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
|
|
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
|
-
"
|
|
32
|
+
"DIException",
|
|
32
33
|
"Lifecycle"
|
|
33
34
|
]
|
|
@@ -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,
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
The
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
749
|
-
self.parent =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1022
|
-
|
|
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
|
|
1230
|
+
class Boot:
|
|
1189
1231
|
# class
|
|
1190
1232
|
|
|
1191
1233
|
environment = None
|
|
1192
1234
|
|
|
1193
1235
|
@classmethod
|
|
1194
|
-
def
|
|
1195
|
-
if
|
|
1196
|
-
|
|
1236
|
+
def get_environment(cls):
|
|
1237
|
+
if Boot.environment is None:
|
|
1238
|
+
Boot.environment = Environment(Boot)
|
|
1197
1239
|
|
|
1198
|
-
return
|
|
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.
|
|
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
|
-
|
|
195
|
+
objects are created on every injection request
|
|
196
196
|
- `thread`
|
|
197
|
-
objects are
|
|
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
|
-
@
|
|
482
|
-
def
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
585
|
+
the return type
|
|
549
586
|
- `has_decorator(decorator: Callable) -> bool`
|
|
550
|
-
return `True`, if the method is decorated with the specified
|
|
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
|
|
|
@@ -11,6 +11,8 @@ src/aspyx/di/aop/__init__.py
|
|
|
11
11
|
src/aspyx/di/aop/aop.py
|
|
12
12
|
src/aspyx/di/configuration/__init__.py
|
|
13
13
|
src/aspyx/di/configuration/configuration.py
|
|
14
|
+
src/aspyx/di/configuration/env_configuration_source.py
|
|
15
|
+
src/aspyx/di/configuration/yaml_configuration_source.py
|
|
14
16
|
src/aspyx/di/util/__init__.py
|
|
15
17
|
src/aspyx/di/util/stringbuilder.py
|
|
16
18
|
src/aspyx/reflection/__init__.py
|
|
@@ -4,17 +4,30 @@ Test cases for the Configuration system in aspyx.di
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
6
|
import unittest
|
|
7
|
+
from pathlib import Path
|
|
7
8
|
|
|
8
|
-
from aspyx.di.configuration import ConfigurationSource, ConfigurationManager, value
|
|
9
|
+
from aspyx.di.configuration import ConfigurationSource, ConfigurationManager, value, EnvConfigurationSource, \
|
|
10
|
+
YamlConfigurationSource
|
|
9
11
|
|
|
10
|
-
from aspyx.di import injectable, Environment, environment
|
|
12
|
+
from aspyx.di import injectable, Environment, environment, create
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
@environment()
|
|
14
16
|
class SampleEnvironment:
|
|
17
|
+
# constructor
|
|
18
|
+
|
|
15
19
|
def __init__(self):
|
|
16
20
|
pass
|
|
17
21
|
|
|
22
|
+
@create()
|
|
23
|
+
def create_env_source(self) -> EnvConfigurationSource:
|
|
24
|
+
return EnvConfigurationSource()
|
|
25
|
+
|
|
26
|
+
@create()
|
|
27
|
+
def create_yaml_source(self) -> YamlConfigurationSource:
|
|
28
|
+
return YamlConfigurationSource(f"{Path(__file__).parent}/config.yaml"
|
|
29
|
+
)
|
|
30
|
+
|
|
18
31
|
@injectable()
|
|
19
32
|
class SampleConfigurationSource1(ConfigurationSource):
|
|
20
33
|
# constructor
|
|
@@ -67,6 +80,22 @@ class Foo:
|
|
|
67
80
|
class TestConfiguration(unittest.TestCase):
|
|
68
81
|
testEnvironment = Environment(SampleEnvironment)
|
|
69
82
|
|
|
83
|
+
def test_yaml(self):
|
|
84
|
+
env = TestConfiguration.testEnvironment
|
|
85
|
+
|
|
86
|
+
config = env.get(ConfigurationManager)
|
|
87
|
+
|
|
88
|
+
server = config.get("server.name", str)
|
|
89
|
+
self.assertEqual(server, "bla")
|
|
90
|
+
|
|
91
|
+
def test_env(self):
|
|
92
|
+
env = TestConfiguration.testEnvironment
|
|
93
|
+
|
|
94
|
+
config = env.get(ConfigurationManager)
|
|
95
|
+
|
|
96
|
+
os = config.get("HOME", str)
|
|
97
|
+
self.assertIsNotNone(os)
|
|
98
|
+
|
|
70
99
|
def test_configuration(self):
|
|
71
100
|
env = TestConfiguration.testEnvironment
|
|
72
101
|
|
|
@@ -8,8 +8,7 @@ import logging
|
|
|
8
8
|
import unittest
|
|
9
9
|
from typing import Dict
|
|
10
10
|
|
|
11
|
-
from aspyx.di import
|
|
12
|
-
from aspyx.di.di import order, on_running
|
|
11
|
+
from aspyx.di import DIException, injectable, order, on_init, on_running, on_destroy, inject_environment, inject, Factory, create, environment, Environment, PostProcessor, factory
|
|
13
12
|
|
|
14
13
|
from .di_import import ImportedEnvironment
|
|
15
14
|
|
|
@@ -26,15 +25,16 @@ def configure_logging(levels: Dict[str, int]) -> None:
|
|
|
26
25
|
|
|
27
26
|
#configure_logging({"aspyx": logging.DEBUG})
|
|
28
27
|
|
|
29
|
-
# not here
|
|
30
|
-
|
|
31
|
-
|
|
32
28
|
@injectable()
|
|
33
29
|
@order(10)
|
|
34
30
|
class SamplePostProcessor(PostProcessor):
|
|
35
31
|
def process(self, instance: object, environment: Environment):
|
|
36
32
|
pass #print(f"created a {instance}")
|
|
37
33
|
|
|
34
|
+
class Baa:
|
|
35
|
+
def init(self):
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
38
|
class Foo:
|
|
39
39
|
def __init__(self):
|
|
40
40
|
self.inited = False
|
|
@@ -96,6 +96,10 @@ class Bar(Base):
|
|
|
96
96
|
self.destroyed = False
|
|
97
97
|
self.environment = None
|
|
98
98
|
|
|
99
|
+
@create()
|
|
100
|
+
def create_baa(self) -> Baa:
|
|
101
|
+
return Baa()
|
|
102
|
+
|
|
99
103
|
@on_init()
|
|
100
104
|
def init(self):
|
|
101
105
|
self.inited = True
|
|
@@ -167,12 +171,12 @@ class TestDI(unittest.TestCase):
|
|
|
167
171
|
self.assertEqual(type(base), Bar)
|
|
168
172
|
|
|
169
173
|
def test_inject_ambiguous_class(self):
|
|
170
|
-
with self.assertRaises(
|
|
174
|
+
with self.assertRaises(DIException):
|
|
171
175
|
env = TestDI.testEnvironment
|
|
172
176
|
env.get(Ambiguous)
|
|
173
177
|
|
|
174
178
|
def test_create_unknown(self):
|
|
175
|
-
with self.assertRaises(
|
|
179
|
+
with self.assertRaises(DIException):
|
|
176
180
|
env = TestDI.testEnvironment
|
|
177
181
|
env.get(Unknown)
|
|
178
182
|
|
|
@@ -197,7 +201,9 @@ class TestDI(unittest.TestCase):
|
|
|
197
201
|
def test_create_factory(self):
|
|
198
202
|
env = TestDI.testEnvironment
|
|
199
203
|
baz = env.get(Baz)
|
|
204
|
+
baa= env.get(Baa)
|
|
200
205
|
self.assertIsNotNone(baz)
|
|
206
|
+
self.assertIsNotNone(baa)
|
|
201
207
|
|
|
202
208
|
def test_singleton(self):
|
|
203
209
|
env = TestDI.testEnvironment
|
|
@@ -6,7 +6,7 @@ from __future__ import annotations
|
|
|
6
6
|
import unittest
|
|
7
7
|
|
|
8
8
|
from aspyx.di import injectable, environment, Environment
|
|
9
|
-
from aspyx.di.di import
|
|
9
|
+
from aspyx.di.di import DIException
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
@injectable()
|
|
@@ -28,7 +28,7 @@ class SampleEnvironment:
|
|
|
28
28
|
|
|
29
29
|
class TestCycle(unittest.TestCase):
|
|
30
30
|
def test_cycle(self):
|
|
31
|
-
with self.assertRaises(
|
|
31
|
+
with self.assertRaises(DIException):
|
|
32
32
|
Environment(SampleEnvironment)
|
|
33
33
|
|
|
34
34
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|