pico-ioc 1.4.0__py3-none-any.whl → 2.0.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/api.py CHANGED
@@ -1,221 +1,805 @@
1
- from __future__ import annotations
2
-
3
- import inspect as _inspect
1
+ # src/pico_ioc/api.py
2
+ import os
3
+ import json
4
+ import inspect
5
+ import functools
4
6
  import importlib
7
+ import pkgutil
5
8
  import logging
6
- from types import ModuleType
7
- from typing import Callable, Optional, Tuple, Any, Dict, Iterable, Sequence
8
-
9
+ from dataclasses import is_dataclass, fields, dataclass, MISSING
10
+ from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union, get_args, get_origin, Annotated, Protocol
11
+ from .constants import LOGGER, PICO_INFRA, PICO_NAME, PICO_KEY, PICO_META
12
+ from .exceptions import (
13
+ ProviderNotFoundError,
14
+ CircularDependencyError,
15
+ ComponentCreationError,
16
+ ScopeError,
17
+ ConfigurationError,
18
+ SerializationError,
19
+ InvalidBindingError,
20
+ )
21
+ from .factory import ComponentFactory, ProviderMetadata, DeferredProvider
22
+ from .locator import ComponentLocator
23
+ from .scope import ScopeManager, ScopedCaches
9
24
  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
15
-
16
-
17
- def reset() -> None:
18
- _state.set_context(None)
19
-
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)
25
+ from .aop import UnifiedComponentProxy
26
+
27
+ KeyT = Union[str, type]
28
+ Provider = Callable[[], Any]
29
+
30
+ class ConfigSource(Protocol):
31
+ def get(self, key: str) -> Optional[str]: ...
32
+
33
+ class EnvSource:
34
+ def __init__(self, prefix: str = "") -> None:
35
+ self.prefix = prefix
36
+ def get(self, key: str) -> Optional[str]:
37
+ return os.environ.get(self.prefix + key)
38
+
39
+ class FileSource:
40
+ def __init__(self, path: str, prefix: str = "") -> None:
41
+ self.prefix = prefix
42
+ try:
43
+ with open(path, "r", encoding="utf-8") as f:
44
+ self._data = json.load(f)
45
+ except Exception:
46
+ self._data = {}
47
+ def get(self, key: str) -> Optional[str]:
48
+ k = self.prefix + key
49
+ v = self._data
50
+ for part in k.split("__"):
51
+ if isinstance(v, dict) and part in v:
52
+ v = v[part]
53
+ else:
54
+ return None
55
+ if isinstance(v, (str, int, float, bool)):
56
+ return str(v)
57
+ return None
58
+
59
+ def _meta_get(obj: Any) -> Dict[str, Any]:
60
+ m = getattr(obj, PICO_META, None)
61
+ if m is None:
62
+ m = {}
63
+ setattr(obj, PICO_META, m)
64
+ return m
65
+
66
+ def component(cls=None, *, name: Any = None):
67
+ def dec(c):
68
+ setattr(c, PICO_INFRA, "component")
69
+ setattr(c, PICO_NAME, name if name is not None else getattr(c, "__name__", str(c)))
70
+ setattr(c, PICO_KEY, name if name is not None else c)
71
+ _meta_get(c)
72
+ return c
73
+ return dec(cls) if cls else dec
74
+
75
+ def factory(cls):
76
+ setattr(cls, PICO_INFRA, "factory")
77
+ setattr(cls, PICO_NAME, getattr(cls, "__name__", str(cls)))
78
+ _meta_get(cls)
79
+ return cls
80
+
81
+ def provides(key: Any):
82
+ def dec(fn):
83
+ @functools.wraps(fn)
84
+ def w(*a, **k):
85
+ return fn(*a, **k)
86
+ setattr(w, PICO_INFRA, "provides")
87
+ setattr(w, PICO_NAME, key)
88
+ setattr(w, PICO_KEY, key)
89
+ _meta_get(w)
90
+ return w
91
+ return dec
92
+
93
+ class Qualifier(str):
94
+ __slots__ = ()
95
+
96
+ def qualifier(*qs: Qualifier):
97
+ def dec(cls):
98
+ m = _meta_get(cls)
99
+ cur = tuple(m.get("qualifier", ()))
100
+ seen = set(cur)
101
+ merged = list(cur)
102
+ for q in qs:
103
+ if q not in seen:
104
+ merged.append(q)
105
+ seen.add(q)
106
+ m["qualifier"] = tuple(merged)
107
+ return cls
108
+ return dec
109
+
110
+ def on_missing(selector: object, *, priority: int = 0):
111
+ def dec(obj):
112
+ m = _meta_get(obj)
113
+ m["on_missing"] = {"selector": selector, "priority": int(priority)}
114
+ return obj
115
+ return dec
116
+
117
+ def primary(obj):
118
+ m = _meta_get(obj)
119
+ m["primary"] = True
120
+ return obj
121
+
122
+ def conditional(*, profiles: Tuple[str, ...] = (), require_env: Tuple[str, ...] = (), predicate: Optional[Callable[[], bool]] = None):
123
+ def dec(obj):
124
+ m = _meta_get(obj)
125
+ m["conditional"] = {"profiles": tuple(profiles), "require_env": tuple(require_env), "predicate": predicate}
126
+ return obj
127
+ return dec
128
+
129
+ def lazy(obj):
130
+ m = _meta_get(obj)
131
+ m["lazy"] = True
132
+ return obj
133
+
134
+ def configuration(cls=None, *, prefix: Optional[str] = None):
135
+ def dec(c):
136
+ setattr(c, PICO_INFRA, "configuration")
137
+ m = _meta_get(c)
138
+ if prefix is not None:
139
+ m["config_prefix"] = prefix
140
+ return c
141
+ return dec(cls) if cls else dec
142
+
143
+ def configure(fn):
144
+ m = _meta_get(fn)
145
+ m["configure"] = True
146
+ return fn
147
+
148
+ def cleanup(fn):
149
+ m = _meta_get(fn)
150
+ m["cleanup"] = True
151
+ return fn
152
+
153
+ def scope(name: str):
154
+ def dec(obj):
155
+ m = _meta_get(obj)
156
+ m["scope"] = name
157
+ return obj
158
+ return dec
159
+
160
+ def configured(target: Any, *, prefix: Optional[str] = None):
161
+ def dec(cls):
162
+ setattr(cls, PICO_INFRA, "configured")
163
+ m = _meta_get(cls)
164
+ m["configured"] = {"target": target, "prefix": prefix}
165
+ return cls
166
+ return dec
167
+
168
+ def _truthy(s: str) -> bool:
169
+ return s.strip().lower() in {"1", "true", "yes", "on", "y", "t"}
170
+
171
+ def _coerce(val: Optional[str], t: type) -> Any:
172
+ if val is None:
173
+ return None
174
+ if t is str:
175
+ return val
176
+ if t is int:
177
+ return int(val)
178
+ if t is float:
179
+ return float(val)
180
+ if t is bool:
181
+ return _truthy(val)
182
+ org = get_origin(t)
183
+ if org is Union:
184
+ args = [a for a in get_args(t) if a is not type(None)]
185
+ if not args:
186
+ return None
187
+ return _coerce(val, args[0])
188
+ return val
189
+
190
+ def _upper_key(name: str) -> str:
191
+ return name.upper()
192
+
193
+ def _lookup(sources: Tuple[ConfigSource, ...], key: str) -> Optional[str]:
194
+ for src in sources:
195
+ v = src.get(key)
196
+ if v is not None:
197
+ return v
198
+ return None
25
199
 
