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/api.py
CHANGED
|
@@ -1,505 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import inspect
|
|
4
|
-
import functools
|
|
1
|
+
# src/pico_ioc/api.py
|
|
2
|
+
|
|
5
3
|
import importlib
|
|
6
4
|
import pkgutil
|
|
7
5
|
import logging
|
|
8
|
-
|
|
9
|
-
from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union
|
|
10
|
-
from .
|
|
11
|
-
from .
|
|
12
|
-
ProviderNotFoundError,
|
|
13
|
-
CircularDependencyError,
|
|
14
|
-
ScopeError,
|
|
15
|
-
ConfigurationError,
|
|
16
|
-
SerializationError,
|
|
17
|
-
InvalidBindingError,
|
|
18
|
-
)
|
|
19
|
-
from .factory import ComponentFactory, ProviderMetadata, DeferredProvider
|
|
6
|
+
import inspect
|
|
7
|
+
from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union
|
|
8
|
+
from .exceptions import ConfigurationError, InvalidBindingError
|
|
9
|
+
from .factory import ComponentFactory, ProviderMetadata
|
|
20
10
|
from .locator import ComponentLocator
|
|
21
11
|
from .scope import ScopeManager, ScopedCaches
|
|
22
12
|
from .container import PicoContainer
|
|
23
|
-
from .
|
|
13
|
+
from .decorators import component, factory, provides, Qualifier, configure, cleanup, configured
|
|
14
|
+
from .config_builder import ContextConfig, configuration
|
|
15
|
+
from .registrar import Registrar
|
|
24
16
|
|
|
25
17
|
KeyT = Union[str, type]
|
|
26
18
|
Provider = Callable[[], Any]
|
|
27
19
|
|
|
28
|
-
class ConfigSource(Protocol):
|
|
29
|
-
def get(self, key: str) -> Optional[str]: ...
|
|
30
|
-
|
|
31
|
-
class EnvSource:
|
|
32
|
-
def __init__(self, prefix: str = "") -> None:
|
|
33
|
-
self.prefix = prefix
|
|
34
|
-
def get(self, key: str) -> Optional[str]:
|
|
35
|
-
return os.environ.get(self.prefix + key)
|
|
36
|
-
|
|
37
|
-
class FileSource:
|
|
38
|
-
def __init__(self, path: str, prefix: str = "") -> None:
|
|
39
|
-
self.prefix = prefix
|
|
40
|
-
try:
|
|
41
|
-
with open(path, "r", encoding="utf-8") as f:
|
|
42
|
-
self._data = json.load(f)
|
|
43
|
-
except Exception:
|
|
44
|
-
self._data = {}
|
|
45
|
-
def get(self, key: str) -> Optional[str]:
|
|
46
|
-
k = self.prefix + key
|
|
47
|
-
v = self._data
|
|
48
|
-
for part in k.split("__"):
|
|
49
|
-
if isinstance(v, dict) and part in v:
|
|
50
|
-
v = v[part]
|
|
51
|
-
else:
|
|
52
|
-
return None
|
|
53
|
-
if isinstance(v, (str, int, float, bool)):
|
|
54
|
-
return str(v)
|
|
55
|
-
return None
|
|
56
|
-
|
|
57
|
-
class FlatDictSource(ConfigSource):
|
|
58
|
-
def __init__(self, data: Mapping[str, Any], prefix: str = "", case_sensitive: bool = True):
|
|
59
|
-
base = dict(data)
|
|
60
|
-
if case_sensitive:
|
|
61
|
-
self._data = {str(k): v for k, v in base.items()}
|
|
62
|
-
self._prefix = prefix
|
|
63
|
-
else:
|
|
64
|
-
self._data = {str(k).upper(): v for k, v in base.items()}
|
|
65
|
-
self._prefix = prefix.upper()
|
|
66
|
-
self._case_sensitive = case_sensitive
|
|
67
|
-
|
|
68
|
-
def get(self, key: str) -> Optional[str]:
|
|
69
|
-
if not key:
|
|
70
|
-
return None
|
|
71
|
-
k = f"{self._prefix}{key}" if self._prefix else key
|
|
72
|
-
if not self._case_sensitive:
|
|
73
|
-
k = k.upper()
|
|
74
|
-
v = self._data.get(k)
|
|
75
|
-
if v is None:
|
|
76
|
-
return None
|
|
77
|
-
if isinstance(v, (str, int, float, bool)):
|
|
78
|
-
return str(v)
|
|
79
|
-
return None
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def _meta_get(obj: Any) -> Dict[str, Any]:
|
|
83
|
-
m = getattr(obj, PICO_META, None)
|
|
84
|
-
if m is None:
|
|
85
|
-
m = {}
|
|
86
|
-
setattr(obj, PICO_META, m)
|
|
87
|
-
return m
|
|
88
|
-
|
|
89
|
-
def component(
|
|
90
|
-
cls=None,
|
|
91
|
-
*,
|
|
92
|
-
name: Any = None,
|
|
93
|
-
qualifiers: Iterable[str] = (),
|
|
94
|
-
scope: str = "singleton",
|
|
95
|
-
primary: bool = False,
|
|
96
|
-
lazy: bool = False,
|
|
97
|
-
conditional_profiles: Iterable[str] = (),
|
|
98
|
-
conditional_require_env: Iterable[str] = (),
|
|
99
|
-
conditional_predicate: Optional[Callable[[], bool]] = None,
|
|
100
|
-
on_missing_selector: Optional[object] = None,
|
|
101
|
-
on_missing_priority: int = 0,
|
|
102
|
-
):
|
|
103
|
-
def dec(c):
|
|
104
|
-
setattr(c, PICO_INFRA, "component")
|
|
105
|
-
setattr(c, PICO_NAME, name if name is not None else getattr(c, "__name__", str(c)))
|
|
106
|
-
setattr(c, PICO_KEY, name if name is not None else c)
|
|
107
|
-
m = _meta_get(c)
|
|
108
|
-
m["qualifier"] = tuple(str(q) for q in qualifiers or ())
|
|
109
|
-
m["scope"] = scope
|
|
110
|
-
if primary:
|
|
111
|
-
m["primary"] = True
|
|
112
|
-
if lazy:
|
|
113
|
-
m["lazy"] = True
|
|
114
|
-
if conditional_profiles or conditional_require_env or conditional_predicate is not None:
|
|
115
|
-
m["conditional"] = {
|
|
116
|
-
"profiles": tuple(p for p in conditional_profiles or ()),
|
|
117
|
-
"require_env": tuple(e for e in conditional_require_env or ()),
|
|
118
|
-
"predicate": conditional_predicate,
|
|
119
|
-
}
|
|
120
|
-
if on_missing_selector is not None:
|
|
121
|
-
m["on_missing"] = {"selector": on_missing_selector, "priority": int(on_missing_priority)}
|
|
122
|
-
return c
|
|
123
|
-
return dec(cls) if cls else dec
|
|
124
|
-
|
|
125
|
-
def factory(
|
|
126
|
-
cls=None,
|
|
127
|
-
*,
|
|
128
|
-
name: Any = None,
|
|
129
|
-
qualifiers: Iterable[str] = (),
|
|
130
|
-
scope: str = "singleton",
|
|
131
|
-
primary: bool = False,
|
|
132
|
-
lazy: bool = False,
|
|
133
|
-
conditional_profiles: Iterable[str] = (),
|
|
134
|
-
conditional_require_env: Iterable[str] = (),
|
|
135
|
-
conditional_predicate: Optional[Callable[[], bool]] = None,
|
|
136
|
-
on_missing_selector: Optional[object] = None,
|
|
137
|
-
on_missing_priority: int = 0,
|
|
138
|
-
):
|
|
139
|
-
def dec(c):
|
|
140
|
-
setattr(c, PICO_INFRA, "factory")
|
|
141
|
-
setattr(c, PICO_NAME, name if name is not None else getattr(c, "__name__", str(c)))
|
|
142
|
-
m = _meta_get(c)
|
|
143
|
-
m["qualifier"] = tuple(str(q) for q in qualifiers or ())
|
|
144
|
-
m["scope"] = scope
|
|
145
|
-
if primary:
|
|
146
|
-
m["primary"] = True
|
|
147
|
-
if lazy:
|
|
148
|
-
m["lazy"] = True
|
|
149
|
-
if conditional_profiles or conditional_require_env or conditional_predicate is not None:
|
|
150
|
-
m["conditional"] = {
|
|
151
|
-
"profiles": tuple(p for p in conditional_profiles or ()),
|
|
152
|
-
"require_env": tuple(e for e in conditional_require_env or ()),
|
|
153
|
-
"predicate": conditional_predicate,
|
|
154
|
-
}
|
|
155
|
-
if on_missing_selector is not None:
|
|
156
|
-
m["on_missing"] = {"selector": on_missing_selector, "priority": int(on_missing_priority)}
|
|
157
|
-
return c
|
|
158
|
-
return dec(cls) if cls else dec
|
|
159
|
-
|
|
160
|
-
def provides(*dargs, **dkwargs):
|
|
161
|
-
def _apply(fn, key_hint, *, name=None, qualifiers=(), scope="singleton", primary=False, lazy=False, conditional_profiles=(), conditional_require_env=(), conditional_predicate=None, on_missing_selector=None, on_missing_priority=0):
|
|
162
|
-
target = fn.__func__ if isinstance(fn, (staticmethod, classmethod)) else fn
|
|
163
|
-
inferred_key = key_hint
|
|
164
|
-
if inferred_key is MISSING:
|
|
165
|
-
rt = _get_return_type(target)
|
|
166
|
-
if isinstance(rt, type):
|
|
167
|
-
inferred_key = rt
|
|
168
|
-
else:
|
|
169
|
-
inferred_key = getattr(target, "__name__", str(target))
|
|
170
|
-
setattr(target, PICO_INFRA, "provides")
|
|
171
|
-
pico_name = name if name is not None else (inferred_key if isinstance(inferred_key, str) else getattr(target, "__name__", str(target)))
|
|
172
|
-
setattr(target, PICO_NAME, pico_name)
|
|
173
|
-
setattr(target, PICO_KEY, inferred_key)
|
|
174
|
-
m = _meta_get(target)
|
|
175
|
-
m["qualifier"] = tuple(str(q) for q in qualifiers or ())
|
|
176
|
-
m["scope"] = scope
|
|
177
|
-
if primary:
|
|
178
|
-
m["primary"] = True
|
|
179
|
-
if lazy:
|
|
180
|
-
m["lazy"] = True
|
|
181
|
-
if conditional_profiles or conditional_require_env or conditional_predicate is not None:
|
|
182
|
-
m["conditional"] = {
|
|
183
|
-
"profiles": tuple(p for p in conditional_profiles or ()),
|
|
184
|
-
"require_env": tuple(e for e in conditional_require_env or ()),
|
|
185
|
-
"predicate": conditional_predicate,
|
|
186
|
-
}
|
|
187
|
-
if on_missing_selector is not None:
|
|
188
|
-
m["on_missing"] = {"selector": on_missing_selector, "priority": int(on_missing_priority)}
|
|
189
|
-
return fn
|
|
190
|
-
|
|
191
|
-
if dargs and len(dargs) == 1 and inspect.isfunction(dargs[0]) and not dkwargs:
|
|
192
|
-
fn = dargs[0]
|
|
193
|
-
return _apply(fn, MISSING)
|
|
194
|
-
else:
|
|
195
|
-
key = dargs[0] if dargs else MISSING
|
|
196
|
-
def _decorator(fn):
|
|
197
|
-
return _apply(fn, key, **dkwargs)
|
|
198
|
-
return _decorator
|
|
199
|
-
|
|
200
|
-
class Qualifier(str):
|
|
201
|
-
__slots__ = ()
|
|
202
|
-
|
|
203
|
-
def configuration(cls=None, *, prefix: Optional[str] = None):
|
|
204
|
-
def dec(c):
|
|
205
|
-
setattr(c, PICO_INFRA, "configuration")
|
|
206
|
-
m = _meta_get(c)
|
|
207
|
-
if prefix is not None:
|
|
208
|
-
m["config_prefix"] = prefix
|
|
209
|
-
return c
|
|
210
|
-
return dec(cls) if cls else dec
|
|
211
|
-
|
|
212
|
-
def configure(fn):
|
|
213
|
-
m = _meta_get(fn)
|
|
214
|
-
m["configure"] = True
|
|
215
|
-
return fn
|
|
216
|
-
|
|
217
|
-
def cleanup(fn):
|
|
218
|
-
m = _meta_get(fn)
|
|
219
|
-
m["cleanup"] = True
|
|
220
|
-
return fn
|
|
221
|
-
|
|
222
|
-
def configured(target: Any, *, prefix: Optional[str] = None):
|
|
223
|
-
def dec(cls):
|
|
224
|
-
setattr(cls, PICO_INFRA, "configured")
|
|
225
|
-
m = _meta_get(cls)
|
|
226
|
-
m["configured"] = {"target": target, "prefix": prefix}
|
|
227
|
-
return cls
|
|
228
|
-
return dec
|
|
229
|
-
|
|
230
|
-
def _truthy(s: str) -> bool:
|
|
231
|
-
return s.strip().lower() in {"1", "true", "yes", "on", "y", "t"}
|
|
232
|
-
|
|
233
|
-
def _coerce(val: Optional[str], t: type) -> Any:
|
|
234
|
-
if val is None:
|
|
235
|
-
return None
|
|
236
|
-
if t is str:
|
|
237
|
-
return val
|
|
238
|
-
if t is int:
|
|
239
|
-
return int(val)
|
|
240
|
-
if t is float:
|
|
241
|
-
return float(val)
|
|
242
|
-
if t is bool:
|
|
243
|
-
return _truthy(val)
|
|
244
|
-
org = get_origin(t)
|
|
245
|
-
if org is Union:
|
|
246
|
-
args = [a for a in get_args(t) if a is not type(None)]
|
|
247
|
-
if not args:
|
|
248
|
-
return None
|
|
249
|
-
return _coerce(val, args[0])
|
|
250
|
-
return val
|
|
251
|
-
|
|
252
|
-
def _upper_key(name: str) -> str:
|
|
253
|
-
return name.upper()
|
|
254
|
-
|
|
255
|
-
def _lookup(sources: Tuple[ConfigSource, ...], key: str) -> Optional[str]:
|
|
256
|
-
for src in sources:
|
|
257
|
-
v = src.get(key)
|
|
258
|
-
if v is not None:
|
|
259
|
-
return v
|
|
260
|
-
return None
|
|
261
|
-
|
|
262
|
-
def _build_settings_instance(cls: type, sources: Tuple[ConfigSource, ...], prefix: Optional[str]) -> Any:
|
|
263
|
-
if not is_dataclass(cls):
|
|
264
|
-
raise ConfigurationError(f"Configuration class {getattr(cls, '__name__', str(cls))} must be a dataclass")
|
|
265
|
-
values: Dict[str, Any] = {}
|
|
266
|
-
for f in fields(cls):
|
|
267
|
-
base_key = _upper_key(f.name)
|
|
268
|
-
keys_to_try = []
|
|
269
|
-
if prefix:
|
|
270
|
-
keys_to_try.append(prefix + base_key)
|
|
271
|
-
keys_to_try.append(base_key)
|
|
272
|
-
raw = None
|
|
273
|
-
for k in keys_to_try:
|
|
274
|
-
raw = _lookup(sources, k)
|
|
275
|
-
if raw is not None:
|
|
276
|
-
break
|
|
277
|
-
if raw is None:
|
|
278
|
-
if f.default is not MISSING or f.default_factory is not MISSING:
|
|
279
|
-
continue
|
|
280
|
-
raise ConfigurationError(f"Missing configuration key: {(prefix or '') + base_key}")
|
|
281
|
-
values[f.name] = _coerce(raw, f.type if isinstance(f.type, type) or get_origin(f.type) else str)
|
|
282
|
-
return cls(**values)
|
|
283
|
-
|
|
284
|
-
def _extract_list_req(ann: Any):
|
|
285
|
-
def read_qualifier(metas: Iterable[Any]):
|
|
286
|
-
for m in metas:
|
|
287
|
-
if isinstance(m, Qualifier):
|
|
288
|
-
return str(m)
|
|
289
|
-
return None
|
|
290
|
-
origin = get_origin(ann)
|
|
291
|
-
if origin is Annotated:
|
|
292
|
-
args = get_args(ann)
|
|
293
|
-
base = args[0] if args else Any
|
|
294
|
-
metas = args[1:] if len(args) > 1 else ()
|
|
295
|
-
is_list, elem_t, qual = _extract_list_req(base)
|
|
296
|
-
if qual is None:
|
|
297
|
-
qual = read_qualifier(metas)
|
|
298
|
-
return is_list, elem_t, qual
|
|
299
|
-
if origin in (list, List):
|
|
300
|
-
elem = get_args(ann)[0] if get_args(ann) else Any
|
|
301
|
-
if get_origin(elem) is Annotated:
|
|
302
|
-
eargs = get_args(elem)
|
|
303
|
-
ebase = eargs[0] if eargs else Any
|
|
304
|
-
emetas = eargs[1:] if len(eargs) > 1 else ()
|
|
305
|
-
qual = read_qualifier(emetas)
|
|
306
|
-
return True, ebase if isinstance(ebase, type) else Any, qual
|
|
307
|
-
return True, elem if isinstance(elem, type) else Any, None
|
|
308
|
-
return False, None, None
|
|
309
|
-
|
|
310
|
-
def _implements_protocol(typ: type, proto: type) -> bool:
|
|
311
|
-
if not getattr(proto, "_is_protocol", False):
|
|
312
|
-
return False
|
|
313
|
-
try:
|
|
314
|
-
if getattr(proto, "__runtime_protocol__", False) or getattr(proto, "__annotations__", None) is not None:
|
|
315
|
-
inst = object.__new__(typ)
|
|
316
|
-
return isinstance(inst, proto)
|
|
317
|
-
except Exception:
|
|
318
|
-
pass
|
|
319
|
-
for name, val in proto.__dict__.items():
|
|
320
|
-
if name.startswith("_") or not callable(val):
|
|
321
|
-
continue
|
|
322
|
-
return True
|
|
323
|
-
|
|
324
|
-
def _collect_by_type(locator: ComponentLocator, t: type, q: Optional[str]):
|
|
325
|
-
keys = list(locator._metadata.keys())
|
|
326
|
-
out: List[KeyT] = []
|
|
327
|
-
for k in keys:
|
|
328
|
-
md = locator._metadata.get(k)
|
|
329
|
-
if md is None:
|
|
330
|
-
continue
|
|
331
|
-
typ = md.provided_type or md.concrete_class
|
|
332
|
-
if not isinstance(typ, type):
|
|
333
|
-
continue
|
|
334
|
-
ok = False
|
|
335
|
-
try:
|
|
336
|
-
ok = issubclass(typ, t)
|
|
337
|
-
except Exception:
|
|
338
|
-
ok = _implements_protocol(typ, t)
|
|
339
|
-
if ok and (q is None or q in md.qualifiers):
|
|
340
|
-
out.append(k)
|
|
341
|
-
return out
|
|
342
|
-
|
|
343
|
-
def _find_key_by_name(locator: Optional[ComponentLocator], name: str) -> Optional[KeyT]:
|
|
344
|
-
if not locator:
|
|
345
|
-
return None
|
|
346
|
-
for k, md in locator._metadata.items():
|
|
347
|
-
if md.pico_name == name:
|
|
348
|
-
return k
|
|
349
|
-
typ = md.provided_type or md.concrete_class
|
|
350
|
-
if isinstance(typ, type) and getattr(typ, "__name__", "") == name:
|
|
351
|
-
return k
|
|
352
|
-
return None
|
|
353
|
-
|
|
354
|
-
def _normalize_callable(obj):
|
|
355
|
-
return getattr(obj, '__func__', obj)
|
|
356
|
-
|
|
357
|
-
def _get_signature_safe(callable_obj):
|
|
358
|
-
try:
|
|
359
|
-
return inspect.signature(callable_obj)
|
|
360
|
-
except (ValueError, TypeError):
|
|
361
|
-
wrapped = getattr(callable_obj, '__wrapped__', None)
|
|
362
|
-
if wrapped is not None:
|
|
363
|
-
return inspect.signature(wrapped)
|
|
364
|
-
raise
|
|
365
|
-
|
|
366
|
-
@functools.lru_cache(maxsize=1024)
|
|
367
|
-
def _analyze_signature_template(callable_obj):
|
|
368
|
-
callable_obj = _normalize_callable(callable_obj)
|
|
369
|
-
sig = _get_signature_safe(callable_obj)
|
|
370
|
-
plan = []
|
|
371
|
-
for name, param in sig.parameters.items():
|
|
372
|
-
if name in ("self", "cls"): continue
|
|
373
|
-
if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD): continue
|
|
374
|
-
ann = param.annotation
|
|
375
|
-
is_list, elem_t, qual = _extract_list_req(ann)
|
|
376
|
-
if is_list:
|
|
377
|
-
plan.append(("list", name, (elem_t, qual)))
|
|
378
|
-
else:
|
|
379
|
-
plan.append(("key", name, ann))
|
|
380
|
-
return tuple(plan)
|
|
381
|
-
|
|
382
|
-
def _compile_argplan(callable_obj, pico):
|
|
383
|
-
template = _analyze_signature_template(callable_obj)
|
|
384
|
-
locator = pico._locator
|
|
385
|
-
items = []
|
|
386
|
-
for kind, name, data in template:
|
|
387
|
-
if kind == "list":
|
|
388
|
-
elem_t, qual = data
|
|
389
|
-
if locator is not None and isinstance(elem_t, type):
|
|
390
|
-
keys = _collect_by_type(locator, elem_t, qual)
|
|
391
|
-
items.append(("list", name, tuple(keys)))
|
|
392
|
-
continue
|
|
393
|
-
ann = data
|
|
394
|
-
if ann is not inspect._empty and isinstance(ann, type):
|
|
395
|
-
key = ann
|
|
396
|
-
elif ann is not inspect._empty and isinstance(ann, str):
|
|
397
|
-
mapped = _find_key_by_name(locator, ann)
|
|
398
|
-
key = mapped if mapped is not None else ann
|
|
399
|
-
else:
|
|
400
|
-
key = name
|
|
401
|
-
items.append(("key", name, key))
|
|
402
|
-
return tuple(items)
|
|
403
|
-
|
|
404
|
-
def _resolve_args(callable_obj: Callable[..., Any], pico: "PicoContainer") -> Dict[str, Any]:
|
|
405
|
-
plan = _compile_argplan(callable_obj, pico)
|
|
406
|
-
kwargs: Dict[str, Any] = {}
|
|
407
|
-
tracer = pico._tracer
|
|
408
|
-
prev = tracer.override_via("constructor")
|
|
409
|
-
try:
|
|
410
|
-
for kind, name, data in plan:
|
|
411
|
-
if kind == "key":
|
|
412
|
-
primary_key = data
|
|
413
|
-
tracer.note_param(primary_key, name)
|
|
414
|
-
try:
|
|
415
|
-
kwargs[name] = pico.get(primary_key)
|
|
416
|
-
except ProviderNotFoundError as first_error:
|
|
417
|
-
if primary_key != name:
|
|
418
|
-
try:
|
|
419
|
-
kwargs[name] = pico.get(name)
|
|
420
|
-
except ProviderNotFoundError:
|
|
421
|
-
raise first_error from None
|
|
422
|
-
else:
|
|
423
|
-
raise first_error from None
|
|
424
|
-
else:
|
|
425
|
-
vals = [pico.get(k) for k in data]
|
|
426
|
-
kwargs[name] = vals
|
|
427
|
-
finally:
|
|
428
|
-
tracer.restore_via(prev)
|
|
429
|
-
return kwargs
|
|
430
|
-
|
|
431
|
-
def _needs_async_configure(obj: Any) -> bool:
|
|
432
|
-
for _, m in inspect.getmembers(obj, predicate=inspect.ismethod):
|
|
433
|
-
meta = getattr(m, PICO_META, {})
|
|
434
|
-
if meta.get("configure", False) and inspect.iscoroutinefunction(m):
|
|
435
|
-
return True
|
|
436
|
-
return False
|
|
437
|
-
|
|
438
|
-
def _iter_configure_methods(obj: Any):
|
|
439
|
-
for _, m in inspect.getmembers(obj, predicate=inspect.ismethod):
|
|
440
|
-
meta = getattr(m, PICO_META, {})
|
|
441
|
-
if meta.get("configure", False):
|
|
442
|
-
yield m
|
|
443
|
-
|
|
444
|
-
def _build_class(cls: type, pico: "PicoContainer", locator: ComponentLocator) -> Any:
|
|
445
|
-
init = cls.__init__
|
|
446
|
-
if init is object.__init__:
|
|
447
|
-
inst = cls()
|
|
448
|
-
else:
|
|
449
|
-
deps = _resolve_args(init, pico)
|
|
450
|
-
inst = cls(**deps)
|
|
451
|
-
ainit = getattr(inst, "__ainit__", None)
|
|
452
|
-
has_async = (callable(ainit) and inspect.iscoroutinefunction(ainit)) or _needs_async_configure(inst)
|
|
453
|
-
if has_async:
|
|
454
|
-
async def runner():
|
|
455
|
-
if callable(ainit):
|
|
456
|
-
kwargs = {}
|
|
457
|
-
try:
|
|
458
|
-
kwargs = _resolve_args(ainit, pico)
|
|
459
|
-
except Exception:
|
|
460
|
-
kwargs = {}
|
|
461
|
-
res = ainit(**kwargs)
|
|
462
|
-
if inspect.isawaitable(res):
|
|
463
|
-
await res
|
|
464
|
-
for m in _iter_configure_methods(inst):
|
|
465
|
-
args = _resolve_args(m, pico)
|
|
466
|
-
r = m(**args)
|
|
467
|
-
if inspect.isawaitable(r):
|
|
468
|
-
await r
|
|
469
|
-
return inst
|
|
470
|
-
return runner()
|
|
471
|
-
for m in _iter_configure_methods(inst):
|
|
472
|
-
args = _resolve_args(m, pico)
|
|
473
|
-
m(**args)
|
|
474
|
-
return inst
|
|
475
|
-
|
|
476
|
-
def _build_method(fn: Callable[..., Any], pico: "PicoContainer", locator: ComponentLocator) -> Any:
|
|
477
|
-
deps = _resolve_args(fn, pico)
|
|
478
|
-
obj = fn(**deps)
|
|
479
|
-
has_async = _needs_async_configure(obj)
|
|
480
|
-
if has_async:
|
|
481
|
-
async def runner():
|
|
482
|
-
for m in _iter_configure_methods(obj):
|
|
483
|
-
args = _resolve_args(m, pico)
|
|
484
|
-
r = m(**args)
|
|
485
|
-
if inspect.isawaitable(r):
|
|
486
|
-
await r
|
|
487
|
-
return obj
|
|
488
|
-
return runner()
|
|
489
|
-
for m in _iter_configure_methods(obj):
|
|
490
|
-
args = _resolve_args(m, pico)
|
|
491
|
-
m(**args)
|
|
492
|
-
return obj
|
|
493
|
-
|
|
494
|
-
def _get_return_type(fn: Callable[..., Any]) -> Optional[type]:
|
|
495
|
-
try:
|
|
496
|
-
ra = inspect.signature(fn).return_annotation
|
|
497
|
-
except Exception:
|
|
498
|
-
return None
|
|
499
|
-
if ra is inspect._empty:
|
|
500
|
-
return None
|
|
501
|
-
return ra if isinstance(ra, type) else None
|
|
502
|
-
|
|
503
20
|
def _scan_package(package) -> Iterable[Any]:
|
|
504
21
|
for _, name, _ in pkgutil.walk_packages(package.__path__, package.__name__ + "."):
|
|
505
22
|
yield importlib.import_module(name)
|
|
@@ -524,20 +41,7 @@ def _iter_input_modules(inputs: Union[Any, Iterable[Any]]) -> Iterable[Any]:
|
|
|
524
41
|
seen.add(name)
|
|
525
42
|
yield mod
|
|
526
43
|
|
|
527
|
-
def
|
|
528
|
-
if not isinstance(selector, type):
|
|
529
|
-
return False
|
|
530
|
-
for md in reg_md.values():
|
|
531
|
-
typ = md.provided_type or md.concrete_class
|
|
532
|
-
if isinstance(typ, type):
|
|
533
|
-
try:
|
|
534
|
-
if issubclass(typ, selector):
|
|
535
|
-
return True
|
|
536
|
-
except Exception:
|
|
537
|
-
continue
|
|
538
|
-
return False
|
|
539
|
-
|
|
540
|
-
def _normalize_override_provider(v: Any) -> Tuple[Provider, bool]:
|
|
44
|
+
def _normalize_override_provider(v: Any):
|
|
541
45
|
if isinstance(v, tuple) and len(v) == 2:
|
|
542
46
|
src, lz = v
|
|
543
47
|
if callable(src):
|
|
@@ -547,612 +51,84 @@ def _normalize_override_provider(v: Any) -> Tuple[Provider, bool]:
|
|
|
547
51
|
return (lambda f=v: f()), False
|
|
548
52
|
return (lambda inst=v: inst), False
|
|
549
53
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
self._deferred: List[DeferredProvider] = []
|
|
565
|
-
self._candidates: Dict[KeyT, List[Tuple[bool, Provider, ProviderMetadata]]] = {}
|
|
566
|
-
self._metadata: Dict[KeyT, ProviderMetadata] = {}
|
|
567
|
-
self._indexes: Dict[str, Dict[Any, List[KeyT]]] = {}
|
|
568
|
-
self._on_missing: List[Tuple[int, KeyT, type]] = []
|
|
569
|
-
self._log = logger or LOGGER
|
|
570
|
-
self._config_sources: Tuple[ConfigSource, ...] = tuple(config)
|
|
571
|
-
from .config_runtime import ConfigResolver, TypeAdapterRegistry, ObjectGraphBuilder
|
|
572
|
-
self._resolver = ConfigResolver(tuple(tree_sources))
|
|
573
|
-
self._adapters = TypeAdapterRegistry()
|
|
574
|
-
self._graph = ObjectGraphBuilder(self._resolver, self._adapters)
|
|
575
|
-
self._provides_functions: Dict[KeyT, Callable[..., Any]] = {}
|
|
576
|
-
|
|
577
|
-
def locator(self) -> ComponentLocator:
|
|
578
|
-
loc = ComponentLocator(self._metadata, self._indexes)
|
|
579
|
-
setattr(loc, "_provides_functions", dict(self._provides_functions))
|
|
580
|
-
return loc
|
|
581
|
-
|
|
582
|
-
def attach_runtime(self, pico, locator: ComponentLocator) -> None:
|
|
583
|
-
for deferred in self._deferred:
|
|
584
|
-
deferred.attach(pico, locator)
|
|
585
|
-
for key, md in list(self._metadata.items()):
|
|
586
|
-
if md.lazy:
|
|
587
|
-
original = self._factory.get(key, origin='lazy')
|
|
588
|
-
def lazy_proxy_provider(_orig=original, _p=pico):
|
|
589
|
-
return UnifiedComponentProxy(container=_p, object_creator=_orig)
|
|
590
|
-
self._factory.bind(key, lazy_proxy_provider)
|
|
591
|
-
|
|
592
|
-
def _queue(self, key: KeyT, provider: Provider, md: ProviderMetadata) -> None:
|
|
593
|
-
lst = self._candidates.setdefault(key, [])
|
|
594
|
-
lst.append((md.primary, provider, md))
|
|
595
|
-
if isinstance(provider, DeferredProvider):
|
|
596
|
-
self._deferred.append(provider)
|
|
597
|
-
|
|
598
|
-
def _bind_if_absent(self, key: KeyT, provider: Provider) -> None:
|
|
599
|
-
if not self._factory.has(key):
|
|
600
|
-
self._factory.bind(key, provider)
|
|
601
|
-
|
|
602
|
-
def _enabled_by_condition(self, obj: Any) -> bool:
|
|
603
|
-
meta = getattr(obj, PICO_META, {})
|
|
604
|
-
c = meta.get("conditional", None)
|
|
605
|
-
if not c:
|
|
606
|
-
return True
|
|
607
|
-
p = set(c.get("profiles") or ())
|
|
608
|
-
if p and not (p & self._profiles):
|
|
609
|
-
return False
|
|
610
|
-
req = c.get("require_env") or ()
|
|
611
|
-
for k in req:
|
|
612
|
-
if k not in self._environ or not self._environ.get(k):
|
|
613
|
-
return False
|
|
614
|
-
pred = c.get("predicate")
|
|
615
|
-
if pred is None:
|
|
616
|
-
return True
|
|
617
|
-
try:
|
|
618
|
-
ok = bool(pred())
|
|
619
|
-
except Exception:
|
|
620
|
-
return False
|
|
621
|
-
if not ok:
|
|
622
|
-
return False
|
|
623
|
-
return True
|
|
624
|
-
|
|
625
|
-
def _register_component_class(self, cls: type) -> None:
|
|
626
|
-
if not self._enabled_by_condition(cls):
|
|
627
|
-
return
|
|
628
|
-
key = getattr(cls, PICO_KEY, cls)
|
|
629
|
-
provider = DeferredProvider(lambda pico, loc, c=cls: _build_class(c, pico, loc))
|
|
630
|
-
qset = set(str(q) for q in getattr(cls, PICO_META, {}).get("qualifier", ()))
|
|
631
|
-
sc = getattr(cls, PICO_META, {}).get("scope", "singleton")
|
|
632
|
-
md = ProviderMetadata(key=key, provided_type=cls, concrete_class=cls, factory_class=None, factory_method=None, qualifiers=qset, primary=bool(getattr(cls, PICO_META, {}).get("primary")), lazy=bool(getattr(cls, PICO_META, {}).get("lazy", False)), infra=getattr(cls, PICO_INFRA, None), pico_name=getattr(cls, PICO_NAME, None), scope=sc)
|
|
633
|
-
self._queue(key, provider, md)
|
|
634
|
-
|
|
635
|
-
def _register_factory_class(self, cls: type) -> None:
|
|
636
|
-
if not self._enabled_by_condition(cls):
|
|
637
|
-
return
|
|
638
|
-
for name in dir(cls):
|
|
639
|
-
try:
|
|
640
|
-
raw = inspect.getattr_static(cls, name)
|
|
641
|
-
except Exception:
|
|
642
|
-
continue
|
|
643
|
-
fn = None
|
|
644
|
-
kind = None
|
|
645
|
-
if isinstance(raw, staticmethod):
|
|
646
|
-
fn = raw.__func__
|
|
647
|
-
kind = "static"
|
|
648
|
-
elif isinstance(raw, classmethod):
|
|
649
|
-
fn = raw.__func__
|
|
650
|
-
kind = "class"
|
|
651
|
-
elif inspect.isfunction(raw):
|
|
652
|
-
fn = raw
|
|
653
|
-
kind = "instance"
|
|
654
|
-
else:
|
|
655
|
-
continue
|
|
656
|
-
if getattr(fn, PICO_INFRA, None) != "provides":
|
|
657
|
-
continue
|
|
658
|
-
if not self._enabled_by_condition(fn):
|
|
659
|
-
continue
|
|
660
|
-
k = getattr(fn, PICO_KEY)
|
|
661
|
-
if kind == "instance":
|
|
662
|
-
provider = DeferredProvider(lambda pico, loc, fc=cls, mn=name: _build_method(getattr(_build_class(fc, pico, loc), mn), pico, loc))
|
|
663
|
-
else:
|
|
664
|
-
provider = DeferredProvider(lambda pico, loc, f=fn: _build_method(f, pico, loc))
|
|
665
|
-
rt = _get_return_type(fn)
|
|
666
|
-
qset = set(str(q) for q in getattr(fn, PICO_META, {}).get("qualifier", ()))
|
|
667
|
-
sc = getattr(fn, PICO_META, {}).get("scope", getattr(cls, PICO_META, {}).get("scope", "singleton"))
|
|
668
|
-
md = ProviderMetadata(
|
|
669
|
-
key=k,
|
|
670
|
-
provided_type=rt if isinstance(rt, type) else (k if isinstance(k, type) else None),
|
|
671
|
-
concrete_class=None,
|
|
672
|
-
factory_class=cls,
|
|
673
|
-
factory_method=name,
|
|
674
|
-
qualifiers=qset,
|
|
675
|
-
primary=bool(getattr(fn, PICO_META, {}).get("primary")),
|
|
676
|
-
lazy=bool(getattr(fn, PICO_META, {}).get("lazy", False)),
|
|
677
|
-
infra=getattr(cls, PICO_INFRA, None),
|
|
678
|
-
pico_name=getattr(fn, PICO_NAME, None),
|
|
679
|
-
scope=sc
|
|
680
|
-
)
|
|
681
|
-
self._queue(k, provider, md)
|
|
682
|
-
|
|
683
|
-
def _register_configuration_class(self, cls: type) -> None:
|
|
684
|
-
if not self._enabled_by_condition(cls):
|
|
685
|
-
return
|
|
686
|
-
pref = getattr(cls, PICO_META, {}).get("config_prefix", None)
|
|
687
|
-
if is_dataclass(cls):
|
|
688
|
-
key = cls
|
|
689
|
-
provider = DeferredProvider(lambda pico, loc, c=cls, p=pref, src=self._config_sources: _build_settings_instance(c, src, p))
|
|
690
|
-
md = ProviderMetadata(key=key, provided_type=cls, concrete_class=cls, factory_class=None, factory_method=None, qualifiers=set(), primary=True, lazy=False, infra="configuration", pico_name=getattr(cls, PICO_NAME, None), scope="singleton")
|
|
691
|
-
self._queue(key, provider, md)
|
|
692
|
-
|
|
693
|
-
def _register_configured_class(self, cls: type) -> None:
|
|
694
|
-
if not self._enabled_by_condition(cls):
|
|
695
|
-
return
|
|
696
|
-
meta = getattr(cls, PICO_META, {})
|
|
697
|
-
cfg = meta.get("configured", None)
|
|
698
|
-
if not cfg:
|
|
699
|
-
return
|
|
700
|
-
target = cfg.get("target")
|
|
701
|
-
prefix = cfg.get("prefix")
|
|
702
|
-
if not isinstance(target, type):
|
|
703
|
-
return
|
|
704
|
-
provider = DeferredProvider(lambda pico, loc, t=target, p=prefix, g=self._graph: g.build_from_prefix(t, p))
|
|
705
|
-
qset = set(str(q) for q in meta.get("qualifier", ()))
|
|
706
|
-
sc = meta.get("scope", "singleton")
|
|
707
|
-
md = ProviderMetadata(key=target, provided_type=target, concrete_class=None, factory_class=None, factory_method=None, qualifiers=qset, primary=True, lazy=False, infra="configured", pico_name=prefix, scope=sc)
|
|
708
|
-
self._queue(target, provider, md)
|
|
709
|
-
|
|
710
|
-
def _register_provides_function(self, fn: Callable[..., Any]) -> None:
|
|
711
|
-
if not self._enabled_by_condition(fn):
|
|
712
|
-
return
|
|
713
|
-
k = getattr(fn, PICO_KEY)
|
|
714
|
-
provider = DeferredProvider(lambda pico, loc, f=fn: _build_method(f, pico, loc))
|
|
715
|
-
rt = _get_return_type(fn)
|
|
716
|
-
qset = set(str(q) for q in getattr(fn, PICO_META, {}).get("qualifier", ()))
|
|
717
|
-
sc = getattr(fn, PICO_META, {}).get("scope", "singleton")
|
|
718
|
-
md = ProviderMetadata(
|
|
719
|
-
key=k,
|
|
720
|
-
provided_type=rt if isinstance(rt, type) else (k if isinstance(k, type) else None),
|
|
721
|
-
concrete_class=None,
|
|
722
|
-
factory_class=None,
|
|
723
|
-
factory_method=getattr(fn, "__name__", None),
|
|
724
|
-
qualifiers=qset,
|
|
725
|
-
primary=bool(getattr(fn, PICO_META, {}).get("primary")),
|
|
726
|
-
lazy=bool(getattr(fn, PICO_META, {}).get("lazy", False)),
|
|
727
|
-
infra="provides",
|
|
728
|
-
pico_name=getattr(fn, PICO_NAME, None),
|
|
729
|
-
scope=sc
|
|
730
|
-
)
|
|
731
|
-
self._queue(k, provider, md)
|
|
732
|
-
self._provides_functions[k] = fn
|
|
733
|
-
|
|
734
|
-
def register_module(self, module: Any) -> None:
|
|
735
|
-
for _, obj in inspect.getmembers(module):
|
|
736
|
-
if inspect.isclass(obj):
|
|
737
|
-
meta = getattr(obj, PICO_META, {})
|
|
738
|
-
if "on_missing" in meta:
|
|
739
|
-
sel = meta["on_missing"]["selector"]
|
|
740
|
-
pr = int(meta["on_missing"].get("priority", 0))
|
|
741
|
-
self._on_missing.append((pr, sel, obj))
|
|
742
|
-
continue
|
|
743
|
-
infra = getattr(obj, PICO_INFRA, None)
|
|
744
|
-
if infra == "component":
|
|
745
|
-
self._register_component_class(obj)
|
|
746
|
-
elif infra == "factory":
|
|
747
|
-
self._register_factory_class(obj)
|
|
748
|
-
elif infra == "configuration":
|
|
749
|
-
self._register_configuration_class(obj)
|
|
750
|
-
elif infra == "configured":
|
|
751
|
-
self._register_configured_class(obj)
|
|
752
|
-
for _, fn in inspect.getmembers(module, predicate=inspect.isfunction):
|
|
753
|
-
if getattr(fn, PICO_INFRA, None) == "provides":
|
|
754
|
-
self._register_provides_function(fn)
|
|
755
|
-
|
|
756
|
-
def _prefix_exists(self, md: ProviderMetadata) -> bool:
|
|
757
|
-
if md.infra != "configured":
|
|
758
|
-
return False
|
|
759
|
-
try:
|
|
760
|
-
_ = self._resolver.subtree(md.pico_name)
|
|
761
|
-
return True
|
|
762
|
-
except Exception:
|
|
763
|
-
return False
|
|
764
|
-
|
|
765
|
-
def select_and_bind(self) -> None:
|
|
766
|
-
for key, lst in self._candidates.items():
|
|
767
|
-
def rank(item: Tuple[bool, Provider, ProviderMetadata]) -> Tuple[int, int, int]:
|
|
768
|
-
is_present = 1 if self._prefix_exists(item[2]) else 0
|
|
769
|
-
pref = str(item[2].pico_name or "")
|
|
770
|
-
pref_len = len(pref)
|
|
771
|
-
is_primary = 1 if item[0] else 0
|
|
772
|
-
return (is_present, pref_len, is_primary)
|
|
773
|
-
lst_sorted = sorted(lst, key=rank, reverse=True)
|
|
774
|
-
chosen = lst_sorted[0]
|
|
775
|
-
self._bind_if_absent(key, chosen[1])
|
|
776
|
-
self._metadata[key] = chosen[2]
|
|
777
|
-
|
|
778
|
-
def _find_md_for_type(self, t: type) -> Optional[ProviderMetadata]:
|
|
779
|
-
cands: List[ProviderMetadata] = []
|
|
780
|
-
for md in self._metadata.values():
|
|
781
|
-
typ = md.provided_type or md.concrete_class
|
|
782
|
-
if not isinstance(typ, type):
|
|
783
|
-
continue
|
|
784
|
-
try:
|
|
785
|
-
if issubclass(typ, t):
|
|
786
|
-
cands.append(md)
|
|
787
|
-
except Exception:
|
|
788
|
-
continue
|
|
789
|
-
if not cands:
|
|
790
|
-
return None
|
|
791
|
-
prim = [m for m in cands if m.primary]
|
|
792
|
-
return prim[0] if prim else cands[0]
|
|
793
|
-
|
|
794
|
-
def _iter_param_types(self, callable_obj: Callable[..., Any]) -> Iterable[type]:
|
|
795
|
-
sig = inspect.signature(callable_obj)
|
|
796
|
-
for name, param in sig.parameters.items():
|
|
797
|
-
if name in ("self", "cls"):
|
|
798
|
-
continue
|
|
799
|
-
if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
800
|
-
continue
|
|
801
|
-
ann = param.annotation
|
|
802
|
-
is_list, elem_t, _ = _extract_list_req(ann)
|
|
803
|
-
t = elem_t if is_list else (ann if isinstance(ann, type) else None)
|
|
804
|
-
if isinstance(t, type):
|
|
805
|
-
yield t
|
|
806
|
-
|
|
807
|
-
def _infer_narrower_scope(self, md: ProviderMetadata) -> Optional[str]:
|
|
808
|
-
if md.concrete_class is not None:
|
|
809
|
-
init = md.concrete_class.__init__
|
|
810
|
-
for t in self._iter_param_types(init):
|
|
811
|
-
dep = self._find_md_for_type(t)
|
|
812
|
-
if dep and dep.scope != "singleton":
|
|
813
|
-
return dep.scope
|
|
814
|
-
if md.factory_class is not None and md.factory_method is not None:
|
|
815
|
-
fn = getattr(md.factory_class, md.factory_method)
|
|
816
|
-
for t in self._iter_param_types(fn):
|
|
817
|
-
dep = self._find_md_for_type(t)
|
|
818
|
-
if dep and dep.scope != "singleton":
|
|
819
|
-
return dep.scope
|
|
820
|
-
if md.infra == "provides":
|
|
821
|
-
fn = self._provides_functions.get(md.key)
|
|
822
|
-
if callable(fn):
|
|
823
|
-
for t in self._iter_param_types(fn):
|
|
824
|
-
dep = self._find_md_for_type(t)
|
|
825
|
-
if dep and dep.scope != "singleton":
|
|
826
|
-
return dep.scope
|
|
827
|
-
return None
|
|
828
|
-
|
|
829
|
-
def _promote_scopes(self) -> None:
|
|
830
|
-
for k, md in list(self._metadata.items()):
|
|
831
|
-
if md.scope == "singleton":
|
|
832
|
-
ns = self._infer_narrower_scope(md)
|
|
833
|
-
if ns and ns != "singleton":
|
|
834
|
-
self._metadata[k] = ProviderMetadata(
|
|
835
|
-
key=md.key,
|
|
836
|
-
provided_type=md.provided_type,
|
|
837
|
-
concrete_class=md.concrete_class,
|
|
838
|
-
factory_class=md.factory_class,
|
|
839
|
-
factory_method=md.factory_method,
|
|
840
|
-
qualifiers=md.qualifiers,
|
|
841
|
-
primary=md.primary,
|
|
842
|
-
lazy=md.lazy,
|
|
843
|
-
infra=md.infra,
|
|
844
|
-
pico_name=md.pico_name,
|
|
845
|
-
override=md.override,
|
|
846
|
-
scope=ns
|
|
847
|
-
)
|
|
848
|
-
|
|
849
|
-
def _rebuild_indexes(self) -> None:
|
|
850
|
-
self._indexes.clear()
|
|
851
|
-
def add(idx: str, val: Any, key: KeyT):
|
|
852
|
-
b = self._indexes.setdefault(idx, {}).setdefault(val, [])
|
|
853
|
-
if key not in b:
|
|
854
|
-
b.append(key)
|
|
855
|
-
for k, md in self._metadata.items():
|
|
856
|
-
for q in md.qualifiers:
|
|
857
|
-
add("qualifier", q, k)
|
|
858
|
-
if md.primary:
|
|
859
|
-
add("primary", True, k)
|
|
860
|
-
add("lazy", bool(md.lazy), k)
|
|
861
|
-
if md.infra is not None:
|
|
862
|
-
add("infra", md.infra, k)
|
|
863
|
-
if md.pico_name is not None:
|
|
864
|
-
add("pico_name", md.pico_name, k)
|
|
865
|
-
|
|
866
|
-
def _find_md_for_name(self, name: str) -> Optional[KeyT]:
|
|
867
|
-
for k, md in self._metadata.items():
|
|
868
|
-
if md.pico_name == name:
|
|
869
|
-
return k
|
|
870
|
-
t = md.provided_type or md.concrete_class
|
|
871
|
-
if isinstance(t, type) and getattr(t, "__name__", "") == name:
|
|
872
|
-
return k
|
|
873
|
-
return None
|
|
874
|
-
|
|
875
|
-
def _validate_bindings(self) -> None:
|
|
876
|
-
errors: List[str] = []
|
|
877
|
-
|
|
878
|
-
def _fmt(k: KeyT) -> str:
|
|
879
|
-
return getattr(k, '__name__', str(k))
|
|
880
|
-
|
|
881
|
-
def _skip_type(t: type) -> bool:
|
|
882
|
-
if t in (str, int, float, bool, bytes):
|
|
883
|
-
return True
|
|
884
|
-
if t is Any:
|
|
885
|
-
return True
|
|
886
|
-
if getattr(t, "_is_protocol", False):
|
|
887
|
-
return True
|
|
888
|
-
return False
|
|
889
|
-
|
|
890
|
-
def _should_validate(param: inspect.Parameter) -> bool:
|
|
891
|
-
if param.default is not inspect._empty:
|
|
892
|
-
return False
|
|
893
|
-
ann = param.annotation
|
|
894
|
-
origin = get_origin(ann)
|
|
895
|
-
if origin is Union:
|
|
896
|
-
args = get_args(ann)
|
|
897
|
-
if type(None) in args:
|
|
898
|
-
return False
|
|
899
|
-
return True
|
|
900
|
-
|
|
901
|
-
loc = ComponentLocator(self._metadata, self._indexes)
|
|
902
|
-
|
|
903
|
-
for k, md in self._metadata.items():
|
|
904
|
-
if md.infra == "configuration":
|
|
905
|
-
continue
|
|
906
|
-
|
|
907
|
-
callables_to_check: List[Callable[..., Any]] = []
|
|
908
|
-
loc_name = "unknown"
|
|
909
|
-
|
|
910
|
-
if md.concrete_class is not None:
|
|
911
|
-
callables_to_check.append(md.concrete_class.__init__)
|
|
912
|
-
loc_name = "constructor"
|
|
913
|
-
elif md.factory_class is not None and md.factory_method is not None:
|
|
914
|
-
try:
|
|
915
|
-
fn = getattr(md.factory_class, md.factory_method)
|
|
916
|
-
callables_to_check.append(fn)
|
|
917
|
-
loc_name = f"factory {_fmt(md.factory_class)}.{md.factory_method}"
|
|
918
|
-
except AttributeError:
|
|
919
|
-
errors.append(f"Component '{_fmt(k)}' refers to missing factory method '{md.factory_method}' on class '{_fmt(md.factory_class)}'")
|
|
920
|
-
continue
|
|
921
|
-
elif md.infra == "provides":
|
|
922
|
-
fn = self._provides_functions.get(k)
|
|
923
|
-
if callable(fn):
|
|
924
|
-
callables_to_check.append(fn)
|
|
925
|
-
loc_name = f"function {getattr(fn, '__name__', 'unknown')}"
|
|
926
|
-
else:
|
|
927
|
-
continue
|
|
928
|
-
else:
|
|
929
|
-
if not md.concrete_class and not md.factory_class and md.override:
|
|
930
|
-
continue
|
|
931
|
-
|
|
932
|
-
for callable_obj in callables_to_check:
|
|
933
|
-
try:
|
|
934
|
-
sig = inspect.signature(callable_obj)
|
|
935
|
-
except (ValueError, TypeError):
|
|
936
|
-
continue
|
|
937
|
-
|
|
938
|
-
for name, param in sig.parameters.items():
|
|
939
|
-
if name in ("self", "cls"):
|
|
940
|
-
continue
|
|
941
|
-
if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
942
|
-
continue
|
|
943
|
-
if not _should_validate(param):
|
|
944
|
-
continue
|
|
945
|
-
|
|
946
|
-
ann = param.annotation
|
|
947
|
-
is_list, elem_t, qual = _extract_list_req(ann)
|
|
948
|
-
|
|
949
|
-
if is_list:
|
|
950
|
-
if qual:
|
|
951
|
-
matching = loc.with_qualifier_any(qual).keys()
|
|
952
|
-
if not matching and isinstance(elem_t, type) and not _skip_type(elem_t):
|
|
953
|
-
errors.append(f"{_fmt(k)} ({loc_name}) expects List[{_fmt(elem_t)}] with qualifier '{qual}' but no matching components exist")
|
|
954
|
-
continue
|
|
955
|
-
|
|
956
|
-
elif isinstance(ann, str):
|
|
957
|
-
key_found_by_name = self._find_md_for_name(ann)
|
|
958
|
-
directly_bound = ann in self._metadata or self._factory.has(ann)
|
|
959
|
-
if not key_found_by_name and not directly_bound:
|
|
960
|
-
errors.append(f"{_fmt(k)} ({loc_name}) depends on string key '{ann}' which is not bound")
|
|
961
|
-
continue
|
|
962
|
-
|
|
963
|
-
elif isinstance(ann, type) and not _skip_type(ann):
|
|
964
|
-
dep_key_found = self._factory.has(ann) or ann in self._metadata
|
|
965
|
-
if not dep_key_found:
|
|
966
|
-
assignable_md = self._find_md_for_type(ann)
|
|
967
|
-
if assignable_md is None:
|
|
968
|
-
by_name_key = self._find_md_for_name(getattr(ann, "__name__", ""))
|
|
969
|
-
if by_name_key is None:
|
|
970
|
-
errors.append(f"{_fmt(k)} ({loc_name}) depends on {_fmt(ann)} which is not bound")
|
|
971
|
-
continue
|
|
972
|
-
|
|
973
|
-
elif ann is inspect._empty:
|
|
974
|
-
name_key = name
|
|
975
|
-
key_found_by_name = self._find_md_for_name(name_key)
|
|
976
|
-
directly_bound = name_key in self._metadata or self._factory.has(name_key)
|
|
977
|
-
if not key_found_by_name and not directly_bound:
|
|
978
|
-
errors.append(f"{_fmt(k)} ({loc_name}) depends on parameter '{name}' with no type hint, and key '{name_key}' is not bound")
|
|
979
|
-
continue
|
|
980
|
-
|
|
981
|
-
if errors:
|
|
982
|
-
raise InvalidBindingError(errors)
|
|
983
|
-
|
|
984
|
-
def finalize(self, overrides: Optional[Dict[KeyT, Any]]) -> None:
|
|
985
|
-
self.select_and_bind()
|
|
986
|
-
self._promote_scopes()
|
|
987
|
-
self._rebuild_indexes()
|
|
988
|
-
for _, selector, default_cls in sorted(self._on_missing, key=lambda x: -x[0]):
|
|
989
|
-
key = selector
|
|
990
|
-
if key in self._metadata or self._factory.has(key) or _can_be_selected_for(self._metadata, selector):
|
|
991
|
-
continue
|
|
992
|
-
provider = DeferredProvider(lambda pico, loc, c=default_cls: _build_class(c, pico, loc))
|
|
993
|
-
qset = set(str(q) for q in getattr(default_cls, PICO_META, {}).get("qualifier", ()))
|
|
994
|
-
sc = getattr(default_cls, PICO_META, {}).get("scope", "singleton")
|
|
995
|
-
md = ProviderMetadata(
|
|
996
|
-
key=key,
|
|
997
|
-
provided_type=key if isinstance(key, type) else None,
|
|
998
|
-
concrete_class=default_cls,
|
|
999
|
-
factory_class=None,
|
|
1000
|
-
factory_method=None,
|
|
1001
|
-
qualifiers=qset,
|
|
1002
|
-
primary=True,
|
|
1003
|
-
lazy=bool(getattr(default_cls, PICO_META, {}).get("lazy", False)),
|
|
1004
|
-
infra=getattr(default_cls, PICO_INFRA, None),
|
|
1005
|
-
pico_name=getattr(default_cls, PICO_NAME, None),
|
|
1006
|
-
override=True,
|
|
1007
|
-
scope=sc
|
|
1008
|
-
)
|
|
1009
|
-
self._bind_if_absent(key, provider)
|
|
1010
|
-
self._metadata[key] = md
|
|
1011
|
-
if isinstance(provider, DeferredProvider):
|
|
1012
|
-
self._deferred.append(provider)
|
|
1013
|
-
self._rebuild_indexes()
|
|
1014
|
-
self._validate_bindings()
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
def init(modules: Union[Any, Iterable[Any]], *, profiles: Tuple[str, ...] = (), allowed_profiles: Optional[Iterable[str]] = None, environ: Optional[Dict[str, str]] = None, overrides: Optional[Dict[KeyT, Any]] = None, logger: Optional[logging.Logger] = None, config: Tuple[ConfigSource, ...] = (), custom_scopes: Optional[Dict[str, "ScopeProtocol"]] = None, validate_only: bool = False, container_id: Optional[str] = None, tree_config: Tuple["TreeSource", ...] = (), observers: Optional[List["ContainerObserver"]] = None,) -> PicoContainer:
|
|
54
|
+
def init(
|
|
55
|
+
modules: Union[Any, Iterable[Any]],
|
|
56
|
+
*,
|
|
57
|
+
profiles: Tuple[str, ...] = (),
|
|
58
|
+
allowed_profiles: Optional[Iterable[str]] = None,
|
|
59
|
+
environ: Optional[Dict[str, str]] = None,
|
|
60
|
+
overrides: Optional[Dict[KeyT, Any]] = None,
|
|
61
|
+
logger: Optional[logging.Logger] = None,
|
|
62
|
+
config: Optional[ContextConfig] = None,
|
|
63
|
+
custom_scopes: Optional[Iterable[str]] = None,
|
|
64
|
+
validate_only: bool = False,
|
|
65
|
+
container_id: Optional[str] = None,
|
|
66
|
+
observers: Optional[List["ContainerObserver"]] = None,
|
|
67
|
+
) -> PicoContainer:
|
|
1018
68
|
active = tuple(p.strip() for p in profiles if p)
|
|
69
|
+
|
|
1019
70
|
allowed_set = set(a.strip() for a in allowed_profiles) if allowed_profiles is not None else None
|
|
1020
71
|
if allowed_set is not None:
|
|
1021
72
|
unknown = set(active) - allowed_set
|
|
1022
73
|
if unknown:
|
|
1023
74
|
raise ConfigurationError(f"Unknown profiles: {sorted(unknown)}; allowed: {sorted(allowed_set)}")
|
|
75
|
+
|
|
1024
76
|
factory = ComponentFactory()
|
|
1025
77
|
caches = ScopedCaches()
|
|
1026
78
|
scopes = ScopeManager()
|
|
1027
79
|
if custom_scopes:
|
|
1028
|
-
for
|
|
1029
|
-
scopes.register_scope(
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
scopes,
|
|
1034
|
-
container_id=container_id,
|
|
1035
|
-
profiles=active,
|
|
1036
|
-
observers=observers or [],
|
|
1037
|
-
)
|
|
1038
|
-
registrar = Registrar(factory, profiles=active, environ=environ, logger=logger, config=config, tree_sources=tree_config)
|
|
80
|
+
for name in custom_scopes:
|
|
81
|
+
scopes.register_scope(name)
|
|
82
|
+
|
|
83
|
+
pico = PicoContainer(factory, caches, scopes, container_id=container_id, profiles=active, observers=observers or [])
|
|
84
|
+
registrar = Registrar(factory, profiles=active, environ=environ, logger=logger, config=config)
|
|
1039
85
|
for m in _iter_input_modules(modules):
|
|
1040
86
|
registrar.register_module(m)
|
|
87
|
+
|
|
1041
88
|
if overrides:
|
|
1042
89
|
for k, v in overrides.items():
|
|
1043
90
|
prov, _ = _normalize_override_provider(v)
|
|
1044
91
|
factory.bind(k, prov)
|
|
1045
|
-
|
|
92
|
+
|
|
93
|
+
registrar.finalize(overrides, pico_instance=pico)
|
|
1046
94
|
if validate_only:
|
|
1047
95
|
locator = registrar.locator()
|
|
1048
96
|
pico.attach_locator(locator)
|
|
1049
97
|
_fail_fast_cycle_check(pico)
|
|
1050
98
|
return pico
|
|
99
|
+
|
|
1051
100
|
locator = registrar.locator()
|
|
1052
101
|
registrar.attach_runtime(pico, locator)
|
|
1053
102
|
pico.attach_locator(locator)
|
|
1054
103
|
_fail_fast_cycle_check(pico)
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
return inspect.signature(fn)
|
|
1082
|
-
|
|
1083
|
-
def _compile_argplan_static(callable_obj: Callable[..., Any], locator: ComponentLocator) -> Tuple[Tuple[str, str, Any], ...]:
|
|
1084
|
-
sig = _get_signature_static(callable_obj)
|
|
1085
|
-
items: List[Tuple[str, str, Any]] = []
|
|
1086
|
-
for name, param in sig.parameters.items():
|
|
1087
|
-
if name in ("self", "cls"):
|
|
1088
|
-
continue
|
|
1089
|
-
if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
1090
|
-
continue
|
|
1091
|
-
ann = param.annotation
|
|
1092
|
-
is_list, elem_t, qual = _extract_list_req(ann)
|
|
1093
|
-
if is_list and locator is not None and isinstance(elem_t, type):
|
|
1094
|
-
keys = _collect_by_type(locator, elem_t, qual)
|
|
1095
|
-
items.append(("list", name, tuple(keys)))
|
|
1096
|
-
continue
|
|
1097
|
-
if ann is not inspect._empty and isinstance(ann, type):
|
|
1098
|
-
key: KeyT = ann
|
|
1099
|
-
elif ann is not inspect._empty and isinstance(ann, str):
|
|
1100
|
-
mapped = _find_key_by_name(locator, ann)
|
|
1101
|
-
key = mapped if mapped is not None else ann
|
|
1102
|
-
else:
|
|
1103
|
-
key = name
|
|
1104
|
-
items.append(("key", name, key))
|
|
1105
|
-
return tuple(items)
|
|
1106
|
-
|
|
1107
|
-
def _dependency_keys_for_static(md: ProviderMetadata, locator: ComponentLocator) -> Tuple[KeyT, ...]:
|
|
1108
|
-
if md.concrete_class is not None:
|
|
1109
|
-
fn = md.concrete_class.__init__
|
|
1110
|
-
elif md.factory_class is not None and md.factory_method is not None:
|
|
1111
|
-
fn = getattr(md.factory_class, md.factory_method)
|
|
1112
|
-
elif md.infra == "provides":
|
|
1113
|
-
fn = getattr(locator, "_provides_functions", {}).get(md.key)
|
|
1114
|
-
if not callable(fn):
|
|
1115
|
-
return ()
|
|
1116
|
-
else:
|
|
1117
|
-
return ()
|
|
1118
|
-
plan = _compile_argplan_static(fn, locator)
|
|
1119
|
-
deps: List[KeyT] = []
|
|
1120
|
-
for kind, _, data in plan:
|
|
1121
|
-
if kind == "key":
|
|
1122
|
-
deps.append(data)
|
|
1123
|
-
else:
|
|
1124
|
-
for k in data:
|
|
1125
|
-
deps.append(k)
|
|
1126
|
-
return tuple(deps)
|
|
1127
|
-
|
|
1128
|
-
def _build_resolution_graph(pico: "PicoContainer") -> Dict[KeyT, Tuple[KeyT, ...]]:
|
|
1129
|
-
loc = pico._locator
|
|
1130
|
-
if not loc:
|
|
1131
|
-
return {}
|
|
1132
|
-
|
|
1133
|
-
def _map_dep_to_bound_key(dep_key: KeyT) -> KeyT:
|
|
1134
|
-
if dep_key in loc._metadata:
|
|
1135
|
-
return dep_key
|
|
1136
|
-
if isinstance(dep_key, type):
|
|
1137
|
-
for k, md in loc._metadata.items():
|
|
1138
|
-
typ = md.provided_type or md.concrete_class
|
|
1139
|
-
if isinstance(typ, type):
|
|
1140
|
-
try:
|
|
1141
|
-
if issubclass(typ, dep_key):
|
|
1142
|
-
return k
|
|
1143
|
-
except Exception:
|
|
1144
|
-
continue
|
|
1145
|
-
return dep_key
|
|
1146
|
-
|
|
1147
|
-
graph: Dict[KeyT, Tuple[KeyT, ...]] = {}
|
|
1148
|
-
for key, md in list(loc._metadata.items()):
|
|
1149
|
-
deps: List[KeyT] = []
|
|
1150
|
-
for d in _dependency_keys_for_static(md, loc):
|
|
1151
|
-
mapped = _map_dep_to_bound_key(d)
|
|
1152
|
-
deps.append(mapped)
|
|
1153
|
-
graph[key] = tuple(deps)
|
|
1154
|
-
return graph
|
|
104
|
+
|
|
105
|
+
if not validate_only:
|
|
106
|
+
eager_singletons = []
|
|
107
|
+
for key, md in locator._metadata.items():
|
|
108
|
+
if md.scope == "singleton" and not md.lazy:
|
|
109
|
+
cache = pico._cache_for(key)
|
|
110
|
+
instance = cache.get(key)
|
|
111
|
+
if instance is None:
|
|
112
|
+
instance = pico.get(key)
|
|
113
|
+
eager_singletons.append(instance)
|
|
114
|
+
else:
|
|
115
|
+
eager_singletons.append(instance)
|
|
116
|
+
|
|
117
|
+
configure_awaitables = []
|
|
118
|
+
for instance in eager_singletons:
|
|
119
|
+
res = pico._run_configure_methods(instance)
|
|
120
|
+
if inspect.isawaitable(res):
|
|
121
|
+
configure_awaitables.append(res)
|
|
122
|
+
|
|
123
|
+
if configure_awaitables:
|
|
124
|
+
raise ConfigurationError(
|
|
125
|
+
"Sync init() found eagerly loaded singletons with async @configure methods. "
|
|
126
|
+
"This can be caused by an async __ainit__ or async @configure. "
|
|
127
|
+
"Use an async main function and await pico.aget() for those components, "
|
|
128
|
+
"or mark them as lazy=True."
|
|
129
|
+
)
|
|
1155
130
|
|
|
131
|
+
return pico
|
|
1156
132
|
|
|
1157
133
|
def _find_cycle(graph: Dict[KeyT, Tuple[KeyT, ...]]) -> Optional[Tuple[KeyT, ...]]:
|
|
1158
134
|
temp: Set[KeyT] = set()
|
|
@@ -1187,10 +163,9 @@ def _format_key(k: KeyT) -> str:
|
|
|
1187
163
|
return getattr(k, "__name__", str(k))
|
|
1188
164
|
|
|
1189
165
|
def _fail_fast_cycle_check(pico: "PicoContainer") -> None:
|
|
1190
|
-
graph =
|
|
166
|
+
graph = pico.build_resolution_graph()
|
|
1191
167
|
cyc = _find_cycle(graph)
|
|
1192
168
|
if not cyc:
|
|
1193
169
|
return
|
|
1194
170
|
path = " -> ".join(_format_key(k) for k in cyc)
|
|
1195
171
|
raise InvalidBindingError([f"Circular dependency detected: {path}"])
|
|
1196
|
-
|