pico-ioc 2.0.5__py3-none-any.whl → 2.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pico_ioc/__init__.py +7 -11
- pico_ioc/_version.py +1 -1
- pico_ioc/analysis.py +92 -0
- pico_ioc/aop.py +3 -6
- pico_ioc/api.py +27 -1084
- pico_ioc/component_scanner.py +166 -0
- pico_ioc/config_builder.py +91 -0
- pico_ioc/config_registrar.py +219 -0
- pico_ioc/config_runtime.py +17 -2
- pico_ioc/container.py +197 -156
- pico_ioc/decorators.py +192 -0
- pico_ioc/dependency_validator.py +103 -0
- pico_ioc/exceptions.py +0 -16
- pico_ioc/factory.py +2 -1
- pico_ioc/locator.py +80 -2
- pico_ioc/provider_selector.py +35 -0
- pico_ioc/registrar.py +169 -0
- {pico_ioc-2.0.5.dist-info → pico_ioc-2.1.0.dist-info}/METADATA +93 -44
- pico_ioc-2.1.0.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.0.dist-info}/WHEEL +0 -0
- {pico_ioc-2.0.5.dist-info → pico_ioc-2.1.0.dist-info}/licenses/LICENSE +0 -0
- {pico_ioc-2.0.5.dist-info → pico_ioc-2.1.0.dist-info}/top_level.txt +0 -0
pico_ioc/container.py
CHANGED
|
@@ -1,86 +1,75 @@
|
|
|
1
|
-
# src/pico_ioc/container.py
|
|
2
1
|
import inspect
|
|
3
2
|
import contextvars
|
|
4
|
-
|
|
3
|
+
import functools
|
|
4
|
+
from typing import Any, Dict, List, Optional, Tuple, overload, Union, Callable, Iterable, Set, get_args, get_origin, Annotated, Protocol, Mapping
|
|
5
5
|
from contextlib import contextmanager
|
|
6
6
|
from .constants import LOGGER, PICO_META
|
|
7
|
-
from .exceptions import
|
|
8
|
-
from .factory import ComponentFactory
|
|
7
|
+
from .exceptions import ComponentCreationError, ProviderNotFoundError, AsyncResolutionError
|
|
8
|
+
from .factory import ComponentFactory, ProviderMetadata
|
|
9
9
|
from .locator import ComponentLocator
|
|
10
10
|
from .scope import ScopedCaches, ScopeManager
|
|
11
11
|
from .aop import UnifiedComponentProxy, ContainerObserver
|
|
12
|
+
from .analysis import analyze_callable_dependencies, DependencyRequest
|
|
12
13
|
|
|
13
14
|
KeyT = Union[str, type]
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return
|
|
43
|
-
|
|
44
|
-
def
|
|
45
|
-
if
|
|
46
|
-
return
|
|
47
|
-
|
|
48
|
-
if
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
full = tuple(chain) + (current,)
|
|
73
|
-
for idx, k in enumerate(full, 1):
|
|
74
|
-
mark = " ❌" if idx == len(full) else ""
|
|
75
|
-
lines.append(f" {idx}. {name_of(k)} [scope={scope_of(k)}]{mark}")
|
|
76
|
-
if idx < len(full):
|
|
77
|
-
parent = k
|
|
78
|
-
child = full[idx]
|
|
79
|
-
via, param = self._edges.get((parent, child), ("provider", "?"))
|
|
80
|
-
lines.append(f" └─ via {via} param '{param}' → {name_of(child)}")
|
|
81
|
-
lines.append("")
|
|
82
|
-
lines.append("Hint: break the cycle with a @configure setter or use a factory/provider.")
|
|
83
|
-
return "\n".join(lines)
|
|
15
|
+
|
|
16
|
+
def _normalize_callable(obj):
|
|
17
|
+
return getattr(obj, '__func__', obj)
|
|
18
|
+
|
|
19
|
+
def _get_signature_safe(callable_obj):
|
|
20
|
+
try:
|
|
21
|
+
return inspect.signature(callable_obj)
|
|
22
|
+
except (ValueError, TypeError):
|
|
23
|
+
wrapped = getattr(callable_obj, '__wrapped__', None)
|
|
24
|
+
if wrapped is not None:
|
|
25
|
+
return inspect.signature(wrapped)
|
|
26
|
+
raise
|
|
27
|
+
|
|
28
|
+
def _needs_async_configure(obj: Any) -> bool:
|
|
29
|
+
for _, m in inspect.getmembers(obj, predicate=inspect.ismethod):
|
|
30
|
+
meta = getattr(m, PICO_META, {})
|
|
31
|
+
if meta.get("configure", False) and inspect.iscoroutinefunction(m):
|
|
32
|
+
return True
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
def _iter_configure_methods(obj: Any):
|
|
36
|
+
for _, m in inspect.getmembers(obj, predicate=inspect.ismethod):
|
|
37
|
+
meta = getattr(m, PICO_META, {})
|
|
38
|
+
if meta.get("configure", False):
|
|
39
|
+
yield m
|
|
40
|
+
|
|
41
|
+
def _build_resolution_graph(loc) -> Dict[KeyT, Tuple[KeyT, ...]]:
|
|
42
|
+
if not loc:
|
|
43
|
+
return {}
|
|
44
|
+
|
|
45
|
+
def _map_dep_to_bound_key(dep_key: KeyT) -> KeyT:
|
|
46
|
+
if dep_key in loc._metadata:
|
|
47
|
+
return dep_key
|
|
48
|
+
|
|
49
|
+
if isinstance(dep_key, str):
|
|
50
|
+
mapped = loc.find_key_by_name(dep_key)
|
|
51
|
+
if mapped is not None:
|
|
52
|
+
return mapped
|
|
53
|
+
|
|
54
|
+
if isinstance(dep_key, type):
|
|
55
|
+
for k, md in loc._metadata.items():
|
|
56
|
+
typ = md.provided_type or md.concrete_class
|
|
57
|
+
if isinstance(typ, type):
|
|
58
|
+
try:
|
|
59
|
+
if issubclass(typ, dep_key):
|
|
60
|
+
return k
|
|
61
|
+
except Exception:
|
|
62
|
+
continue
|
|
63
|
+
return dep_key
|
|
64
|
+
|
|
65
|
+
graph: Dict[KeyT, Tuple[KeyT, ...]] = {}
|
|
66
|
+
for key, md in list(loc._metadata.items()):
|
|
67
|
+
deps: List[KeyT] = []
|
|
68
|
+
for d in loc.dependency_keys_for_static(md):
|
|
69
|
+
mapped = _map_dep_to_bound_key(d)
|
|
70
|
+
deps.append(mapped)
|
|
71
|
+
graph[key] = tuple(deps)
|
|
72
|
+
return graph
|
|
84
73
|
|
|
85
74
|
class PicoContainer:
|
|
86
75
|
_container_id_var: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar("pico_container_id", default=None)
|
|
@@ -104,7 +93,6 @@ class PicoContainer:
|
|
|
104
93
|
import time as _t
|
|
105
94
|
self.context = PicoContainer._Ctx(container_id=self.container_id, profiles=profiles, created_at=_t.time())
|
|
106
95
|
PicoContainer._container_registry[self.container_id] = self
|
|
107
|
-
self._tracer = ResolutionTracer(self)
|
|
108
96
|
|
|
109
97
|
@staticmethod
|
|
110
98
|
def _generate_container_id() -> str:
|
|
@@ -153,6 +141,7 @@ class PicoContainer:
|
|
|
153
141
|
def _canonical_key(self, key: KeyT) -> KeyT:
|
|
154
142
|
if self._factory.has(key):
|
|
155
143
|
return key
|
|
144
|
+
|
|
156
145
|
if isinstance(key, type) and self._locator:
|
|
157
146
|
cands: List[Tuple[bool, Any]] = []
|
|
158
147
|
for k, md in self._locator._metadata.items():
|
|
@@ -167,16 +156,19 @@ class PicoContainer:
|
|
|
167
156
|
if cands:
|
|
168
157
|
prim = [k for is_p, k in cands if is_p]
|
|
169
158
|
return prim[0] if prim else cands[0][1]
|
|
159
|
+
|
|
170
160
|
if isinstance(key, str) and self._locator:
|
|
171
161
|
for k, md in self._locator._metadata.items():
|
|
172
162
|
if md.pico_name == key:
|
|
173
163
|
return k
|
|
164
|
+
|
|
174
165
|
return key
|
|
175
166
|
|
|
176
167
|
def _resolve_or_create_internal(self, key: KeyT) -> Tuple[Any, float, bool]:
|
|
177
168
|
key = self._canonical_key(key)
|
|
178
169
|
cache = self._cache_for(key)
|
|
179
170
|
cached = cache.get(key)
|
|
171
|
+
|
|
180
172
|
if cached is not None:
|
|
181
173
|
self.context.cache_hit_count += 1
|
|
182
174
|
for o in self._observers: o.on_cache_hit(key)
|
|
@@ -184,19 +176,9 @@ class PicoContainer:
|
|
|
184
176
|
|
|
185
177
|
import time as _tm
|
|
186
178
|
t0 = _tm.perf_counter()
|
|
187
|
-
chain = list(_resolve_chain.get())
|
|
188
|
-
|
|
189
|
-
for k_in_chain in chain:
|
|
190
|
-
if k_in_chain == key:
|
|
191
|
-
details = self._tracer.describe_cycle(tuple(chain), key, self._locator)
|
|
192
|
-
raise ComponentCreationError(key, CircularDependencyError(chain, key, details=details))
|
|
193
179
|
|
|
194
|
-
token_chain = _resolve_chain.set(tuple(chain + [key]))
|
|
195
180
|
token_container = self.activate()
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
requester = chain[-1] if chain else None
|
|
199
|
-
instance_or_awaitable = None
|
|
181
|
+
requester = None
|
|
200
182
|
|
|
201
183
|
try:
|
|
202
184
|
provider = self._factory.get(key, origin=requester)
|
|
@@ -211,8 +193,6 @@ class PicoContainer:
|
|
|
211
193
|
return instance_or_awaitable, took_ms, False
|
|
212
194
|
|
|
213
195
|
finally:
|
|
214
|
-
self._tracer.leave(token_tracer)
|
|
215
|
-
_resolve_chain.reset(token_chain)
|
|
216
196
|
self.deactivate(token_container)
|
|
217
197
|
|
|
218
198
|
@overload
|
|
@@ -256,24 +236,6 @@ class PicoContainer:
|
|
|
256
236
|
|
|
257
237
|
return final_instance
|
|
258
238
|
|
|
259
|
-
def _resolve_type_key(self, key: type):
|
|
260
|
-
if not self._locator:
|
|
261
|
-
return None
|
|
262
|
-
cands: List[Tuple[bool, Any]] = []
|
|
263
|
-
for k, md in self._locator._metadata.items():
|
|
264
|
-
typ = md.provided_type or md.concrete_class
|
|
265
|
-
if not isinstance(typ, type):
|
|
266
|
-
continue
|
|
267
|
-
try:
|
|
268
|
-
if typ is not key and issubclass(typ, key):
|
|
269
|
-
cands.append((md.primary, k))
|
|
270
|
-
except Exception:
|
|
271
|
-
continue
|
|
272
|
-
if not cands:
|
|
273
|
-
return None
|
|
274
|
-
prim = [k for is_p, k in cands if is_p]
|
|
275
|
-
return prim[0] if prim else cands[0][1]
|
|
276
|
-
|
|
277
239
|
def _maybe_wrap_with_aspects(self, key, instance: Any) -> Any:
|
|
278
240
|
if isinstance(instance, UnifiedComponentProxy):
|
|
279
241
|
return instance
|
|
@@ -283,14 +245,9 @@ class PicoContainer:
|
|
|
283
245
|
return UnifiedComponentProxy(container=self, target=instance)
|
|
284
246
|
return instance
|
|
285
247
|
|
|
286
|
-
def
|
|
287
|
-
for _, obj in self._caches.all_items()
|
|
288
|
-
|
|
289
|
-
meta = getattr(m, PICO_META, {})
|
|
290
|
-
if meta.get("cleanup", False):
|
|
291
|
-
from .api import _resolve_args
|
|
292
|
-
kwargs = _resolve_args(m, self)
|
|
293
|
-
m(**kwargs)
|
|
248
|
+
def _iterate_cleanup_targets(self) -> Iterable[Any]:
|
|
249
|
+
yield from (obj for _, obj in self._caches.all_items())
|
|
250
|
+
|
|
294
251
|
if self._locator:
|
|
295
252
|
seen = set()
|
|
296
253
|
for md in self._locator._metadata.values():
|
|
@@ -298,12 +255,37 @@ class PicoContainer:
|
|
|
298
255
|
if fc and fc not in seen:
|
|
299
256
|
seen.add(fc)
|
|
300
257
|
inst = self.get(fc) if self._factory.has(fc) else fc()
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
258
|
+
yield inst
|
|
259
|
+
|
|
260
|
+
def _call_cleanup_method(self, method: Callable[..., Any]) -> Any:
|
|
261
|
+
deps_requests = analyze_callable_dependencies(method)
|
|
262
|
+
return method(**self._resolve_args(deps_requests))
|
|
263
|
+
|
|
264
|
+
def cleanup_all(self) -> None:
|
|
265
|
+
for obj in self._iterate_cleanup_targets():
|
|
266
|
+
for _, m in inspect.getmembers(obj, predicate=inspect.ismethod):
|
|
267
|
+
meta = getattr(m, PICO_META, {})
|
|
268
|
+
if meta.get("cleanup", False):
|
|
269
|
+
res = self._call_cleanup_method(m)
|
|
270
|
+
if inspect.isawaitable(res):
|
|
271
|
+
LOGGER.warning(f"Async cleanup method {m} called during sync shutdown. Awaitable ignored.")
|
|
272
|
+
|
|
273
|
+
async def cleanup_all_async(self) -> None:
|
|
274
|
+
for obj in self._iterate_cleanup_targets():
|
|
275
|
+
for _, m in inspect.getmembers(obj, predicate=inspect.ismethod):
|
|
276
|
+
meta = getattr(m, PICO_META, {})
|
|
277
|
+
if meta.get("cleanup", False):
|
|
278
|
+
res = self._call_cleanup_method(m)
|
|
279
|
+
if inspect.isawaitable(res):
|
|
280
|
+
await res
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
from .event_bus import EventBus
|
|
284
|
+
for _, obj in self._caches.all_items():
|
|
285
|
+
if isinstance(obj, EventBus):
|
|
286
|
+
await obj.aclose()
|
|
287
|
+
except Exception:
|
|
288
|
+
pass
|
|
307
289
|
|
|
308
290
|
def activate_scope(self, name: str, scope_id: Any):
|
|
309
291
|
return self.scopes.activate(name, scope_id)
|
|
@@ -333,39 +315,6 @@ class PicoContainer:
|
|
|
333
315
|
out[f"{getattr(k,'__name__',k)}.{name}"] = False
|
|
334
316
|
return out
|
|
335
317
|
|
|
336
|
-
async def cleanup_all_async(self) -> None:
|
|
337
|
-
for _, obj in self._caches.all_items():
|
|
338
|
-
for _, m in inspect.getmembers(obj, predicate=inspect.ismethod):
|
|
339
|
-
meta = getattr(m, PICO_META, {})
|
|
340
|
-
if meta.get("cleanup", False):
|
|
341
|
-
from .api import _resolve_args
|
|
342
|
-
res = m(**_resolve_args(m, self))
|
|
343
|
-
import inspect as _i
|
|
344
|
-
if _i.isawaitable(res):
|
|
345
|
-
await res
|
|
346
|
-
if self._locator:
|
|
347
|
-
seen = set()
|
|
348
|
-
for md in self._locator._metadata.values():
|
|
349
|
-
fc = md.factory_class
|
|
350
|
-
if fc and fc not in seen:
|
|
351
|
-
seen.add(fc)
|
|
352
|
-
inst = self.get(fc) if self._factory.has(fc) else fc()
|
|
353
|
-
for _, m in inspect.getmembers(inst, predicate=inspect.ismethod):
|
|
354
|
-
meta = getattr(m, PICO_META, {})
|
|
355
|
-
if meta.get("cleanup", False):
|
|
356
|
-
from .api import _resolve_args
|
|
357
|
-
res = m(**_resolve_args(m, self))
|
|
358
|
-
import inspect as _i
|
|
359
|
-
if _i.isawaitable(res):
|
|
360
|
-
await res
|
|
361
|
-
try:
|
|
362
|
-
from .event_bus import EventBus
|
|
363
|
-
for _, obj in self._caches.all_items():
|
|
364
|
-
if isinstance(obj, EventBus):
|
|
365
|
-
await obj.aclose()
|
|
366
|
-
except Exception:
|
|
367
|
-
pass
|
|
368
|
-
|
|
369
318
|
def stats(self) -> Dict[str, Any]:
|
|
370
319
|
import time as _t
|
|
371
320
|
resolves = self.context.resolve_count
|
|
@@ -385,6 +334,9 @@ class PicoContainer:
|
|
|
385
334
|
self.cleanup_all()
|
|
386
335
|
PicoContainer._container_registry.pop(self.container_id, None)
|
|
387
336
|
|
|
337
|
+
def build_resolution_graph(self) -> None:
|
|
338
|
+
return _build_resolution_graph(self._locator)
|
|
339
|
+
|
|
388
340
|
def export_graph(
|
|
389
341
|
self,
|
|
390
342
|
path: str,
|
|
@@ -398,10 +350,8 @@ class PicoContainer:
|
|
|
398
350
|
if not self._locator:
|
|
399
351
|
raise RuntimeError("No locator attached; cannot export dependency graph.")
|
|
400
352
|
|
|
401
|
-
from .api import _build_resolution_graph
|
|
402
|
-
|
|
403
353
|
md_by_key = self._locator._metadata
|
|
404
|
-
graph = _build_resolution_graph(self)
|
|
354
|
+
graph = _build_resolution_graph(self._locator)
|
|
405
355
|
|
|
406
356
|
lines: List[str] = []
|
|
407
357
|
lines.append("digraph Pico {")
|
|
@@ -441,3 +391,94 @@ class PicoContainer:
|
|
|
441
391
|
with open(path, "w", encoding="utf-8") as f:
|
|
442
392
|
f.write("\n".join(lines))
|
|
443
393
|
|
|
394
|
+
|
|
395
|
+
def _resolve_args(self, dependencies: Tuple[DependencyRequest, ...]) -> Dict[str, Any]:
|
|
396
|
+
kwargs: Dict[str, Any] = {}
|
|
397
|
+
if not dependencies:
|
|
398
|
+
return kwargs
|
|
399
|
+
|
|
400
|
+
for dep in dependencies:
|
|
401
|
+
if dep.is_list:
|
|
402
|
+
keys: Tuple[KeyT, ...] = ()
|
|
403
|
+
if self._locator is not None and isinstance(dep.key, type):
|
|
404
|
+
keys = tuple(self._locator.collect_by_type(dep.key, dep.qualifier))
|
|
405
|
+
kwargs[dep.parameter_name] = [self.get(k) for k in keys]
|
|
406
|
+
continue
|
|
407
|
+
|
|
408
|
+
primary_key = dep.key
|
|
409
|
+
if isinstance(primary_key, str) and self._locator is not None:
|
|
410
|
+
mapped = self._locator.find_key_by_name(primary_key)
|
|
411
|
+
primary_key = mapped if mapped is not None else primary_key
|
|
412
|
+
|
|
413
|
+
try:
|
|
414
|
+
kwargs[dep.parameter_name] = self.get(primary_key)
|
|
415
|
+
except Exception as first_error:
|
|
416
|
+
if primary_key != dep.parameter_name:
|
|
417
|
+
try:
|
|
418
|
+
kwargs[dep.parameter_name] = self.get(dep.parameter_name)
|
|
419
|
+
except Exception:
|
|
420
|
+
raise first_error from None
|
|
421
|
+
else:
|
|
422
|
+
raise first_error from None
|
|
423
|
+
return kwargs
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def build_class(self, cls: type, locator: ComponentLocator, dependencies: Tuple[DependencyRequest, ...]) -> Any:
|
|
427
|
+
init = cls.__init__
|
|
428
|
+
if init is object.__init__:
|
|
429
|
+
inst = cls()
|
|
430
|
+
else:
|
|
431
|
+
deps = self._resolve_args(dependencies)
|
|
432
|
+
inst = cls(**deps)
|
|
433
|
+
|
|
434
|
+
ainit = getattr(inst, "__ainit__", None)
|
|
435
|
+
has_async = (callable(ainit) and inspect.iscoroutinefunction(ainit)) or _needs_async_configure(inst)
|
|
436
|
+
|
|
437
|
+
if has_async:
|
|
438
|
+
async def runner():
|
|
439
|
+
if callable(ainit):
|
|
440
|
+
kwargs = {}
|
|
441
|
+
try:
|
|
442
|
+
ainit_deps = analyze_callable_dependencies(ainit)
|
|
443
|
+
kwargs = self._resolve_args(ainit_deps)
|
|
444
|
+
except Exception:
|
|
445
|
+
kwargs = {}
|
|
446
|
+
res = ainit(**kwargs)
|
|
447
|
+
if inspect.isawaitable(res):
|
|
448
|
+
await res
|
|
449
|
+
for m in _iter_configure_methods(inst):
|
|
450
|
+
configure_deps = analyze_callable_dependencies(m)
|
|
451
|
+
args = self._resolve_args(configure_deps)
|
|
452
|
+
r = m(**args)
|
|
453
|
+
if inspect.isawaitable(r):
|
|
454
|
+
await r
|
|
455
|
+
return inst
|
|
456
|
+
return runner()
|
|
457
|
+
|
|
458
|
+
for m in _iter_configure_methods(inst):
|
|
459
|
+
configure_deps = analyze_callable_dependencies(m)
|
|
460
|
+
args = self._resolve_args(configure_deps)
|
|
461
|
+
m(**args)
|
|
462
|
+
return inst
|
|
463
|
+
|
|
464
|
+
def build_method(self, fn: Callable[..., Any], locator: ComponentLocator, dependencies: Tuple[DependencyRequest, ...]) -> Any:
|
|
465
|
+
deps = self._resolve_args(dependencies)
|
|
466
|
+
obj = fn(**deps)
|
|
467
|
+
|
|
468
|
+
has_async = _needs_async_configure(obj)
|
|
469
|
+
if has_async:
|
|
470
|
+
async def runner():
|
|
471
|
+
for m in _iter_configure_methods(obj):
|
|
472
|
+
configure_deps = analyze_callable_dependencies(m)
|
|
473
|
+
args = self._resolve_args(configure_deps)
|
|
474
|
+
r = m(**args)
|
|
475
|
+
if inspect.isawaitable(r):
|
|
476
|
+
await r
|
|
477
|
+
return obj
|
|
478
|
+
return runner()
|
|
479
|
+
|
|
480
|
+
for m in _iter_configure_methods(obj):
|
|
481
|
+
configure_deps = analyze_callable_dependencies(m)
|
|
482
|
+
args = self._resolve_args(configure_deps)
|
|
483
|
+
m(**args)
|
|
484
|
+
return obj
|
pico_ioc/decorators.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
from typing import Any, Callable, Dict, Iterable, Optional
|
|
2
|
+
import inspect
|
|
3
|
+
from dataclasses import MISSING
|
|
4
|
+
from .constants import PICO_INFRA, PICO_NAME, PICO_KEY, PICO_META
|
|
5
|
+
|
|
6
|
+
def _meta_get(obj: Any) -> Dict[str, Any]:
|
|
7
|
+
m = getattr(obj, PICO_META, None)
|
|
8
|
+
if m is None:
|
|
9
|
+
m = {}
|
|
10
|
+
setattr(obj, PICO_META, m)
|
|
11
|
+
return m
|
|
12
|
+
|
|
13
|
+
def _apply_common_metadata(
|
|
14
|
+
obj: Any,
|
|
15
|
+
*,
|
|
16
|
+
qualifiers: Iterable[str] = (),
|
|
17
|
+
scope: str = "singleton",
|
|
18
|
+
primary: bool = False,
|
|
19
|
+
lazy: bool = False,
|
|
20
|
+
conditional_profiles: Iterable[str] = (),
|
|
21
|
+
conditional_require_env: Iterable[str] = (),
|
|
22
|
+
conditional_predicate: Optional[Callable[[], bool]] = None,
|
|
23
|
+
on_missing_selector: Optional[object] = None,
|
|
24
|
+
on_missing_priority: int = 0,
|
|
25
|
+
):
|
|
26
|
+
m = _meta_get(obj)
|
|
27
|
+
m["qualifier"] = tuple(str(q) for q in qualifiers or ())
|
|
28
|
+
m["scope"] = scope
|
|
29
|
+
|
|
30
|
+
if primary:
|
|
31
|
+
m["primary"] = True
|
|
32
|
+
if lazy:
|
|
33
|
+
m["lazy"] = True
|
|
34
|
+
|
|
35
|
+
has_conditional = (
|
|
36
|
+
conditional_profiles or
|
|
37
|
+
conditional_require_env or
|
|
38
|
+
conditional_predicate is not None
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if has_conditional:
|
|
42
|
+
m["conditional"] = {
|
|
43
|
+
"profiles": tuple(p for p in conditional_profiles or ()),
|
|
44
|
+
"require_env": tuple(e for e in conditional_require_env or ()),
|
|
45
|
+
"predicate": conditional_predicate,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if on_missing_selector is not None:
|
|
49
|
+
m["on_missing"] = {
|
|
50
|
+
"selector": on_missing_selector,
|
|
51
|
+
"priority": int(on_missing_priority)
|
|
52
|
+
}
|
|
53
|
+
return obj
|
|
54
|
+
|
|
55
|
+
def component(
|
|
56
|
+
cls=None,
|
|
57
|
+
*,
|
|
58
|
+
name: Any = None,
|
|
59
|
+
qualifiers: Iterable[str] = (),
|
|
60
|
+
scope: str = "singleton",
|
|
61
|
+
primary: bool = False,
|
|
62
|
+
lazy: bool = False,
|
|
63
|
+
conditional_profiles: Iterable[str] = (),
|
|
64
|
+
conditional_require_env: Iterable[str] = (),
|
|
65
|
+
conditional_predicate: Optional[Callable[[], bool]] = None,
|
|
66
|
+
on_missing_selector: Optional[object] = None,
|
|
67
|
+
on_missing_priority: int = 0,
|
|
68
|
+
):
|
|
69
|
+
def dec(c):
|
|
70
|
+
setattr(c, PICO_INFRA, "component")
|
|
71
|
+
setattr(c, PICO_NAME, name if name is not None else getattr(c, "__name__", str(c)))
|
|
72
|
+
setattr(c, PICO_KEY, name if name is not None else c)
|
|
73
|
+
|
|
74
|
+
_apply_common_metadata(
|
|
75
|
+
c,
|
|
76
|
+
qualifiers=qualifiers,
|
|
77
|
+
scope=scope,
|
|
78
|
+
primary=primary,
|
|
79
|
+
lazy=lazy,
|
|
80
|
+
conditional_profiles=conditional_profiles,
|
|
81
|
+
conditional_require_env=conditional_require_env,
|
|
82
|
+
conditional_predicate=conditional_predicate,
|
|
83
|
+
on_missing_selector=on_missing_selector,
|
|
84
|
+
on_missing_priority=on_missing_priority,
|
|
85
|
+
)
|
|
86
|
+
return c
|
|
87
|
+
return dec(cls) if cls else dec
|
|
88
|
+
|
|
89
|
+
def factory(
|
|
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, "factory")
|
|
105
|
+
setattr(c, PICO_NAME, name if name is not None else getattr(c, "__name__", str(c)))
|
|
106
|
+
|
|
107
|
+
_apply_common_metadata(
|
|
108
|
+
c,
|
|
109
|
+
qualifiers=qualifiers,
|
|
110
|
+
scope=scope,
|
|
111
|
+
primary=primary,
|
|
112
|
+
lazy=lazy,
|
|
113
|
+
conditional_profiles=conditional_profiles,
|
|
114
|
+
conditional_require_env=conditional_require_env,
|
|
115
|
+
conditional_predicate=conditional_predicate,
|
|
116
|
+
on_missing_selector=on_missing_selector,
|
|
117
|
+
on_missing_priority=on_missing_priority,
|
|
118
|
+
)
|
|
119
|
+
return c
|
|
120
|
+
return dec(cls) if cls else dec
|
|
121
|
+
|
|
122
|
+
def provides(*dargs, **dkwargs):
|
|
123
|
+
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):
|
|
124
|
+
target = fn.__func__ if isinstance(fn, (staticmethod, classmethod)) else fn
|
|
125
|
+
|
|
126
|
+
inferred_key = key_hint
|
|
127
|
+
if inferred_key is MISSING:
|
|
128
|
+
rt = get_return_type(target)
|
|
129
|
+
if isinstance(rt, type):
|
|
130
|
+
inferred_key = rt
|
|
131
|
+
else:
|
|
132
|
+
inferred_key = getattr(target, "__name__", str(target))
|
|
133
|
+
|
|
134
|
+
setattr(target, PICO_INFRA, "provides")
|
|
135
|
+
pico_name = name if name is not None else (inferred_key if isinstance(inferred_key, str) else getattr(target, "__name__", str(target)))
|
|
136
|
+
setattr(target, PICO_NAME, pico_name)
|
|
137
|
+
setattr(target, PICO_KEY, inferred_key)
|
|
138
|
+
|
|
139
|
+
_apply_common_metadata(
|
|
140
|
+
target,
|
|
141
|
+
qualifiers=qualifiers,
|
|
142
|
+
scope=scope,
|
|
143
|
+
primary=primary,
|
|
144
|
+
lazy=lazy,
|
|
145
|
+
conditional_profiles=conditional_profiles,
|
|
146
|
+
conditional_require_env=conditional_require_env,
|
|
147
|
+
conditional_predicate=conditional_predicate,
|
|
148
|
+
on_missing_selector=on_missing_selector,
|
|
149
|
+
on_missing_priority=on_missing_priority,
|
|
150
|
+
)
|
|
151
|
+
return fn
|
|
152
|
+
|
|
153
|
+
if dargs and len(dargs) == 1 and inspect.isfunction(dargs[0]) and not dkwargs:
|
|
154
|
+
fn = dargs[0]
|
|
155
|
+
return _apply(fn, MISSING)
|
|
156
|
+
else:
|
|
157
|
+
key = dargs[0] if dargs else MISSING
|
|
158
|
+
def _decorator(fn):
|
|
159
|
+
return _apply(fn, key, **dkwargs)
|
|
160
|
+
return _decorator
|
|
161
|
+
|
|
162
|
+
class Qualifier(str):
|
|
163
|
+
__slots__ = ()
|
|
164
|
+
|
|
165
|
+
def configure(fn):
|
|
166
|
+
m = _meta_get(fn)
|
|
167
|
+
m["configure"] = True
|
|
168
|
+
return fn
|
|
169
|
+
|
|
170
|
+
def cleanup(fn):
|
|
171
|
+
m = _meta_get(fn)
|
|
172
|
+
m["cleanup"] = True
|
|
173
|
+
return fn
|
|
174
|
+
|
|
175
|
+
def configured(target: Any, *, prefix: str = "", mapping: str = "auto"):
|
|
176
|
+
if mapping not in ("auto", "flat", "tree"):
|
|
177
|
+
raise ValueError("mapping must be one of 'auto', 'flat', or 'tree'")
|
|
178
|
+
def dec(cls):
|
|
179
|
+
setattr(cls, PICO_INFRA, "configured")
|
|
180
|
+
m = _meta_get(cls)
|
|
181
|
+
m["configured"] = {"target": target, "prefix": prefix, "mapping": mapping}
|
|
182
|
+
return cls
|
|
183
|
+
return dec
|
|
184
|
+
|
|
185
|
+
def get_return_type(fn: Callable[..., Any]) -> Optional[type]:
|
|
186
|
+
try:
|
|
187
|
+
ra = inspect.signature(fn).return_annotation
|
|
188
|
+
except Exception:
|
|
189
|
+
return None
|
|
190
|
+
if ra is inspect._empty:
|
|
191
|
+
return None
|
|
192
|
+
return ra if isinstance(ra, type) else None
|