pico-ioc 2.0.5__py3-none-any.whl → 2.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pico_ioc/__init__.py +9 -12
- pico_ioc/_version.py +1 -1
- pico_ioc/analysis.py +92 -0
- pico_ioc/aop.py +20 -12
- pico_ioc/api.py +63 -1088
- pico_ioc/component_scanner.py +166 -0
- pico_ioc/config_builder.py +87 -0
- pico_ioc/config_registrar.py +236 -0
- pico_ioc/config_runtime.py +54 -13
- pico_ioc/container.py +208 -155
- pico_ioc/decorators.py +193 -0
- pico_ioc/dependency_validator.py +103 -0
- pico_ioc/event_bus.py +24 -25
- pico_ioc/exceptions.py +0 -16
- pico_ioc/factory.py +2 -1
- pico_ioc/locator.py +76 -2
- pico_ioc/provider_selector.py +35 -0
- pico_ioc/registrar.py +188 -0
- pico_ioc/scope.py +13 -4
- {pico_ioc-2.0.5.dist-info → pico_ioc-2.1.1.dist-info}/METADATA +94 -45
- pico_ioc-2.1.1.dist-info/RECORD +25 -0
- pico_ioc-2.0.5.dist-info/RECORD +0 -17
- {pico_ioc-2.0.5.dist-info → pico_ioc-2.1.1.dist-info}/WHEEL +0 -0
- {pico_ioc-2.0.5.dist-info → pico_ioc-2.1.1.dist-info}/licenses/LICENSE +0 -0
- {pico_ioc-2.0.5.dist-info → pico_ioc-2.1.1.dist-info}/top_level.txt +0 -0
pico_ioc/__init__.py
CHANGED
|
@@ -3,7 +3,6 @@ from .constants import LOGGER_NAME, LOGGER, PICO_INFRA, PICO_NAME, PICO_KEY, PIC
|
|
|
3
3
|
from .exceptions import (
|
|
4
4
|
PicoError,
|
|
5
5
|
ProviderNotFoundError,
|
|
6
|
-
CircularDependencyError,
|
|
7
6
|
ComponentCreationError,
|
|
8
7
|
ScopeError,
|
|
9
8
|
ConfigurationError,
|
|
@@ -18,23 +17,20 @@ from .api import (
|
|
|
18
17
|
factory,
|
|
19
18
|
provides,
|
|
20
19
|
Qualifier,
|
|
21
|
-
configuration,
|
|
22
20
|
configure,
|
|
23
21
|
cleanup,
|
|
24
|
-
ConfigSource,
|
|
25
|
-
EnvSource,
|
|
26
|
-
FileSource,
|
|
27
|
-
FlatDictSource,
|
|
28
22
|
init,
|
|
29
23
|
configured,
|
|
30
24
|
)
|
|
25
|
+
from .config_builder import configuration, ContextConfig, EnvSource, FileSource, FlatDictSource, Value
|
|
31
26
|
from .scope import ScopeManager, ContextVarScope, ScopeProtocol, ScopedCaches
|
|
32
27
|
from .locator import ComponentLocator
|
|
33
28
|
from .factory import ComponentFactory, ProviderMetadata, DeferredProvider
|
|
34
29
|
from .aop import MethodCtx, MethodInterceptor, intercepted_by, UnifiedComponentProxy, health, ContainerObserver
|
|
35
30
|
from .container import PicoContainer
|
|
36
31
|
from .event_bus import EventBus, ExecPolicy, ErrorPolicy, Event, subscribe, AutoSubscriberMixin
|
|
37
|
-
from .config_runtime import JsonTreeSource, YamlTreeSource, DictSource, Discriminator
|
|
32
|
+
from .config_runtime import JsonTreeSource, YamlTreeSource, DictSource, Discriminator, Value
|
|
33
|
+
from .analysis import DependencyRequest, analyze_callable_dependencies
|
|
38
34
|
|
|
39
35
|
__all__ = [
|
|
40
36
|
"LOGGER_NAME",
|
|
@@ -45,7 +41,6 @@ __all__ = [
|
|
|
45
41
|
"PICO_META",
|
|
46
42
|
"PicoError",
|
|
47
43
|
"ProviderNotFoundError",
|
|
48
|
-
"CircularDependencyError",
|
|
49
44
|
"ComponentCreationError",
|
|
50
45
|
"ScopeError",
|
|
51
46
|
"ConfigurationError",
|
|
@@ -58,7 +53,6 @@ __all__ = [
|
|
|
58
53
|
"factory",
|
|
59
54
|
"provides",
|
|
60
55
|
"Qualifier",
|
|
61
|
-
"configuration",
|
|
62
56
|
"configure",
|
|
63
57
|
"cleanup",
|
|
64
58
|
"ScopeProtocol",
|
|
@@ -78,10 +72,12 @@ __all__ = [
|
|
|
78
72
|
"PicoContainer",
|
|
79
73
|
"EnvSource",
|
|
80
74
|
"FileSource",
|
|
81
|
-
"ConfigSource",
|
|
82
75
|
"FlatDictSource",
|
|
83
76
|
"init",
|
|
84
77
|
"configured",
|
|
78
|
+
"configuration",
|
|
79
|
+
"ContextConfig",
|
|
80
|
+
"Value",
|
|
85
81
|
"EventBus",
|
|
86
82
|
"ExecPolicy",
|
|
87
83
|
"ErrorPolicy",
|
|
@@ -92,6 +88,7 @@ __all__ = [
|
|
|
92
88
|
"YamlTreeSource",
|
|
93
89
|
"DictSource",
|
|
94
90
|
"Discriminator",
|
|
91
|
+
"Value",
|
|
92
|
+
"DependencyRequest",
|
|
93
|
+
"analyze_callable_dependencies",
|
|
95
94
|
]
|
|
96
|
-
|
|
97
|
-
|
pico_ioc/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = '2.
|
|
1
|
+
__version__ = '2.1.1'
|
pico_ioc/analysis.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Any, Callable, List, Optional, Tuple, Union, get_args, get_origin, Annotated
|
|
4
|
+
from .decorators import Qualifier
|
|
5
|
+
|
|
6
|
+
KeyT = Union[str, type]
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class DependencyRequest:
|
|
10
|
+
parameter_name: str
|
|
11
|
+
key: KeyT
|
|
12
|
+
is_list: bool = False
|
|
13
|
+
qualifier: Optional[str] = None
|
|
14
|
+
is_optional: bool = False
|
|
15
|
+
|
|
16
|
+
def _extract_annotated(ann: Any) -> Tuple[Any, Optional[str]]:
|
|
17
|
+
qualifier = None
|
|
18
|
+
base = ann
|
|
19
|
+
origin = get_origin(ann)
|
|
20
|
+
|
|
21
|
+
if origin is Annotated:
|
|
22
|
+
args = get_args(ann)
|
|
23
|
+
base = args[0] if args else Any
|
|
24
|
+
metas = args[1:] if len(args) > 1 else ()
|
|
25
|
+
for m in metas:
|
|
26
|
+
if isinstance(m, Qualifier):
|
|
27
|
+
qualifier = str(m)
|
|
28
|
+
break
|
|
29
|
+
return base, qualifier
|
|
30
|
+
|
|
31
|
+
def _check_optional(ann: Any) -> Tuple[Any, bool]:
|
|
32
|
+
origin = get_origin(ann)
|
|
33
|
+
if origin is Union:
|
|
34
|
+
args = [a for a in get_args(ann) if a is not type(None)]
|
|
35
|
+
if len(args) == 1:
|
|
36
|
+
return args[0], True
|
|
37
|
+
return ann, False
|
|
38
|
+
|
|
39
|
+
def analyze_callable_dependencies(callable_obj: Callable[..., Any]) -> Tuple[DependencyRequest, ...]:
|
|
40
|
+
try:
|
|
41
|
+
sig = inspect.signature(callable_obj)
|
|
42
|
+
except (ValueError, TypeError):
|
|
43
|
+
return ()
|
|
44
|
+
|
|
45
|
+
plan: List[DependencyRequest] = []
|
|
46
|
+
|
|
47
|
+
for name, param in sig.parameters.items():
|
|
48
|
+
if name in ("self", "cls"):
|
|
49
|
+
continue
|
|
50
|
+
if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
ann = param.annotation
|
|
54
|
+
|
|
55
|
+
base_type, is_optional = _check_optional(ann)
|
|
56
|
+
base_type, qualifier = _extract_annotated(base_type)
|
|
57
|
+
|
|
58
|
+
is_list = False
|
|
59
|
+
elem_t = None
|
|
60
|
+
|
|
61
|
+
origin = get_origin(base_type)
|
|
62
|
+
if origin in (list, List):
|
|
63
|
+
is_list = True
|
|
64
|
+
elem_t = get_args(base_type)[0] if get_args(base_type) else Any
|
|
65
|
+
elem_t, list_qualifier = _extract_annotated(elem_t)
|
|
66
|
+
if qualifier is None:
|
|
67
|
+
qualifier = list_qualifier
|
|
68
|
+
|
|
69
|
+
final_key: KeyT
|
|
70
|
+
if is_list:
|
|
71
|
+
final_key = elem_t if isinstance(elem_t, type) else Any
|
|
72
|
+
elif isinstance(base_type, type):
|
|
73
|
+
final_key = base_type
|
|
74
|
+
elif isinstance(base_type, str):
|
|
75
|
+
final_key = base_type
|
|
76
|
+
elif ann is inspect._empty:
|
|
77
|
+
final_key = name
|
|
78
|
+
else:
|
|
79
|
+
final_key = base_type
|
|
80
|
+
|
|
81
|
+
plan.append(
|
|
82
|
+
DependencyRequest(
|
|
83
|
+
parameter_name=name,
|
|
84
|
+
key=final_key,
|
|
85
|
+
is_list=is_list,
|
|
86
|
+
qualifier=qualifier,
|
|
87
|
+
is_optional=is_optional or (param.default is not inspect._empty)
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return tuple(plan)
|
|
92
|
+
|
pico_ioc/aop.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# src/pico_ioc/aop.py
|
|
2
|
+
|
|
2
3
|
import inspect
|
|
3
4
|
import pickle
|
|
4
5
|
import threading
|
|
5
6
|
from typing import Any, Callable, Dict, List, Tuple, Protocol, Union
|
|
6
|
-
from .exceptions import SerializationError
|
|
7
|
+
from .exceptions import SerializationError, AsyncResolutionError
|
|
7
8
|
|
|
8
9
|
KeyT = Union[str, type]
|
|
9
10
|
|
|
@@ -120,9 +121,19 @@ class UnifiedComponentProxy:
|
|
|
120
121
|
tgt = creator()
|
|
121
122
|
if tgt is None:
|
|
122
123
|
raise RuntimeError("UnifiedComponentProxy object_creator returned None")
|
|
124
|
+
|
|
125
|
+
container = object.__getattribute__(self, "_container")
|
|
126
|
+
if container and hasattr(container, "_run_configure_methods"):
|
|
127
|
+
res = container._run_configure_methods(tgt)
|
|
128
|
+
if inspect.isawaitable(res):
|
|
129
|
+
raise AsyncResolutionError(
|
|
130
|
+
f"Lazy component {type(tgt).__name__} requires async "
|
|
131
|
+
"@configure but was resolved via sync get()"
|
|
132
|
+
)
|
|
133
|
+
|
|
123
134
|
object.__setattr__(self, "_target", tgt)
|
|
124
135
|
return tgt
|
|
125
|
-
|
|
136
|
+
|
|
126
137
|
def _scope_signature(self) -> Tuple[Any, ...]:
|
|
127
138
|
container = object.__getattribute__(self, "_container")
|
|
128
139
|
target = object.__getattribute__(self, "_target")
|
|
@@ -139,7 +150,7 @@ class UnifiedComponentProxy:
|
|
|
139
150
|
return ()
|
|
140
151
|
return (container.scopes.get_id(sc),)
|
|
141
152
|
return ()
|
|
142
|
-
|
|
153
|
+
|
|
143
154
|
def _build_wrapped(self, name: str, bound: Callable[..., Any], interceptors_cls: Tuple[type, ...]):
|
|
144
155
|
container = object.__getattribute__(self, "_container")
|
|
145
156
|
interceptors = [container.get(cls) for cls in interceptors_cls]
|
|
@@ -148,6 +159,7 @@ class UnifiedComponentProxy:
|
|
|
148
159
|
original_func = bound
|
|
149
160
|
if hasattr(bound, '__func__'):
|
|
150
161
|
original_func = bound.__func__
|
|
162
|
+
|
|
151
163
|
if inspect.iscoroutinefunction(original_func):
|
|
152
164
|
async def aw(*args, **kwargs):
|
|
153
165
|
ctx = MethodCtx(
|
|
@@ -182,16 +194,17 @@ class UnifiedComponentProxy:
|
|
|
182
194
|
raise RuntimeError(f"Async interceptor returned awaitable on sync method: {name}")
|
|
183
195
|
return res
|
|
184
196
|
return sig, sw, interceptors_cls
|
|
185
|
-
|
|
197
|
+
|
|
186
198
|
@property
|
|
187
199
|
def __class__(self):
|
|
188
200
|
return self._get_real_object().__class__
|
|
189
|
-
|
|
201
|
+
|
|
190
202
|
def __getattr__(self, name: str) -> Any:
|
|
191
203
|
target = self._get_real_object()
|
|
192
204
|
attr = getattr(target, name)
|
|
193
205
|
if not callable(attr):
|
|
194
206
|
return attr
|
|
207
|
+
|
|
195
208
|
interceptors_cls = _gather_interceptors_for_method(type(target), name)
|
|
196
209
|
if not interceptors_cls:
|
|
197
210
|
return attr
|
|
@@ -201,20 +214,16 @@ class UnifiedComponentProxy:
|
|
|
201
214
|
cache: Dict[str, Tuple[Tuple[Any, ...], Callable[..., Any], Tuple[type, ...]]] = object.__getattribute__(self, "_cache")
|
|
202
215
|
cur_sig = self._scope_signature()
|
|
203
216
|
cached = cache.get(name)
|
|
217
|
+
|
|
204
218
|
if cached is not None:
|
|
205
219
|
sig, wrapped, cls_tuple = cached
|
|
206
220
|
if sig == cur_sig and cls_tuple == interceptors_cls:
|
|
207
221
|
return wrapped
|
|
208
222
|
|
|
209
|
-
cached = cache.get(name)
|
|
210
|
-
if cached is not None:
|
|
211
|
-
sig, wrapped, cls_tuple = cached
|
|
212
|
-
if sig == cur_sig and cls_tuple == interceptors_cls:
|
|
213
|
-
return wrapped
|
|
214
223
|
sig, wrapped, cls_tuple = self._build_wrapped(name, attr, interceptors_cls)
|
|
215
224
|
cache[name] = (sig, wrapped, cls_tuple)
|
|
216
225
|
return wrapped
|
|
217
|
-
|
|
226
|
+
|
|
218
227
|
def __setattr__(self, name, value): setattr(self._get_real_object(), name, value)
|
|
219
228
|
def __delattr__(self, name): delattr(self._get_real_object(), name)
|
|
220
229
|
def __str__(self): return str(self._get_real_object())
|
|
@@ -278,4 +287,3 @@ class UnifiedComponentProxy:
|
|
|
278
287
|
return (pickle.loads, (data,))
|
|
279
288
|
except Exception as e:
|
|
280
289
|
raise SerializationError(f"Proxy target is not serializable: {e}")
|
|
281
|
-
|