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