26
- # -------- fingerprint helpers --------
27
- def _callable_id(cb) -> tuple:
200
+ def _build_settings_instance(cls: type, sources: Tuple[ConfigSource, ...], prefix: Optional[str]) -> Any:
201
+ if not is_dataclass(cls):
202
+ raise ConfigurationError(f"Configuration class {getattr(cls, '__name__', str(cls))} must be a dataclass")
203
+ values: Dict[str, Any] = {}
204
+ for f in fields(cls):
205
+ base_key = _upper_key(f.name)
206
+ keys_to_try = []
207
+ if prefix:
208
+ keys_to_try.append(prefix + base_key)
209
+ keys_to_try.append(base_key)
210
+ raw = None
211
+ for k in keys_to_try:
212
+ raw = _lookup(sources, k)
213
+ if raw is not None:
214
+ break
215
+ if raw is None:
216
+ if f.default is not MISSING or f.default_factory is not MISSING:
217
+ continue
218
+ raise ConfigurationError(f"Missing configuration key: {(prefix or '') + base_key}")
219
+ values[f.name] = _coerce(raw, f.type if isinstance(f.type, type) or get_origin(f.type) else str)
220
+ return cls(**values)
221
+
222
+ def _extract_list_req(ann: Any):
223
+ def read_qualifier(metas: Iterable[Any]):
224
+ for m in metas:
225
+ if isinstance(m, Qualifier):
226
+ return str(m)
227
+ return None
228
+ origin = get_origin(ann)
229
+ if origin is Annotated:
230
+ args = get_args(ann)
231
+ base = args[0] if args else Any
232
+ metas = args[1:] if len(args) > 1 else ()
233
+ is_list, elem_t, qual = _extract_list_req(base)
234
+ if qual is None:
235
+ qual = read_qualifier(metas)
236
+ return is_list, elem_t, qual
237
+ if origin in (list, List):
238
+ elem = get_args(ann)[0] if get_args(ann) else Any
239
+ if get_origin(elem) is Annotated:
240
+ eargs = get_args(elem)
241
+ ebase = eargs[0] if eargs else Any
242
+ emetas = eargs[1:] if len(eargs) > 1 else ()
243
+ qual = read_qualifier(emetas)
244
+ return True, ebase if isinstance(ebase, type) else Any, qual
245
+ return True, elem if isinstance(elem, type) else Any, None
246
+ return False, None, None
247
+
248
+ def _implements_protocol(typ: type, proto: type) -> bool:
249
+ if not getattr(proto, "_is_protocol", False):
250
+ return False
28
251
  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)
