pico-ioc 2.0.5__py3-none-any.whl → 2.1.0__py3-none-any.whl

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