pico-ioc 2.0.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/__init__.py +1 -12
- pico_ioc/_version.py +1 -1
- pico_ioc/aop.py +55 -21
- pico_ioc/api.py +464 -108
- pico_ioc/container.py +161 -26
- pico_ioc/event_bus.py +3 -4
- pico_ioc/exceptions.py +9 -3
- pico_ioc/scope.py +47 -2
- pico_ioc-2.0.1.dist-info/METADATA +243 -0
- pico_ioc-2.0.1.dist-info/RECORD +17 -0
- pico_ioc-2.0.0.dist-info/METADATA +0 -230
- pico_ioc-2.0.0.dist-info/RECORD +0 -17
- {pico_ioc-2.0.0.dist-info → pico_ioc-2.0.1.dist-info}/WHEEL +0 -0
- {pico_ioc-2.0.0.dist-info → pico_ioc-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {pico_ioc-2.0.0.dist-info → pico_ioc-2.0.1.dist-info}/top_level.txt +0 -0
pico_ioc/api.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# src/pico_ioc/api.py
|
|
2
1
|
import os
|
|
3
2
|
import json
|
|
4
3
|
import inspect
|
|
@@ -12,7 +11,6 @@ from .constants import LOGGER, PICO_INFRA, PICO_NAME, PICO_KEY, PICO_META
|
|
|
12
11
|
from .exceptions import (
|
|
13
12
|
ProviderNotFoundError,
|
|
14
13
|
CircularDependencyError,
|
|
15
|
-
ComponentCreationError,
|
|
16
14
|
ScopeError,
|
|
17
15
|
ConfigurationError,
|
|
18
16
|
SerializationError,
|
|
@@ -63,74 +61,120 @@ def _meta_get(obj: Any) -> Dict[str, Any]:
|
|
|
63
61
|
setattr(obj, PICO_META, m)
|
|
64
62
|
return m
|
|
65
63
|
|
|
66
|
-
def component(
|
|
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
|
+
):
|
|
67
78
|
def dec(c):
|
|
68
79
|
setattr(c, PICO_INFRA, "component")
|
|
69
80
|
setattr(c, PICO_NAME, name if name is not None else getattr(c, "__name__", str(c)))
|
|
70
81
|
setattr(c, PICO_KEY, name if name is not None else c)
|
|
71
|
-
_meta_get(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)}
|
|
72
97
|
return c
|
|
73
98
|
return dec(cls) if cls else dec
|
|
74
99
|
|
|
75
|
-
def factory(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
80
134
|
|
|
81
|
-
def provides(
|
|
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
|
+
):
|
|
82
149
|
def dec(fn):
|
|
83
|
-
|
|
150
|
+
target = fn.__func__ if isinstance(fn, (staticmethod, classmethod)) else fn
|
|
151
|
+
@functools.wraps(target)
|
|
84
152
|
def w(*a, **k):
|
|
85
|
-
return
|
|
153
|
+
return target(*a, **k)
|
|
86
154
|
setattr(w, PICO_INFRA, "provides")
|
|
87
|
-
setattr(w, PICO_NAME, key)
|
|
155
|
+
setattr(w, PICO_NAME, name if name is not None else key)
|
|
88
156
|
setattr(w, PICO_KEY, key)
|
|
89
|
-
_meta_get(w)
|
|
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)}
|
|
90
172
|
return w
|
|
91
173
|
return dec
|
|
92
174
|
|
|
93
175
|
class Qualifier(str):
|
|
94
176
|
__slots__ = ()
|
|
95
177
|
|
|
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
178
|
def configuration(cls=None, *, prefix: Optional[str] = None):
|
|
135
179
|
def dec(c):
|
|
136
180
|
setattr(c, PICO_INFRA, "configuration")
|
|
@@ -150,13 +194,6 @@ def cleanup(fn):
|
|
|
150
194
|
m["cleanup"] = True
|
|
151
195
|
return fn
|
|
152
196
|
|
|
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
197
|
def configured(target: Any, *, prefix: Optional[str] = None):
|
|
161
198
|
def dec(cls):
|
|
162
199
|
setattr(cls, PICO_INFRA, "configured")
|
|
@@ -278,27 +315,82 @@ def _collect_by_type(locator: ComponentLocator, t: type, q: Optional[str]):
|
|
|
278
315
|
out.append(k)
|
|
279
316
|
return out
|
|
280
317
|
|
|
281
|
-
def
|
|
282
|
-
|
|
283
|
-
|
|
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 = []
|
|
284
346
|
for name, param in sig.parameters.items():
|
|
285
|
-
if name in ("self", "cls"):
|
|
286
|
-
|
|
287
|
-
if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
288
|
-
continue
|
|
347
|
+
if name in ("self", "cls"): continue
|
|
348
|
+
if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD): continue
|
|
289
349
|
ann = param.annotation
|
|
290
350
|
is_list, elem_t, qual = _extract_list_req(ann)
|
|
291
|
-
if is_list
|
|
292
|
-
|
|
293
|
-
|
|
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
|
|
360
|
+
items = []
|
|
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)))
|
|
294
367
|
continue
|
|
368
|
+
ann = data
|
|
295
369
|
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
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
|
|
299
374
|
else:
|
|
300
375
|
key = name
|
|
301
|
-
|
|
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)
|
|
302
394
|
return kwargs
|
|
303
395
|
|
|
304
396
|
def _needs_async_configure(obj: Any) -> bool:
|
|
@@ -445,9 +537,12 @@ class Registrar:
|
|
|
445
537
|
self._resolver = ConfigResolver(tuple(tree_sources))
|
|
446
538
|
self._adapters = TypeAdapterRegistry()
|
|
447
539
|
self._graph = ObjectGraphBuilder(self._resolver, self._adapters)
|
|
540
|
+
self._provides_functions: Dict[KeyT, Callable[..., Any]] = {}
|
|
448
541
|
|
|
449
542
|
def locator(self) -> ComponentLocator:
|
|
450
|
-
|
|
543
|
+
loc = ComponentLocator(self._metadata, self._indexes)
|
|
544
|
+
setattr(loc, "_provides_functions", dict(self._provides_functions))
|
|
545
|
+
return loc
|
|
451
546
|
|
|
452
547
|
def attach_runtime(self, pico, locator: ComponentLocator) -> None:
|
|
453
548
|
for deferred in self._deferred:
|
|
@@ -476,24 +571,21 @@ class Registrar:
|
|
|
476
571
|
return True
|
|
477
572
|
p = set(c.get("profiles") or ())
|
|
478
573
|
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
574
|
return False
|
|
481
575
|
req = c.get("require_env") or ()
|
|
482
576
|
for k in req:
|
|
483
577
|
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
578
|
return False
|
|
486
579
|
pred = c.get("predicate")
|
|
487
580
|
if pred is None:
|
|
488
581
|
return True
|
|
489
582
|
try:
|
|
490
583
|
ok = bool(pred())
|
|
491
|
-
except Exception
|
|
492
|
-
self._log.info("excluded_by_predicate_error name=%s error=%s", getattr(obj, "__name__", str(obj)), repr(e))
|
|
584
|
+
except Exception:
|
|
493
585
|
return False
|
|
494
586
|
if not ok:
|
|
495
|
-
|
|
496
|
-
return
|
|
587
|
+
return False
|
|
588
|
+
return True
|
|
497
589
|
|
|
498
590
|
def _register_component_class(self, cls: type) -> None:
|
|
499
591
|
if not self._enabled_by_condition(cls):
|
|
@@ -510,19 +602,48 @@ class Registrar:
|
|
|
510
602
|
return
|
|
511
603
|
for name in dir(cls):
|
|
512
604
|
try:
|
|
513
|
-
|
|
605
|
+
raw = inspect.getattr_static(cls, name)
|
|
514
606
|
except Exception:
|
|
515
607
|
continue
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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":
|
|
520
627
|
provider = DeferredProvider(lambda pico, loc, fc=cls, mn=name: _build_method(getattr(_build_class(fc, pico, loc), mn), pico, loc))
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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)
|
|
526
647
|
|
|
527
648
|
def _register_configuration_class(self, cls: type) -> None:
|
|
528
649
|
if not self._enabled_by_condition(cls):
|
|
@@ -551,6 +672,30 @@ class Registrar:
|
|
|
551
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)
|
|
552
673
|
self._queue(target, provider, md)
|
|
553
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
|
+
|
|
554
699
|
def register_module(self, module: Any) -> None:
|
|
555
700
|
for _, obj in inspect.getmembers(module):
|
|
556
701
|
if inspect.isclass(obj):
|
|
@@ -569,6 +714,9 @@ class Registrar:
|
|
|
569
714
|
self._register_configuration_class(obj)
|
|
570
715
|
elif infra == "configured":
|
|
571
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)
|
|
572
720
|
|
|
573
721
|
def _prefix_exists(self, md: ProviderMetadata) -> bool:
|
|
574
722
|
if md.infra != "configured":
|
|
@@ -634,6 +782,13 @@ class Registrar:
|
|
|
634
782
|
dep = self._find_md_for_type(t)
|
|
635
783
|
if dep and dep.scope != "singleton":
|
|
636
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
|
|
637
792
|
return None
|
|
638
793
|
|
|
639
794
|
def _promote_scopes(self) -> None:
|
|
@@ -681,9 +836,13 @@ class Registrar:
|
|
|
681
836
|
if isinstance(t, type) and getattr(t, "__name__", "") == name:
|
|
682
837
|
return k
|
|
683
838
|
return None
|
|
684
|
-
|
|
839
|
+
|
|
685
840
|
def _validate_bindings(self) -> None:
|
|
686
841
|
errors: List[str] = []
|
|
842
|
+
|
|
843
|
+
def _fmt(k: KeyT) -> str:
|
|
844
|
+
return getattr(k, '__name__', str(k))
|
|
845
|
+
|
|
687
846
|
def _skip_type(t: type) -> bool:
|
|
688
847
|
if t in (str, int, float, bool, bytes):
|
|
689
848
|
return True
|
|
@@ -692,6 +851,7 @@ class Registrar:
|
|
|
692
851
|
if getattr(t, "_is_protocol", False):
|
|
693
852
|
return True
|
|
694
853
|
return False
|
|
854
|
+
|
|
695
855
|
def _should_validate(param: inspect.Parameter) -> bool:
|
|
696
856
|
if param.default is not inspect._empty:
|
|
697
857
|
return False
|
|
@@ -702,18 +862,44 @@ class Registrar:
|
|
|
702
862
|
if type(None) in args:
|
|
703
863
|
return False
|
|
704
864
|
return True
|
|
865
|
+
|
|
705
866
|
loc = ComponentLocator(self._metadata, self._indexes)
|
|
867
|
+
|
|
706
868
|
for k, md in self._metadata.items():
|
|
707
869
|
if md.infra == "configuration":
|
|
708
870
|
continue
|
|
871
|
+
|
|
709
872
|
callables_to_check: List[Callable[..., Any]] = []
|
|
873
|
+
loc_name = "unknown"
|
|
874
|
+
|
|
710
875
|
if md.concrete_class is not None:
|
|
711
876
|
callables_to_check.append(md.concrete_class.__init__)
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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
|
+
|
|
715
897
|
for callable_obj in callables_to_check:
|
|
716
|
-
|
|
898
|
+
try:
|
|
899
|
+
sig = inspect.signature(callable_obj)
|
|
900
|
+
except (ValueError, TypeError):
|
|
901
|
+
continue
|
|
902
|
+
|
|
717
903
|
for name, param in sig.parameters.items():
|
|
718
904
|
if name in ("self", "cls"):
|
|
719
905
|
continue
|
|
@@ -721,27 +907,45 @@ class Registrar:
|
|
|
721
907
|
continue
|
|
722
908
|
if not _should_validate(param):
|
|
723
909
|
continue
|
|
910
|
+
|
|
724
911
|
ann = param.annotation
|
|
725
912
|
is_list, elem_t, qual = _extract_list_req(ann)
|
|
913
|
+
|
|
726
914
|
if is_list:
|
|
727
915
|
if qual:
|
|
728
916
|
matching = loc.with_qualifier_any(qual).keys()
|
|
729
|
-
if not matching:
|
|
730
|
-
errors.append(f"{
|
|
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")
|
|
731
919
|
continue
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
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")
|
|
736
936
|
continue
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
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
|
+
|
|
742
946
|
if errors:
|
|
743
947
|
raise InvalidBindingError(errors)
|
|
744
|
-
|
|
948
|
+
|
|
745
949
|
def finalize(self, overrides: Optional[Dict[KeyT, Any]]) -> None:
|
|
746
950
|
self.select_and_bind()
|
|
747
951
|
self._promote_scopes()
|
|
@@ -774,7 +978,8 @@ class Registrar:
|
|
|
774
978
|
self._rebuild_indexes()
|
|
775
979
|
self._validate_bindings()
|
|
776
980
|
|
|
777
|
-
|
|
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:
|
|
778
983
|
active = tuple(p.strip() for p in profiles if p)
|
|
779
984
|
allowed_set = set(a.strip() for a in allowed_profiles) if allowed_profiles is not None else None
|
|
780
985
|
if allowed_set is not None:
|
|
@@ -787,7 +992,14 @@ def init(modules: Union[Any, Iterable[Any]], *, profiles: Tuple[str, ...] = (),
|
|
|
787
992
|
if custom_scopes:
|
|
788
993
|
for n, impl in custom_scopes.items():
|
|
789
994
|
scopes.register_scope(n, impl)
|
|
790
|
-
pico = PicoContainer(
|
|
995
|
+
pico = PicoContainer(
|
|
996
|
+
factory,
|
|
997
|
+
caches,
|
|
998
|
+
scopes,
|
|
999
|
+
container_id=container_id,
|
|
1000
|
+
profiles=active,
|
|
1001
|
+
observers=observers or [],
|
|
1002
|
+
)
|
|
791
1003
|
registrar = Registrar(factory, profiles=active, environ=environ, logger=logger, config=config, tree_sources=tree_config)
|
|
792
1004
|
for m in _iter_input_modules(modules):
|
|
793
1005
|
registrar.register_module(m)
|
|
@@ -797,9 +1009,153 @@ def init(modules: Union[Any, Iterable[Any]], *, profiles: Tuple[str, ...] = (),
|
|
|
797
1009
|
factory.bind(k, prov)
|
|
798
1010
|
registrar.finalize(overrides)
|
|
799
1011
|
if validate_only:
|
|
1012
|
+
locator = registrar.locator()
|
|
1013
|
+
pico.attach_locator(locator)
|
|
1014
|
+
_fail_fast_cycle_check(pico)
|
|
800
1015
|
return pico
|
|
801
1016
|
locator = registrar.locator()
|
|
802
1017
|
registrar.attach_runtime(pico, locator)
|
|
803
1018
|
pico.attach_locator(locator)
|
|
1019
|
+
_fail_fast_cycle_check(pico)
|
|
804
1020
|
return pico
|
|
805
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
|
|
1150
|
+
|
|
1151
|
+
def _format_key(k: KeyT) -> str:
|
|
1152
|
+
return getattr(k, "__name__", str(k))
|
|
1153
|
+
|
|
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}"])
|
|
1161
|
+
|