252
+ if getattr(proto, "__runtime_protocol__", False) or getattr(proto, "__annotations__", None) is not None:
253
+ inst = object.__new__(typ)
254
+ return isinstance(inst, proto)
34
255
  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 ()
57
- 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))
256
+ pass
257
+ for name, val in proto.__dict__.items():
258
+ if name.startswith("_") or not callable(val):
73
259
  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:
260
+ return True
261
+
262
+ def _collect_by_type(locator: ComponentLocator, t: type, q: Optional[str]):
263
+ keys = list(locator._metadata.keys())
264
+ out: List[KeyT] = []
265
+ for k in keys:
266
+ md = locator._metadata.get(k)
267
+ if md is None:
268
+ continue
269
+ typ = md.provided_type or md.concrete_class
270
+ if not isinstance(typ, type):
271
+ continue
272
+ ok = False
273
+ try:
274
+ ok = issubclass(typ, t)
275
+ except Exception:
276
+ ok = _implements_protocol(typ, t)
277
+ if ok and (q is None or q in md.qualifiers):
278
+ out.append(k)
279
+ return out
280
+
281
+ def _resolve_args(callable_obj: Callable[..., Any], pico: "PicoContainer") -> Dict[str, Any]:
282
+ sig = inspect.signature(callable_obj)
283
+ kwargs: Dict[str, Any] = {}
284
+ for name, param in sig.parameters.items():
285
+ if name in ("self", "cls"):
286
+ continue
287
+ if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
288
+ continue
289
+ ann = param.annotation
290
+ is_list, elem_t, qual = _extract_list_req(ann)
291
+ if is_list and pico._locator is not None and isinstance(elem_t, type):
292
+ keys = _collect_by_type(pico._locator, elem_t, qual)
293
+ kwargs[name] = [pico.get(k) for k in keys]
294
+ continue
295
+ if ann is not inspect._empty and isinstance(ann, type):
296
+ key: KeyT = ann
297
+ elif ann is not inspect._empty and isinstance(ann, str):
298
+ key = ann
299
+ else:
300
+ key = name
301
+ kwargs[name] = pico.get(key)
302
+ return kwargs
303
+
304
+ def _needs_async_configure(obj: Any) -> bool:
305
+ for _, m in inspect.getmembers(obj, predicate=inspect.ismethod):
306
+ meta = getattr(m, PICO_META, {})
307
+ if meta.get("configure", False) and inspect.iscoroutinefunction(m):
308
+ return True
309
+ return False
310
+
311
+ def _iter_configure_methods(obj: Any):
312
+ for _, m in inspect.getmembers(obj, predicate=inspect.ismethod):
313
+ meta = getattr(m, PICO_META, {})
314
+ if meta.get("configure", False):
315
+ yield m
316
+
317
+ def _build_class(cls: type, pico: "PicoContainer", locator: ComponentLocator) -> Any:
318
+ init = cls.__init__
319
+ if init is object.__init__:
320
+ inst = cls()
321
+ else:
322
+ deps = _resolve_args(init, pico)
323
+ inst = cls(**deps)
324
+ ainit = getattr(inst, "__ainit__", None)
325
+ has_async = (callable(ainit) and inspect.iscoroutinefunction(ainit)) or _needs_async_configure(inst)
326
+ if has_async:
327
+ async def runner():
328
+ if callable(ainit):
329
+ kwargs = {}
87
330
  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__))
331
+ kwargs = _resolve_args(ainit, pico)
94
332
  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
108
-
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]:
333
+ kwargs = {}
334
+ res = ainit(**kwargs)
335
+ if inspect.isawaitable(res):
336
+ await res
337
+ for m in _iter_configure_methods(inst):
338
+ args = _resolve_args(m, pico)
339
+ r = m(**args)
340
+ if inspect.isawaitable(r):
341
+ await r
342
+ return inst
343
+ return runner()
344
+ for m in _iter_configure_methods(inst):
345
+ args = _resolve_args(m, pico)
346
+ m(**args)
347
+ return inst
348
+
349
+ def _build_method(fn: Callable[..., Any], pico: "PicoContainer", locator: ComponentLocator) -> Any:
350
+ deps = _resolve_args(fn, pico)
351
+ obj = fn(**deps)
352
+ has_async = _needs_async_configure(obj)
353
+ if has_async:
354
+ async def runner():
355
+ for m in _iter_configure_methods(obj):
356
+ args = _resolve_args(m, pico)
357
+ r = m(**args)
358
+ if inspect.isawaitable(r):
359
+ await r
360
+ return obj
361
+ return runner()
362
+ for m in _iter_configure_methods(obj):
363
+ args = _resolve_args(m, pico)
364
+ m(**args)
365
+ return obj
366
+
367
+ def _get_return_type(fn: Callable[..., Any]) -> Optional[type]:
122
368
  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)
