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/container.py
CHANGED
|
@@ -1,86 +1,77 @@
|
|
|
1
1
|
# src/pico_ioc/container.py
|
|
2
|
+
|
|
2
3
|
import inspect
|
|
3
4
|
import contextvars
|
|
4
|
-
|
|
5
|
+
import functools
|
|
6
|
+
from typing import Any, Dict, List, Optional, Tuple, overload, Union, Callable, Iterable, Set, get_args, get_origin, Annotated, Protocol, Mapping
|
|
5
7
|
from contextlib import contextmanager
|
|
6
8
|
from .constants import LOGGER, PICO_META
|
|
7
|
-
from .exceptions import
|
|
8
|
-
from .factory import ComponentFactory
|
|
9
|
+
from .exceptions import ComponentCreationError, ProviderNotFoundError, AsyncResolutionError, ConfigurationError
|
|
10
|
+
from .factory import ComponentFactory, ProviderMetadata
|
|
9
11
|
from .locator import ComponentLocator
|
|
10
12
|
from .scope import ScopedCaches, ScopeManager
|
|
11
13
|
from .aop import UnifiedComponentProxy, ContainerObserver
|
|
14
|
+
from .analysis import analyze_callable_dependencies, DependencyRequest
|
|
12
15
|
|
|
13
16
|
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)
|
|
17
|
+
|
|
18
|
+
def _normalize_callable(obj):
|
|
19
|
+
return getattr(obj, '__func__', obj)
|
|
20
|
+
|
|
21
|
+
def _get_signature_safe(callable_obj):
|
|
22
|
+
try:
|
|
23
|
+
return inspect.signature(callable_obj)
|
|
24
|
+
except (ValueError, TypeError):
|
|
25
|
+
wrapped = getattr(callable_obj, '__wrapped__', None)
|
|
26
|
+
if wrapped is not None:
|
|
27
|
+
return inspect.signature(wrapped)
|
|
28
|
+
raise
|
|
29
|
+
|
|
30
|
+
def _needs_async_configure(obj: Any) -> bool:
|
|
31
|
+
for _, m in inspect.getmembers(obj, predicate=inspect.ismethod):
|
|
32
|
+
meta = getattr(m, PICO_META, {})
|
|
33
|
+
if meta.get("configure", False) and inspect.iscoroutinefunction(m):
|
|
34
|
+
return True
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
def _iter_configure_methods(obj: Any):
|
|
38
|
+
for _, m in inspect.getmembers(obj, predicate=inspect.ismethod):
|
|
39
|
+
meta = getattr(m, PICO_META, {})
|
|
40
|
+
if meta.get("configure", False):
|
|
41
|
+
yield m
|
|
42
|
+
|
|
43
|
+
def _build_resolution_graph(loc) -> Dict[KeyT, Tuple[KeyT, ...]]:
|
|
44
|
+
if not loc:
|
|
45
|
+
return {}
|
|
46
|
+
|
|
47
|
+
def _map_dep_to_bound_key(dep_key: KeyT) -> KeyT:
|
|
48
|
+
if dep_key in loc._metadata:
|
|
49
|
+
return dep_key
|
|
50
|
+
|
|
51
|
+
if isinstance(dep_key, str):
|
|
52
|
+
mapped = loc.find_key_by_name(dep_key)
|
|
53
|
+
if mapped is not None:
|
|
54
|
+
return mapped
|
|
55
|
+
|
|
56
|
+
if isinstance(dep_key, type):
|
|
57
|
+
for k, md in loc._metadata.items():
|
|
58
|
+
typ = md.provided_type or md.concrete_class
|
|
59
|
+
if isinstance(typ, type):
|
|
60
|
+
try:
|
|
61
|
+
if issubclass(typ, dep_key):
|
|
62
|
+
return k
|
|
63
|
+
except Exception:
|
|
64
|
+
continue
|
|
65
|
+
return dep_key
|
|
66
|
+
|
|
67
|
+
graph: Dict[KeyT, Tuple[KeyT, ...]] = {}
|
|
68
|
+
for key, md in list(loc._metadata.items()):
|
|
69
|
+
deps: List[KeyT] = []
|
|
70
|
+
for d in loc.dependency_keys_for_static(md):
|
|
71
|
+
mapped = _map_dep_to_bound_key(d)
|
|
72
|
+
deps.append(mapped)
|
|
73
|
+
graph[key] = tuple(deps)
|
|
74
|
+
return graph
|
|
84
75
|
|
|
85
76
|
class PicoContainer:
|
|
86
77
|
_container_id_var: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar("pico_container_id", default=None)
|
|
@@ -104,7 +95,6 @@ class PicoContainer:
|
|
|
104
95
|
import time as _t
|
|
105
96
|
self.context = PicoContainer._Ctx(container_id=self.container_id, profiles=profiles, created_at=_t.time())
|
|
106
97
|
PicoContainer._container_registry[self.container_id] = self
|
|
107
|
-
self._tracer = ResolutionTracer(self)
|
|
108
98
|
|
|
109
99
|
@staticmethod
|
|
110
100
|
def _generate_container_id() -> str:
|
|
@@ -153,6 +143,7 @@ class PicoContainer:
|
|
|
153
143
|
def _canonical_key(self, key: KeyT) -> KeyT:
|
|
154
144
|
if self._factory.has(key):
|
|
155
145
|
return key
|
|
146
|
+
|
|
156
147
|
if isinstance(key, type) and self._locator:
|
|
157
148
|
cands: List[Tuple[bool, Any]] = []
|
|
158
149
|
for k, md in self._locator._metadata.items():
|
|
@@ -167,16 +158,19 @@ class PicoContainer:
|
|
|
167
158
|
if cands:
|
|
168
159
|
prim = [k for is_p, k in cands if is_p]
|
|
169
160
|
return prim[0] if prim else cands[0][1]
|
|
161
|
+
|
|
170
162
|
if isinstance(key, str) and self._locator:
|
|
171
163
|
for k, md in self._locator._metadata.items():
|
|
172
164
|
if md.pico_name == key:
|
|
173
165
|
return k
|
|
166
|
+
|
|
174
167
|
return key
|
|
175
168
|
|
|
176
169
|
def _resolve_or_create_internal(self, key: KeyT) -> Tuple[Any, float, bool]:
|
|
177
170
|
key = self._canonical_key(key)
|
|
178
171
|
cache = self._cache_for(key)
|
|
179
172
|
cached = cache.get(key)
|
|
173
|
+
|
|
180
174
|
if cached is not None:
|
|
181
175
|
self.context.cache_hit_count += 1
|
|
182
176
|
for o in self._observers: o.on_cache_hit(key)
|
|
@@ -184,19 +178,9 @@ class PicoContainer:
|
|
|
184
178
|
|
|
185
179
|
import time as _tm
|
|
186
180
|
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
181
|
|
|
194
|
-
token_chain = _resolve_chain.set(tuple(chain + [key]))
|
|
195
182
|
token_container = self.activate()
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
requester = chain[-1] if chain else None
|
|
199
|
-
instance_or_awaitable = None
|
|
183
|
+
requester = None
|
|
200
184
|
|
|
201
185
|
try:
|
|
202
186
|
provider = self._factory.get(key, origin=requester)
|
|
@@ -211,10 +195,28 @@ class PicoContainer:
|
|
|
211
195
|
return instance_or_awaitable, took_ms, False
|
|
212
196
|
|
|
213
197
|
finally:
|
|
214
|
-
self._tracer.leave(token_tracer)
|
|
215
|
-
_resolve_chain.reset(token_chain)
|
|
216
198
|
self.deactivate(token_container)
|
|
217
199
|
|
|
200
|
+
def _run_configure_methods(self, instance: Any) -> Any:
|
|
201
|
+
if not _needs_async_configure(instance):
|
|
202
|
+
for m in _iter_configure_methods(instance):
|
|
203
|
+
configure_deps = analyze_callable_dependencies(m)
|
|
204
|
+
args = self._resolve_args(configure_deps)
|
|
205
|
+
res = m(**args)
|
|
206
|
+
if inspect.isawaitable(res):
|
|
207
|
+
LOGGER.warning(f"Async configure method {m} called during sync get. Awaitable ignored.")
|
|
208
|
+
return instance
|
|
209
|
+
|
|
210
|
+
async def runner():
|
|
211
|
+
for m in _iter_configure_methods(instance):
|
|
212
|
+
configure_deps = analyze_callable_dependencies(m)
|
|
213
|
+
args = self._resolve_args(configure_deps)
|
|
214
|
+
r = m(**args)
|
|
215
|
+
if inspect.isawaitable(r):
|
|
216
|
+
await r
|
|
217
|
+
return instance
|
|
218
|
+
return runner()
|
|
219
|
+
|
|
218
220
|
@overload
|
|
219
221
|
def get(self, key: type) -> Any: ...
|
|
220
222
|
@overload
|
|
@@ -230,6 +232,14 @@ class PicoContainer:
|
|
|
230
232
|
key_name = getattr(key, '__name__', str(key))
|
|
231
233
|
raise AsyncResolutionError(key)
|
|
232
234
|
|
|
235
|
+
md = self._locator._metadata.get(key) if self._locator else None
|
|
236
|
+
scope = (md.scope if md else "singleton")
|
|
237
|
+
if scope != "singleton":
|
|
238
|
+
instance_or_awaitable_configured = self._run_configure_methods(instance)
|
|
239
|
+
if inspect.isawaitable(instance_or_awaitable_configured):
|
|
240
|
+
raise AsyncResolutionError(key)
|
|
241
|
+
instance = instance_or_awaitable_configured
|
|
242
|
+
|
|
233
243
|
final_instance = self._maybe_wrap_with_aspects(key, instance)
|
|
234
244
|
cache = self._cache_for(key)
|
|
235
245
|
cache.put(key, final_instance)
|
|
@@ -248,6 +258,15 @@ class PicoContainer:
|
|
|
248
258
|
if inspect.isawaitable(instance_or_awaitable):
|
|
249
259
|
instance = await instance_or_awaitable
|
|
250
260
|
|
|
261
|
+
md = self._locator._metadata.get(key) if self._locator else None
|
|
262
|
+
scope = (md.scope if md else "singleton")
|
|
263
|
+
if scope != "singleton":
|
|
264
|
+
instance_or_awaitable_configured = self._run_configure_methods(instance)
|
|
265
|
+
if inspect.isawaitable(instance_or_awaitable_configured):
|
|
266
|
+
instance = await instance_or_awaitable_configured
|
|
267
|
+
else:
|
|
268
|
+
instance = instance_or_awaitable_configured
|
|
269
|
+
|
|
251
270
|
final_instance = self._maybe_wrap_with_aspects(key, instance)
|
|
252
271
|
cache = self._cache_for(key)
|
|
253
272
|
cache.put(key, final_instance)
|
|
@@ -256,24 +275,6 @@ class PicoContainer:
|
|
|
256
275
|
|
|
257
276
|
return final_instance
|
|
258
277
|
|
|
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
278
|
def _maybe_wrap_with_aspects(self, key, instance: Any) -> Any:
|
|
278
279
|
if isinstance(instance, UnifiedComponentProxy):
|
|
279
280
|
return instance
|
|
@@ -283,14 +284,9 @@ class PicoContainer:
|
|
|
283
284
|
return UnifiedComponentProxy(container=self, target=instance)
|
|
284
285
|
return instance
|
|
285
286
|
|
|
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)
|
|
287
|
+
def _iterate_cleanup_targets(self) -> Iterable[Any]:
|
|
288
|
+
yield from (obj for _, obj in self._caches.all_items())
|
|
289
|
+
|
|
294
290
|
if self._locator:
|
|
295
291
|
seen = set()
|
|
296
292
|
for md in self._locator._metadata.values():
|
|
@@ -298,12 +294,37 @@ class PicoContainer:
|
|
|
298
294
|
if fc and fc not in seen:
|
|
299
295
|
seen.add(fc)
|
|
300
296
|
inst = self.get(fc) if self._factory.has(fc) else fc()
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
297
|
+
yield inst
|
|
298
|
+
|
|
299
|
+
def _call_cleanup_method(self, method: Callable[..., Any]) -> Any:
|
|
300
|
+
deps_requests = analyze_callable_dependencies(method)
|
|
301
|
+
return method(**self._resolve_args(deps_requests))
|
|
302
|
+
|
|
303
|
+
def cleanup_all(self) -> None:
|
|
304
|
+
for obj in self._iterate_cleanup_targets():
|
|
305
|
+
for _, m in inspect.getmembers(obj, predicate=inspect.ismethod):
|
|
306
|
+
meta = getattr(m, PICO_META, {})
|
|
307
|
+
if meta.get("cleanup", False):
|
|
308
|
+
res = self._call_cleanup_method(m)
|
|
309
|
+
if inspect.isawaitable(res):
|
|
310
|
+
LOGGER.warning(f"Async cleanup method {m} called during sync shutdown. Awaitable ignored.")
|
|
311
|
+
|
|
312
|
+
async def cleanup_all_async(self) -> None:
|
|
313
|
+
for obj in self._iterate_cleanup_targets():
|
|
314
|
+
for _, m in inspect.getmembers(obj, predicate=inspect.ismethod):
|
|
315
|
+
meta = getattr(m, PICO_META, {})
|
|
316
|
+
if meta.get("cleanup", False):
|
|
317
|
+
res = self._call_cleanup_method(m)
|
|
318
|
+
if inspect.isawaitable(res):
|
|
319
|
+
await res
|
|
320
|
+
|
|
321
|
+
try:
|
|
322
|
+
from .event_bus import EventBus
|
|
323
|
+
for _, obj in self._caches.all_items():
|
|
324
|
+
if isinstance(obj, EventBus):
|
|
325
|
+
await obj.aclose()
|
|
326
|
+
except Exception:
|
|
327
|
+
pass
|
|
307
328
|
|
|
308
329
|
def activate_scope(self, name: str, scope_id: Any):
|
|
309
330
|
return self.scopes.activate(name, scope_id)
|
|
@@ -333,39 +354,6 @@ class PicoContainer:
|
|
|
333
354
|
out[f"{getattr(k,'__name__',k)}.{name}"] = False
|
|
334
355
|
return out
|
|
335
356
|
|
|
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
357
|
def stats(self) -> Dict[str, Any]:
|
|
370
358
|
import time as _t
|
|
371
359
|
resolves = self.context.resolve_count
|
|
@@ -385,6 +373,9 @@ class PicoContainer:
|
|
|
385
373
|
self.cleanup_all()
|
|
386
374
|
PicoContainer._container_registry.pop(self.container_id, None)
|
|
387
375
|
|
|
376
|
+
def build_resolution_graph(self) -> None:
|
|
377
|
+
return _build_resolution_graph(self._locator)
|
|
378
|
+
|
|
388
379
|
def export_graph(
|
|
389
380
|
self,
|
|
390
381
|
path: str,
|
|
@@ -398,10 +389,8 @@ class PicoContainer:
|
|
|
398
389
|
if not self._locator:
|
|
399
390
|
raise RuntimeError("No locator attached; cannot export dependency graph.")
|
|
400
391
|
|
|
401
|
-
from .api import _build_resolution_graph
|
|
402
|
-
|
|
403
392
|
md_by_key = self._locator._metadata
|
|
404
|
-
graph = _build_resolution_graph(self)
|
|
393
|
+
graph = _build_resolution_graph(self._locator)
|
|
405
394
|
|
|
406
395
|
lines: List[str] = []
|
|
407
396
|
lines.append("digraph Pico {")
|
|
@@ -441,3 +430,67 @@ class PicoContainer:
|
|
|
441
430
|
with open(path, "w", encoding="utf-8") as f:
|
|
442
431
|
f.write("\n".join(lines))
|
|
443
432
|
|
|
433
|
+
|
|
434
|
+
def _resolve_args(self, dependencies: Tuple[DependencyRequest, ...]) -> Dict[str, Any]:
|
|
435
|
+
kwargs: Dict[str, Any] = {}
|
|
436
|
+
if not dependencies:
|
|
437
|
+
return kwargs
|
|
438
|
+
|
|
439
|
+
for dep in dependencies:
|
|
440
|
+
if dep.is_list:
|
|
441
|
+
keys: Tuple[KeyT, ...] = ()
|
|
442
|
+
if self._locator is not None and isinstance(dep.key, type):
|
|
443
|
+
keys = tuple(self._locator.collect_by_type(dep.key, dep.qualifier))
|
|
444
|
+
kwargs[dep.parameter_name] = [self.get(k) for k in keys]
|
|
445
|
+
continue
|
|
446
|
+
|
|
447
|
+
primary_key = dep.key
|
|
448
|
+
if isinstance(primary_key, str) and self._locator is not None:
|
|
449
|
+
mapped = self._locator.find_key_by_name(primary_key)
|
|
450
|
+
primary_key = mapped if mapped is not None else primary_key
|
|
451
|
+
|
|
452
|
+
try:
|
|
453
|
+
kwargs[dep.parameter_name] = self.get(primary_key)
|
|
454
|
+
except Exception as first_error:
|
|
455
|
+
if primary_key != dep.parameter_name:
|
|
456
|
+
try:
|
|
457
|
+
kwargs[dep.parameter_name] = self.get(dep.parameter_name)
|
|
458
|
+
except Exception:
|
|
459
|
+
raise first_error from None
|
|
460
|
+
else:
|
|
461
|
+
raise first_error from None
|
|
462
|
+
return kwargs
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def build_class(self, cls: type, locator: ComponentLocator, dependencies: Tuple[DependencyRequest, ...]) -> Any:
|
|
466
|
+
init = cls.__init__
|
|
467
|
+
if init is object.__init__:
|
|
468
|
+
inst = cls()
|
|
469
|
+
else:
|
|
470
|
+
deps = self._resolve_args(dependencies)
|
|
471
|
+
inst = cls(**deps)
|
|
472
|
+
|
|
473
|
+
ainit = getattr(inst, "__ainit__", None)
|
|
474
|
+
has_async = (callable(ainit) and inspect.iscoroutinefunction(ainit))
|
|
475
|
+
|
|
476
|
+
if has_async:
|
|
477
|
+
async def runner():
|
|
478
|
+
if callable(ainit):
|
|
479
|
+
kwargs = {}
|
|
480
|
+
try:
|
|
481
|
+
ainit_deps = analyze_callable_dependencies(ainit)
|
|
482
|
+
kwargs = self._resolve_args(ainit_deps)
|
|
483
|
+
except Exception:
|
|
484
|
+
kwargs = {}
|
|
485
|
+
res = ainit(**kwargs)
|
|
486
|
+
if inspect.isawaitable(res):
|
|
487
|
+
await res
|
|
488
|
+
return inst
|
|
489
|
+
return runner()
|
|
490
|
+
|
|
491
|
+
return inst
|
|
492
|
+
|
|
493
|
+
def build_method(self, fn: Callable[..., Any], locator: ComponentLocator, dependencies: Tuple[DependencyRequest, ...]) -> Any:
|
|
494
|
+
deps = self._resolve_args(dependencies)
|
|
495
|
+
obj = fn(**deps)
|
|
496
|
+
return obj
|
pico_ioc/decorators.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
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 = "self", *, prefix: str = "", mapping: str = "auto", **kwargs):
|
|
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
|
+
_apply_common_metadata(cls, **kwargs)
|
|
183
|
+
return cls
|
|
184
|
+
return dec
|
|
185
|
+
|
|
186
|
+
def get_return_type(fn: Callable[..., Any]) -> Optional[type]:
|
|
187
|
+
try:
|
|
188
|
+
ra = inspect.signature(fn).return_annotation
|
|
189
|
+
except Exception:
|
|
190
|
+
return None
|
|
191
|
+
if ra is inspect._empty:
|
|
192
|
+
return None
|
|
193
|
+
return ra if isinstance(ra, type) else None
|