369
+ ra = inspect.signature(fn).return_annotation
128
370
  except Exception:
129
- pass
130
- return None
131
-
132
- # ---------------- public API ----------------
133
- def init(
134
- root_package, *, profiles: Optional[list[str]] = None, exclude: Optional[Callable[[str], bool]] = None,
135
- auto_exclude_caller: bool = True, plugins: Tuple[PicoPlugin, ...] = (), reuse: bool = True,
136
- overrides: Optional[Dict[Any, Any]] = None, auto_scan: Sequence[str] = (),
137
- auto_scan_exclude: Optional[Callable[[str], bool]] = None, strict_autoscan: bool = False,
138
- config: Sequence[ConfigSource] = (),
139
- ) -> PicoContainer:
140
- root_name = root_package if isinstance(root_package, str) else getattr(root_package, "__name__", None)
141
- fp = _make_fingerprint_from_signature(locals())
142
-
143
- if reuse:
144
- reused = _maybe_reuse_existing(fp, overrides)
145
- if reused is not None:
146
- return reused
147
-
148
- builder = (PicoContainerBuilder()
149
- .with_plugins(plugins)
150
- .with_profiles(profiles)
151
- .with_overrides(overrides)
152
- .with_config(ConfigRegistry(config or ())))
153
-
154
- combined_exclude = _build_exclude(exclude, auto_exclude_caller, root_name=root_name)
155
- builder.add_scan_package(root_package, exclude=combined_exclude)
156
-
157
- if auto_scan:
158
- for pkg in auto_scan:
371
+ return None
372
+ if ra is inspect._empty:
373
+ return None
374
+ return ra if isinstance(ra, type) else None
375
+
376
+ def _scan_package(package) -> Iterable[Any]:
377
+ for _, name, _ in pkgutil.walk_packages(package.__path__, package.__name__ + "."):
378
+ yield importlib.import_module(name)
379
+
380
+ def _iter_input_modules(inputs: Union[Any, Iterable[Any]]) -> Iterable[Any]:
381
+ seq = inputs if isinstance(inputs, Iterable) and not inspect.ismodule(inputs) and not isinstance(inputs, str) else [inputs]
382
+ seen: Set[str] = set()
383
+ for it in seq:
384
+ if isinstance(it, str):
385
+ mod = importlib.import_module(it)
386
+ else:
387
+ mod = it
388
+ if hasattr(mod, "__path__"):
389
+ for sub in _scan_package(mod):
390
+ name = getattr(sub, "__name__", None)
391
+ if name and name not in seen:
392
+ seen.add(name)
393
+ yield sub
394
+ else:
395
+ name = getattr(mod, "__name__", None)
396
+ if name and name not in seen:
397
+ seen.add(name)
398
+ yield mod
399
+
400
+ def _can_be_selected_for(reg_md: Dict[KeyT, ProviderMetadata], selector: Any) -> bool:
401
+ if not isinstance(selector, type):
402
+ return False
403
+ for md in reg_md.values():
404
+ typ = md.provided_type or md.concrete_class
405
+ if isinstance(typ, type):
159
406
  try:
160
- mod = importlib.import_module(pkg)
161
- scan_exclude = _combine_excludes(exclude, auto_scan_exclude)
162
- builder.add_scan_package(mod, exclude=scan_exclude)
163
- except ImportError as e:
164
- msg = f"pico-ioc: auto_scan package not found: {pkg}"
165
- if strict_autoscan:
166
- logging.error(msg)
167
- raise e
168
- logging.warning(msg)
169
-
170
- container = builder.build()
171
-
172
- # Activate new context atomically
173
- new_ctx = _state.ContainerContext(container=container, fingerprint=fp, root_name=root_name)
174
- _state.set_context(new_ctx)
175
- return container
176
-
177
- def scope(
178
- *, modules: Iterable[Any] = (), roots: Iterable[type] = (), profiles: Optional[list[str]] = None,
179
- overrides: Optional[Dict[Any, Any]] = None, base: Optional[PicoContainer] = None,
180
- include_tags: Optional[set[str]] = None, exclude_tags: Optional[set[str]] = None,
181
- strict: bool = True, lazy: bool = True,
182
- ) -> PicoContainer:
183
- builder = PicoContainerBuilder()
184
-
185
- if base is not None and not strict:
186
- base_providers = getattr(base, "_providers", {})
187
- builder._providers.update(base_providers)
188
- if profiles is None:
189
- builder.with_profiles(list(getattr(base, "_active_profiles", ())))
190
-
191
- builder.with_profiles(profiles)\
192
- .with_overrides(overrides)\
193
- .with_tag_filters(include=include_tags, exclude=exclude_tags)\
194
- .with_roots(roots)
195
-
196
- for m in modules:
197
- builder.add_scan_package(m)
198
-
199
- built_container = builder.with_eager(not lazy).build()
200
-
201
- scoped_container = ScopedContainer(base=base, strict=strict, built_container=built_container)
202
-
203
- if not lazy:
204
- from .proxy import ComponentProxy
205
- for rk in roots or ():
407
+ if issubclass(typ, selector):
408
+ return True
409
+ except Exception:
410
+ continue
411
+ return False
412
+
413
+ def _normalize_override_provider(v: Any) -> Tuple[Provider, bool]:
414
+ if isinstance(v, tuple) and len(v) == 2:
415
+ src, lz = v
416
+ if callable(src):
417
+ return (lambda s=src: s()), bool(lz)
418
+ return (lambda s=src: s), bool(lz)
419
+ if callable(v):
420
+ return (lambda f=v: f()), False
421
+ return (lambda inst=v: inst), False
422
+
423
+ class Registrar:
424
+ def __init__(
425
+ self,
426
+ factory: ComponentFactory,
427
+ *,
428
+ profiles: Tuple[str, ...] = (),
429
+ environ: Optional[Dict[str, str]] = None,
430
+ logger: Optional[logging.Logger] = None,
431
+ config: Tuple[ConfigSource, ...] = (),
432
+ tree_sources: Tuple["TreeSource", ...] = ()
433
+ ) -> None:
434
+ self._factory = factory
435
+ self._profiles = set(p.strip() for p in profiles if p)
436
+ self._environ = environ if environ is not None else os.environ
437
+ self._deferred: List[DeferredProvider] = []
438
+ self._candidates: Dict[KeyT, List[Tuple[bool, Provider, ProviderMetadata]]] = {}
439
+ self._metadata: Dict[KeyT, ProviderMetadata] = {}
440
+ self._indexes: Dict[str, Dict[Any, List[KeyT]]] = {}
441
+ self._on_missing: List[Tuple[int, KeyT, type]] = []
442
+ self._log = logger or LOGGER
443
+ self._config_sources: Tuple[ConfigSource, ...] = tuple(config)
444
+ from .config_runtime import ConfigResolver, TypeAdapterRegistry, ObjectGraphBuilder
445
+ self._resolver = ConfigResolver(tuple(tree_sources))
446
+ self._adapters = TypeAdapterRegistry()
447
+ self._graph = ObjectGraphBuilder(self._resolver, self._adapters)
448
+
449
+ def locator(self) -> ComponentLocator:
450
+ return ComponentLocator(self._metadata, self._indexes)
451
+
452
+ def attach_runtime(self, pico, locator: ComponentLocator) -> None:
453
+ for deferred in self._deferred:
454
+ deferred.attach(pico, locator)
455
+ for key, md in list(self._metadata.items()):
456
+ if md.lazy:
457
+ original = self._factory.get(key)
458
+ def lazy_proxy_provider(_orig=original, _p=pico):
459
+ return UnifiedComponentProxy(container=_p, object_creator=_orig)
460
+ self._factory.bind(key, lazy_proxy_provider)
461
+
462
+ def _queue(self, key: KeyT, provider: Provider, md: ProviderMetadata) -> None:
463
+ lst = self._candidates.setdefault(key, [])
464
+ lst.append((md.primary, provider, md))
465
+ if isinstance(provider, DeferredProvider):
466
+ self._deferred.append(provider)
467
+
468
+ def _bind_if_absent(self, key: KeyT, provider: Provider) -> None:
469
+ if not self._factory.has(key):
470
+ self._factory.bind(key, provider)
471
+
472
+ def _enabled_by_condition(self, obj: Any) -> bool:
473
+ meta = getattr(obj, PICO_META, {})
474
+ c = meta.get("conditional", None)
475
+ if not c:
476
+ return True
477
+ p = set(c.get("profiles") or ())
478
+ if p and not (p & self._profiles):
479
+ self._log.info("excluded_by_profile name=%s need=%s active=%s", getattr(obj, "__name__", str(obj)), sorted(p), sorted(self._profiles))
480
+ return False
481
+ req = c.get("require_env") or ()
482
+ for k in req:
483
+ if k not in self._environ or not self._environ.get(k):
484
+ self._log.info("excluded_by_env name=%s env=%s", getattr(obj, "__name__", str(obj)), k)
485
+ return False
486
+ pred = c.get("predicate")
487
+ if pred is None:
488
+ return True
489
+ try:
490
+ ok = bool(pred())
491
+ except Exception as e:
492
+ self._log.info("excluded_by_predicate_error name=%s error=%s", getattr(obj, "__name__", str(obj)), repr(e))
493
+ return False
494
+ if not ok:
495
+ self._log.info("excluded_by_predicate name=%s", getattr(obj, "__name__", str(obj)))
496
+ return ok
497
+
498
+ def _register_component_class(self, cls: type) -> None:
499
+ if not self._enabled_by_condition(cls):
500
+ return
501
+ key = getattr(cls, PICO_KEY, cls)
502
+ provider = DeferredProvider(lambda pico, loc, c=cls: _build_class(c, pico, loc))
503
+ qset = set(str(q) for q in getattr(cls, PICO_META, {}).get("qualifier", ()))
504
+ sc = getattr(cls, PICO_META, {}).get("scope", "singleton")
505
+ 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)
506
+ self._queue(key, provider, md)
507
+
508
+ def _register_factory_class(self, cls: type) -> None:
509
+ if not self._enabled_by_condition(cls):
510
+ return
511
+ for name in dir(cls):
206
512
  try:
207
- obj = scoped_container.get(rk)
208
- if isinstance(obj, ComponentProxy):
209
- _ = obj._get_real_object()
210
- except NameError:
211
- if strict: raise
212
-
213
- logging.info("Scope container ready.")
214
- return scoped_container
215
-
216
-
217
-
218
- def container_fingerprint() -> Optional[tuple]:
219
- ctx = _state.get_context()
220
- return ctx.fingerprint if ctx else None
513
+ real = getattr(cls, name)
514
+ except Exception:
515
+ continue
516
+ if callable(real) and getattr(real, PICO_INFRA, None) == "provides":
517
+ if not self._enabled_by_condition(real):
518
+ continue
519
+ k = getattr(real, PICO_KEY)
520
+ provider = DeferredProvider(lambda pico, loc, fc=cls, mn=name: _build_method(getattr(_build_class(fc, pico, loc), mn), pico, loc))
521
+ rt = _get_return_type(real)
522
+ qset = set(str(q) for q in getattr(real, PICO_META, {}).get("qualifier", ()))
523
+ sc = getattr(real, PICO_META, {}).get("scope", getattr(cls, PICO_META, {}).get("scope", "singleton"))
524
+ md = ProviderMetadata(key=k, provided_type=rt if isinstance(rt, type) else (k if isinstance(k, type) else None), concrete_class=None, factory_class=cls, factory_method=name, qualifiers=qset, primary=bool(getattr(real, PICO_META, {}).get("primary")), lazy=bool(getattr(real, PICO_META, {}).get("lazy", False)), infra=getattr(cls, PICO_INFRA, None), pico_name=getattr(real, PICO_NAME, None), scope=sc)
525
+ self._queue(k, provider, md)
526
+
527
+ def _register_configuration_class(self, cls: type) -> None:
528
+ if not self._enabled_by_condition(cls):
529
+ return
530
+ pref = getattr(cls, PICO_META, {}).get("config_prefix", None)
531
+ if is_dataclass(cls):
532
+ key = cls
533
+ provider = DeferredProvider(lambda pico, loc, c=cls, p=pref, src=self._config_sources: _build_settings_instance(c, src, p))
534
+ 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")
535
+ self._queue(key, provider, md)
536
+
537
+ def _register_configured_class(self, cls: type) -> None:
538
+ if not self._enabled_by_condition(cls):
539
+ return
540
+ meta = getattr(cls, PICO_META, {})
541
+ cfg = meta.get("configured", None)
542
+ if not cfg:
543
+ return
544
+ target = cfg.get("target")
545
+ prefix = cfg.get("prefix")
546
+ if not isinstance(target, type):
547
+ return
548
+ provider = DeferredProvider(lambda pico, loc, t=target, p=prefix, g=self._graph: g.build_from_prefix(t, p))
549
+ qset = set(str(q) for q in meta.get("qualifier", ()))
550
+ sc = meta.get("scope", "singleton")
551
+ 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)
552
+ self._queue(target, provider, md)
553
+
554
+ def register_module(self, module: Any) -> None:
555
+ for _, obj in inspect.getmembers(module):
556
+ if inspect.isclass(obj):
557
+ meta = getattr(obj, PICO_META, {})
558
+ if "on_missing" in meta:
559
+ sel = meta["on_missing"]["selector"]
560
+ pr = int(meta["on_missing"].get("priority", 0))
561
+ self._on_missing.append((pr, sel, obj))
562
+ continue
563
+ infra = getattr(obj, PICO_INFRA, None)
564
+ if infra == "component":
565
+ self._register_component_class(obj)
566
+ elif infra == "factory":
567
+ self._register_factory_class(obj)
568
+ elif infra == "configuration":
569
+ self._register_configuration_class(obj)
570
+ elif infra == "configured":
571
+ self._register_configured_class(obj)
572
+
573
+ def _prefix_exists(self, md: ProviderMetadata) -> bool:
574
+ if md.infra != "configured":
575
+ return False
576
+ try:
577
+ _ = self._resolver.subtree(md.pico_name)
578
+ return True
579
+ except Exception:
580
+ return False
581
+
582
+ def select_and_bind(self) -> None:
583
+ for key, lst in self._candidates.items():
584
+ def rank(item: Tuple[bool, Provider, ProviderMetadata]) -> Tuple[int, int, int]:
585
+ is_present = 1 if self._prefix_exists(item[2]) else 0
586
+ pref = str(item[2].pico_name or "")
587
+ pref_len = len(pref)
588
+ is_primary = 1 if item[0] else 0
589
+ return (is_present, pref_len, is_primary)
590
+ lst_sorted = sorted(lst, key=rank, reverse=True)
591
+ chosen = lst_sorted[0]
592
+ self._bind_if_absent(key, chosen[1])
593
+ self._metadata[key] = chosen[2]
594
+
595
+ def _find_md_for_type(self, t: type) -> Optional[ProviderMetadata]:
596
+ cands: List[ProviderMetadata] = []
597
+ for md in self._metadata.values():
598
+ typ = md.provided_type or md.concrete_class
599
+ if not isinstance(typ, type):
600
+ continue
601
+ try:
602
+ if issubclass(typ, t):
603
+ cands.append(md)
604
+ except Exception:
605
+ continue
606
+ if not cands:
607
+ return None
608
+ prim = [m for m in cands if m.primary]
609
+ return prim[0] if prim else cands[0]
610
+
611
+ def _iter_param_types(self, callable_obj: Callable[..., Any]) -> Iterable[type]:
612
+ sig = inspect.signature(callable_obj)
613
+ for name, param in sig.parameters.items():
614
+ if name in ("self", "cls"):
615
+ continue
616
+ if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
617
+ continue
618
+ ann = param.annotation
619
+ is_list, elem_t, _ = _extract_list_req(ann)
620
+ t = elem_t if is_list else (ann if isinstance(ann, type) else None)
621
+ if isinstance(t, type):
622
+ yield t
623
+
624
+ def _infer_narrower_scope(self, md: ProviderMetadata) -> Optional[str]:
625
+ if md.concrete_class is not None:
626
+ init = md.concrete_class.__init__
627
+ for t in self._iter_param_types(init):
628
+ dep = self._find_md_for_type(t)
629
+ if dep and dep.scope != "singleton":
630
+ return dep.scope
631
+ if md.factory_class is not None and md.factory_method is not None:
632
+ fn = getattr(md.factory_class, md.factory_method)
633
+ for t in self._iter_param_types(fn):
634
+ dep = self._find_md_for_type(t)
635
+ if dep and dep.scope != "singleton":
636
+ return dep.scope
637
+ return None
638
+
639
+ def _promote_scopes(self) -> None:
640
+ for k, md in list(self._metadata.items()):
641
+ if md.scope == "singleton":
642
+ ns = self._infer_narrower_scope(md)
643
+ if ns and ns != "singleton":
644
+ self._metadata[k] = ProviderMetadata(
645
+ key=md.key,
646
+ provided_type=md.provided_type,
647
+ concrete_class=md.concrete_class,
648
+ factory_class=md.factory_class,
649
+ factory_method=md.factory_method,
650
+ qualifiers=md.qualifiers,
651
+ primary=md.primary,
652
+ lazy=md.lazy,
653
+ infra=md.infra,
654
+ pico_name=md.pico_name,
655
+ override=md.override,
656
+ scope=ns
657
+ )
658
+
659
+ def _rebuild_indexes(self) -> None:
660
+ self._indexes.clear()
661
+ def add(idx: str, val: Any, key: KeyT):
662
+ b = self._indexes.setdefault(idx, {}).setdefault(val, [])
663
+ if key not in b:
664
+ b.append(key)
665
+ for k, md in self._metadata.items():
666
+ for q in md.qualifiers:
667
+ add("qualifier", q, k)
668
+ if md.primary:
669
+ add("primary", True, k)
670
+ add("lazy", bool(md.lazy), k)
671
+ if md.infra is not None:
672
+ add("infra", md.infra, k)
673
+ if md.pico_name is not None:
674
+ add("pico_name", md.pico_name, k)
675
+
676
+ def _find_md_for_name(self, name: str) -> Optional[KeyT]:
677
+ for k, md in self._metadata.items():
678
+ if md.pico_name == name:
679
+ return k
680
+ t = md.provided_type or md.concrete_class
681
+ if isinstance(t, type) and getattr(t, "__name__", "") == name:
682
+ return k
683
+ return None
684
+
685
+ def _validate_bindings(self) -> None:
686
+ errors: List[str] = []
687
+ def _skip_type(t: type) -> bool:
688
+ if t in (str, int, float, bool, bytes):
689
+ return True
690
+ if t is Any:
691
+ return True
692
+ if getattr(t, "_is_protocol", False):
693
+ return True
694
+ return False
695
+ def _should_validate(param: inspect.Parameter) -> bool:
696
+ if param.default is not inspect._empty:
697
+ return False
698
+ ann = param.annotation
699
+ origin = get_origin(ann)
700
+ if origin is Union:
701
+ args = get_args(ann)
702
+ if type(None) in args:
703
+ return False
704
+ return True
705
+ loc = ComponentLocator(self._metadata, self._indexes)
706
+ for k, md in self._metadata.items():
707
+ if md.infra == "configuration":
708
+ continue
709
+ callables_to_check: List[Callable[..., Any]] = []
710
+ if md.concrete_class is not None:
711
+ callables_to_check.append(md.concrete_class.__init__)
712
+ if md.factory_class is not None and md.factory_method is not None:
713
+ fn = getattr(md.factory_class, md.factory_method)
714
+ callables_to_check.append(fn)
715
+ for callable_obj in callables_to_check:
716
+ sig = inspect.signature(callable_obj)
717
+ for name, param in sig.parameters.items():
718
+ if name in ("self", "cls"):
719
+ continue
720
+ if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
721
+ continue
722
+ if not _should_validate(param):
723
+ continue
724
+ ann = param.annotation
725
+ is_list, elem_t, qual = _extract_list_req(ann)
726
+ if is_list:
727
+ if qual:
728
+ matching = loc.with_qualifier_any(qual).keys()
729
+ if not matching:
730
+ errors.append(f"{getattr(k,'__name__',k)} expects List[{getattr(elem_t,'__name__',elem_t)}] with qualifier '{qual}' but no matching components exist")
731
+ continue
732
+ if isinstance(ann, str):
733
+ if ann in self._metadata or self._factory.has(ann) or self._find_md_for_name(ann) is not None:
734
+ continue
735
+ errors.append(f"{getattr(k,'__name__',k)} depends on string key '{ann}' which is not bound")
736
+ continue
737
+ if isinstance(ann, type) and not _skip_type(ann):
738
+ dep = self._find_md_for_type(ann)
739
+ if dep is None:
740
+ loc_name = "constructor" if callable_obj.__name__ == "__init__" else f"factory {md.factory_class.__name__}.{md.factory_method}"
741
+ errors.append(f"{getattr(k,'__name__',k)} {loc_name} depends on {getattr(ann,'__name__',ann)} which is not bound")
742
+ if errors:
743
+ raise InvalidBindingError(errors)
744
+
745
+ def finalize(self, overrides: Optional[Dict[KeyT, Any]]) -> None:
746
+ self.select_and_bind()
747
+ self._promote_scopes()
748
+ self._rebuild_indexes()
749
+ for _, selector, default_cls in sorted(self._on_missing, key=lambda x: -x[0]):
750
+ key = selector
751
+ if key in self._metadata or self._factory.has(key) or _can_be_selected_for(self._metadata, selector):
752
+ continue
753
+ provider = DeferredProvider(lambda pico, loc, c=default_cls: _build_class(c, pico, loc))
754
+ qset = set(str(q) for q in getattr(default_cls, PICO_META, {}).get("qualifier", ()))
755
+ sc = getattr(default_cls, PICO_META, {}).get("scope", "singleton")
756
+ md = ProviderMetadata(
757
+ key=key,
758
+ provided_type=key if isinstance(key, type) else None,
759
+ concrete_class=default_cls,
760
+ factory_class=None,
761
+ factory_method=None,
762
+ qualifiers=qset,
763
+ primary=True,
764
+ lazy=bool(getattr(default_cls, PICO_META, {}).get("lazy", False)),
765
+ infra=getattr(default_cls, PICO_INFRA, None),
766
+ pico_name=getattr(default_cls, PICO_NAME, None),
767
+ override=True,
768
+ scope=sc
769
+ )
770
+ self._bind_if_absent(key, provider)
771
+ self._metadata[key] = md
772
+ if isinstance(provider, DeferredProvider):
773
+ self._deferred.append(provider)
774
+ self._rebuild_indexes()
775
+ self._validate_bindings()
776
+
777
+ 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", ...] = ()) -> PicoContainer:
778
+ active = tuple(p.strip() for p in profiles if p)
779
+ allowed_set = set(a.strip() for a in allowed_profiles) if allowed_profiles is not None else None
780
+ if allowed_set is not None:
781
+ unknown = set(active) - allowed_set
782
+ if unknown:
783
+ raise ConfigurationError(f"Unknown profiles: {sorted(unknown)}; allowed: {sorted(allowed_set)}")
784
+ factory = ComponentFactory()
785
+ caches = ScopedCaches()
786
+ scopes = ScopeManager()
787
+ if custom_scopes:
788
+ for n, impl in custom_scopes.items():
789
+ scopes.register_scope(n, impl)
790
+ pico = PicoContainer(factory, caches, scopes, container_id=container_id, profiles=active)
791
+ registrar = Registrar(factory, profiles=active, environ=environ, logger=logger, config=config, tree_sources=tree_config)
792
+ for m in _iter_input_modules(modules):
793
+ registrar.register_module(m)
794
+ if overrides:
795
+ for k, v in overrides.items():
796
+ prov, _ = _normalize_override_provider(v)
797
+ factory.bind(k, prov)
798
+ registrar.finalize(overrides)
799
+ if validate_only:
800
+ return pico
801
+ locator = registrar.locator()
802
+ registrar.attach_runtime(pico, locator)
803
+ pico.attach_locator(locator)
804
+ return pico
221
805