pico-ioc 2.0.0__py3-none-any.whl → 2.0.2__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 +3 -12
- pico_ioc/_version.py +1 -1
- pico_ioc/aop.py +55 -21
- pico_ioc/api.py +497 -116
- 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.2.dist-info/METADATA +243 -0
- pico_ioc-2.0.2.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.2.dist-info}/WHEEL +0 -0
- {pico_ioc-2.0.0.dist-info → pico_ioc-2.0.2.dist-info}/licenses/LICENSE +0 -0
- {pico_ioc-2.0.0.dist-info → pico_ioc-2.0.2.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
|
|
@@ -7,12 +6,11 @@ import importlib
|
|
|
7
6
|
import pkgutil
|
|
8
7
|
import logging
|
|
9
8
|
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
|
|
9
|
+
from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union, get_args, get_origin, Annotated, Protocol, Mapping
|
|
11
10
|
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,
|
|
@@ -56,6 +54,31 @@ class FileSource:
|
|
|
56
54
|
return str(v)
|
|
57
55
|
return None
|
|
58
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
|
+
|
|
59
82
|
def _meta_get(obj: Any) -> Dict[str, Any]:
|
|
60
83
|
m = getattr(obj, PICO_META, None)
|
|
61
84
|
if m is None:
|
|
@@ -63,73 +86,119 @@ def _meta_get(obj: Any) -> Dict[str, Any]:
|
|
|
63
86
|
setattr(obj, PICO_META, m)
|
|
64
87
|
return m
|
|
65
88
|
|
|
66
|
-
def component(
|
|
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
|
+
):
|
|
67
103
|
def dec(c):
|
|
68
104
|
setattr(c, PICO_INFRA, "component")
|
|
69
105
|
setattr(c, PICO_NAME, name if name is not None else getattr(c, "__name__", str(c)))
|
|
70
106
|
setattr(c, PICO_KEY, name if name is not None else c)
|
|
71
|
-
_meta_get(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)}
|
|
72
122
|
return c
|
|
73
123
|
return dec(cls) if cls else dec
|
|
74
124
|
|
|
75
|
-
def factory(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
return
|
|
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
|
|
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
|
|
116
159
|
|
|
117
|
-
def
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
121
190
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
|
128
199
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
m["lazy"] = True
|
|
132
|
-
return obj
|
|
200
|
+
class Qualifier(str):
|
|
201
|
+
__slots__ = ()
|
|
133
202
|
|
|
134
203
|
def configuration(cls=None, *, prefix: Optional[str] = None):
|
|
135
204
|
def dec(c):
|
|
@@ -150,13 +219,6 @@ def cleanup(fn):
|
|
|
150
219
|
m["cleanup"] = True
|
|
151
220
|
return fn
|
|
152
221
|
|
|
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
222
|
def configured(target: Any, *, prefix: Optional[str] = None):
|
|
161
223
|
def dec(cls):
|
|
162
224
|
setattr(cls, PICO_INFRA, "configured")
|
|
@@ -278,27 +340,82 @@ def _collect_by_type(locator: ComponentLocator, t: type, q: Optional[str]):
|
|
|
278
340
|
out.append(k)
|
|
279
341
|
return out
|
|
280
342
|
|
|
281
|
-
def
|
|
282
|
-
|
|
283
|
-
|
|
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 = []
|
|
284
371
|
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
|
|
372
|
+
if name in ("self", "cls"): continue
|
|
373
|
+
if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD): continue
|
|
289
374
|
ann = param.annotation
|
|
290
375
|
is_list, elem_t, qual = _extract_list_req(ann)
|
|
291
|
-
if is_list
|
|
292
|
-
|
|
293
|
-
|
|
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)))
|
|
294
392
|
continue
|
|
393
|
+
ann = data
|
|
295
394
|
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
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
|
|
299
399
|
else:
|
|
300
400
|
key = name
|
|
301
|
-
|
|
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
|
+
tracer.note_param(data if isinstance(data, (str, type)) else name, name)
|
|
413
|
+
kwargs[name] = pico.get(data)
|
|
414
|
+
else:
|
|
415
|
+
vals = [pico.get(k) for k in data]
|
|
416
|
+
kwargs[name] = vals
|
|
417
|
+
finally:
|
|
418
|
+
tracer.restore_via(prev)
|
|
302
419
|
return kwargs
|
|
303
420
|
|
|
304
421
|
def _needs_async_configure(obj: Any) -> bool:
|
|
@@ -445,9 +562,12 @@ class Registrar:
|
|
|
445
562
|
self._resolver = ConfigResolver(tuple(tree_sources))
|
|
446
563
|
self._adapters = TypeAdapterRegistry()
|
|
447
564
|
self._graph = ObjectGraphBuilder(self._resolver, self._adapters)
|
|
565
|
+
self._provides_functions: Dict[KeyT, Callable[..., Any]] = {}
|
|
448
566
|
|
|
449
567
|
def locator(self) -> ComponentLocator:
|
|
450
|
-
|
|
568
|
+
loc = ComponentLocator(self._metadata, self._indexes)
|
|
569
|
+
setattr(loc, "_provides_functions", dict(self._provides_functions))
|
|
570
|
+
return loc
|
|
451
571
|
|
|
452
572
|
def attach_runtime(self, pico, locator: ComponentLocator) -> None:
|
|
453
573
|
for deferred in self._deferred:
|
|
@@ -476,24 +596,21 @@ class Registrar:
|
|
|
476
596
|
return True
|
|
477
597
|
p = set(c.get("profiles") or ())
|
|
478
598
|
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
599
|
return False
|
|
481
600
|
req = c.get("require_env") or ()
|
|
482
601
|
for k in req:
|
|
483
602
|
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
603
|
return False
|
|
486
604
|
pred = c.get("predicate")
|
|
487
605
|
if pred is None:
|
|
488
606
|
return True
|
|
489
607
|
try:
|
|
490
608
|
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))
|
|
609
|
+
except Exception:
|
|
493
610
|
return False
|
|
494
611
|
if not ok:
|
|
495
|
-
|
|
496
|
-
return
|
|
612
|
+
return False
|
|
613
|
+
return True
|
|
497
614
|
|
|
498
615
|
def _register_component_class(self, cls: type) -> None:
|
|
499
616
|
if not self._enabled_by_condition(cls):
|
|
@@ -510,19 +627,48 @@ class Registrar:
|
|
|
510
627
|
return
|
|
511
628
|
for name in dir(cls):
|
|
512
629
|
try:
|
|
513
|
-
|
|
630
|
+
raw = inspect.getattr_static(cls, name)
|
|
514
631
|
except Exception:
|
|
515
632
|
continue
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
633
|
+
fn = None
|
|
634
|
+
kind = None
|
|
635
|
+
if isinstance(raw, staticmethod):
|
|
636
|
+
fn = raw.__func__
|
|
637
|
+
kind = "static"
|
|
638
|
+
elif isinstance(raw, classmethod):
|
|
639
|
+
fn = raw.__func__
|
|
640
|
+
kind = "class"
|
|
641
|
+
elif inspect.isfunction(raw):
|
|
642
|
+
fn = raw
|
|
643
|
+
kind = "instance"
|
|
644
|
+
else:
|
|
645
|
+
continue
|
|
646
|
+
if getattr(fn, PICO_INFRA, None) != "provides":
|
|
647
|
+
continue
|
|
648
|
+
if not self._enabled_by_condition(fn):
|
|
649
|
+
continue
|
|
650
|
+
k = getattr(fn, PICO_KEY)
|
|
651
|
+
if kind == "instance":
|
|
520
652
|
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
|
-
|
|
653
|
+
else:
|
|
654
|
+
provider = DeferredProvider(lambda pico, loc, f=fn: _build_method(f, pico, loc))
|
|
655
|
+
rt = _get_return_type(fn)
|
|
656
|
+
qset = set(str(q) for q in getattr(fn, PICO_META, {}).get("qualifier", ()))
|
|
657
|
+
sc = getattr(fn, PICO_META, {}).get("scope", getattr(cls, PICO_META, {}).get("scope", "singleton"))
|
|
658
|
+
md = ProviderMetadata(
|
|
659
|
+
key=k,
|
|
660
|
+
provided_type=rt if isinstance(rt, type) else (k if isinstance(k, type) else None),
|
|
661
|
+
concrete_class=None,
|
|
662
|
+
factory_class=cls,
|
|
663
|
+
factory_method=name,
|
|
664
|
+
qualifiers=qset,
|
|
665
|
+
primary=bool(getattr(fn, PICO_META, {}).get("primary")),
|
|
666
|
+
lazy=bool(getattr(fn, PICO_META, {}).get("lazy", False)),
|
|
667
|
+
infra=getattr(cls, PICO_INFRA, None),
|
|
668
|
+
pico_name=getattr(fn, PICO_NAME, None),
|
|
669
|
+
scope=sc
|
|
670
|
+
)
|
|
671
|
+
self._queue(k, provider, md)
|
|
526
672
|
|
|
527
673
|
def _register_configuration_class(self, cls: type) -> None:
|
|
528
674
|
if not self._enabled_by_condition(cls):
|
|
@@ -551,6 +697,30 @@ class Registrar:
|
|
|
551
697
|
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
698
|
self._queue(target, provider, md)
|
|
553
699
|
|
|
700
|
+
def _register_provides_function(self, fn: Callable[..., Any]) -> None:
|
|
701
|
+
if not self._enabled_by_condition(fn):
|
|
702
|
+
return
|
|
703
|
+
k = getattr(fn, PICO_KEY)
|
|
704
|
+
provider = DeferredProvider(lambda pico, loc, f=fn: _build_method(f, pico, loc))
|
|
705
|
+
rt = _get_return_type(fn)
|
|
706
|
+
qset = set(str(q) for q in getattr(fn, PICO_META, {}).get("qualifier", ()))
|
|
707
|
+
sc = getattr(fn, PICO_META, {}).get("scope", "singleton")
|
|
708
|
+
md = ProviderMetadata(
|
|
709
|
+
key=k,
|
|
710
|
+
provided_type=rt if isinstance(rt, type) else (k if isinstance(k, type) else None),
|
|
711
|
+
concrete_class=None,
|
|
712
|
+
factory_class=None,
|
|
713
|
+
factory_method=getattr(fn, "__name__", None),
|
|
714
|
+
qualifiers=qset,
|
|
715
|
+
primary=bool(getattr(fn, PICO_META, {}).get("primary")),
|
|
716
|
+
lazy=bool(getattr(fn, PICO_META, {}).get("lazy", False)),
|
|
717
|
+
infra="provides",
|
|
718
|
+
pico_name=getattr(fn, PICO_NAME, None),
|
|
719
|
+
scope=sc
|
|
720
|
+
)
|
|
721
|
+
self._queue(k, provider, md)
|
|
722
|
+
self._provides_functions[k] = fn
|
|
723
|
+
|
|
554
724
|
def register_module(self, module: Any) -> None:
|
|
555
725
|
for _, obj in inspect.getmembers(module):
|
|
556
726
|
if inspect.isclass(obj):
|
|
@@ -569,6 +739,9 @@ class Registrar:
|
|
|
569
739
|
self._register_configuration_class(obj)
|
|
570
740
|
elif infra == "configured":
|
|
571
741
|
self._register_configured_class(obj)
|
|
742
|
+
for _, fn in inspect.getmembers(module, predicate=inspect.isfunction):
|
|
743
|
+
if getattr(fn, PICO_INFRA, None) == "provides":
|
|
744
|
+
self._register_provides_function(fn)
|
|
572
745
|
|
|
573
746
|
def _prefix_exists(self, md: ProviderMetadata) -> bool:
|
|
574
747
|
if md.infra != "configured":
|
|
@@ -634,6 +807,13 @@ class Registrar:
|
|
|
634
807
|
dep = self._find_md_for_type(t)
|
|
635
808
|
if dep and dep.scope != "singleton":
|
|
636
809
|
return dep.scope
|
|
810
|
+
if md.infra == "provides":
|
|
811
|
+
fn = self._provides_functions.get(md.key)
|
|
812
|
+
if callable(fn):
|
|
813
|
+
for t in self._iter_param_types(fn):
|
|
814
|
+
dep = self._find_md_for_type(t)
|
|
815
|
+
if dep and dep.scope != "singleton":
|
|
816
|
+
return dep.scope
|
|
637
817
|
return None
|
|
638
818
|
|
|
639
819
|
def _promote_scopes(self) -> None:
|
|
@@ -681,9 +861,13 @@ class Registrar:
|
|
|
681
861
|
if isinstance(t, type) and getattr(t, "__name__", "") == name:
|
|
682
862
|
return k
|
|
683
863
|
return None
|
|
684
|
-
|
|
864
|
+
|
|
685
865
|
def _validate_bindings(self) -> None:
|
|
686
866
|
errors: List[str] = []
|
|
867
|
+
|
|
868
|
+
def _fmt(k: KeyT) -> str:
|
|
869
|
+
return getattr(k, '__name__', str(k))
|
|
870
|
+
|
|
687
871
|
def _skip_type(t: type) -> bool:
|
|
688
872
|
if t in (str, int, float, bool, bytes):
|
|
689
873
|
return True
|
|
@@ -692,6 +876,7 @@ class Registrar:
|
|
|
692
876
|
if getattr(t, "_is_protocol", False):
|
|
693
877
|
return True
|
|
694
878
|
return False
|
|
879
|
+
|
|
695
880
|
def _should_validate(param: inspect.Parameter) -> bool:
|
|
696
881
|
if param.default is not inspect._empty:
|
|
697
882
|
return False
|
|
@@ -702,18 +887,44 @@ class Registrar:
|
|
|
702
887
|
if type(None) in args:
|
|
703
888
|
return False
|
|
704
889
|
return True
|
|
890
|
+
|
|
705
891
|
loc = ComponentLocator(self._metadata, self._indexes)
|
|
892
|
+
|
|
706
893
|
for k, md in self._metadata.items():
|
|
707
894
|
if md.infra == "configuration":
|
|
708
895
|
continue
|
|
896
|
+
|
|
709
897
|
callables_to_check: List[Callable[..., Any]] = []
|
|
898
|
+
loc_name = "unknown"
|
|
899
|
+
|
|
710
900
|
if md.concrete_class is not None:
|
|
711
901
|
callables_to_check.append(md.concrete_class.__init__)
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
902
|
+
loc_name = "constructor"
|
|
903
|
+
elif md.factory_class is not None and md.factory_method is not None:
|
|
904
|
+
try:
|
|
905
|
+
fn = getattr(md.factory_class, md.factory_method)
|
|
906
|
+
callables_to_check.append(fn)
|
|
907
|
+
loc_name = f"factory {_fmt(md.factory_class)}.{md.factory_method}"
|
|
908
|
+
except AttributeError:
|
|
909
|
+
errors.append(f"Component '{_fmt(k)}' refers to missing factory method '{md.factory_method}' on class '{_fmt(md.factory_class)}'")
|
|
910
|
+
continue
|
|
911
|
+
elif md.infra == "provides":
|
|
912
|
+
fn = self._provides_functions.get(k)
|
|
913
|
+
if callable(fn):
|
|
914
|
+
callables_to_check.append(fn)
|
|
915
|
+
loc_name = f"function {getattr(fn, '__name__', 'unknown')}"
|
|
916
|
+
else:
|
|
917
|
+
continue
|
|
918
|
+
else:
|
|
919
|
+
if not md.concrete_class and not md.factory_class and md.override:
|
|
920
|
+
continue
|
|
921
|
+
|
|
715
922
|
for callable_obj in callables_to_check:
|
|
716
|
-
|
|
923
|
+
try:
|
|
924
|
+
sig = inspect.signature(callable_obj)
|
|
925
|
+
except (ValueError, TypeError):
|
|
926
|
+
continue
|
|
927
|
+
|
|
717
928
|
for name, param in sig.parameters.items():
|
|
718
929
|
if name in ("self", "cls"):
|
|
719
930
|
continue
|
|
@@ -721,27 +932,45 @@ class Registrar:
|
|
|
721
932
|
continue
|
|
722
933
|
if not _should_validate(param):
|
|
723
934
|
continue
|
|
935
|
+
|
|
724
936
|
ann = param.annotation
|
|
725
937
|
is_list, elem_t, qual = _extract_list_req(ann)
|
|
938
|
+
|
|
726
939
|
if is_list:
|
|
727
940
|
if qual:
|
|
728
941
|
matching = loc.with_qualifier_any(qual).keys()
|
|
729
|
-
if not matching:
|
|
730
|
-
errors.append(f"{
|
|
942
|
+
if not matching and isinstance(elem_t, type) and not _skip_type(elem_t):
|
|
943
|
+
errors.append(f"{_fmt(k)} ({loc_name}) expects List[{_fmt(elem_t)}] with qualifier '{qual}' but no matching components exist")
|
|
944
|
+
continue
|
|
945
|
+
|
|
946
|
+
elif isinstance(ann, str):
|
|
947
|
+
key_found_by_name = self._find_md_for_name(ann)
|
|
948
|
+
directly_bound = ann in self._metadata or self._factory.has(ann)
|
|
949
|
+
if not key_found_by_name and not directly_bound:
|
|
950
|
+
errors.append(f"{_fmt(k)} ({loc_name}) depends on string key '{ann}' which is not bound")
|
|
951
|
+
continue
|
|
952
|
+
|
|
953
|
+
elif isinstance(ann, type) and not _skip_type(ann):
|
|
954
|
+
dep_key_found = self._factory.has(ann) or ann in self._metadata
|
|
955
|
+
if not dep_key_found:
|
|
956
|
+
assignable_md = self._find_md_for_type(ann)
|
|
957
|
+
if assignable_md is None:
|
|
958
|
+
by_name_key = self._find_md_for_name(getattr(ann, "__name__", ""))
|
|
959
|
+
if by_name_key is None:
|
|
960
|
+
errors.append(f"{_fmt(k)} ({loc_name}) depends on {_fmt(ann)} which is not bound")
|
|
731
961
|
continue
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
962
|
+
|
|
963
|
+
elif ann is inspect._empty:
|
|
964
|
+
name_key = name
|
|
965
|
+
key_found_by_name = self._find_md_for_name(name_key)
|
|
966
|
+
directly_bound = name_key in self._metadata or self._factory.has(name_key)
|
|
967
|
+
if not key_found_by_name and not directly_bound:
|
|
968
|
+
errors.append(f"{_fmt(k)} ({loc_name}) depends on parameter '{name}' with no type hint, and key '{name_key}' is not bound")
|
|
736
969
|
continue
|
|
737
|
-
|
|
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")
|
|
970
|
+
|
|
742
971
|
if errors:
|
|
743
972
|
raise InvalidBindingError(errors)
|
|
744
|
-
|
|
973
|
+
|
|
745
974
|
def finalize(self, overrides: Optional[Dict[KeyT, Any]]) -> None:
|
|
746
975
|
self.select_and_bind()
|
|
747
976
|
self._promote_scopes()
|
|
@@ -774,7 +1003,8 @@ class Registrar:
|
|
|
774
1003
|
self._rebuild_indexes()
|
|
775
1004
|
self._validate_bindings()
|
|
776
1005
|
|
|
777
|
-
|
|
1006
|
+
|
|
1007
|
+
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
1008
|
active = tuple(p.strip() for p in profiles if p)
|
|
779
1009
|
allowed_set = set(a.strip() for a in allowed_profiles) if allowed_profiles is not None else None
|
|
780
1010
|
if allowed_set is not None:
|
|
@@ -787,7 +1017,14 @@ def init(modules: Union[Any, Iterable[Any]], *, profiles: Tuple[str, ...] = (),
|
|
|
787
1017
|
if custom_scopes:
|
|
788
1018
|
for n, impl in custom_scopes.items():
|
|
789
1019
|
scopes.register_scope(n, impl)
|
|
790
|
-
pico = PicoContainer(
|
|
1020
|
+
pico = PicoContainer(
|
|
1021
|
+
factory,
|
|
1022
|
+
caches,
|
|
1023
|
+
scopes,
|
|
1024
|
+
container_id=container_id,
|
|
1025
|
+
profiles=active,
|
|
1026
|
+
observers=observers or [],
|
|
1027
|
+
)
|
|
791
1028
|
registrar = Registrar(factory, profiles=active, environ=environ, logger=logger, config=config, tree_sources=tree_config)
|
|
792
1029
|
for m in _iter_input_modules(modules):
|
|
793
1030
|
registrar.register_module(m)
|
|
@@ -797,9 +1034,153 @@ def init(modules: Union[Any, Iterable[Any]], *, profiles: Tuple[str, ...] = (),
|
|
|
797
1034
|
factory.bind(k, prov)
|
|
798
1035
|
registrar.finalize(overrides)
|
|
799
1036
|
if validate_only:
|
|
1037
|
+
locator = registrar.locator()
|
|
1038
|
+
pico.attach_locator(locator)
|
|
1039
|
+
_fail_fast_cycle_check(pico)
|
|
800
1040
|
return pico
|
|
801
1041
|
locator = registrar.locator()
|
|
802
1042
|
registrar.attach_runtime(pico, locator)
|
|
803
1043
|
pico.attach_locator(locator)
|
|
1044
|
+
_fail_fast_cycle_check(pico)
|
|
804
1045
|
return pico
|
|
805
1046
|
|
|
1047
|
+
def _dependency_keys_for(key: KeyT, pico: "PicoContainer") -> Tuple[KeyT, ...]:
|
|
1048
|
+
loc = pico._locator
|
|
1049
|
+
if not loc:
|
|
1050
|
+
return ()
|
|
1051
|
+
md = loc._metadata.get(key)
|
|
1052
|
+
if not md:
|
|
1053
|
+
return ()
|
|
1054
|
+
if md.concrete_class is not None:
|
|
1055
|
+
fn = md.concrete_class.__init__
|
|
1056
|
+
elif md.factory_class is not None and md.factory_method is not None:
|
|
1057
|
+
fn = getattr(md.factory_class, md.factory_method)
|
|
1058
|
+
else:
|
|
1059
|
+
return ()
|
|
1060
|
+
plan = _compile_argplan(fn, pico)
|
|
1061
|
+
deps: List[KeyT] = []
|
|
1062
|
+
for kind, _, data in plan:
|
|
1063
|
+
if kind == "key":
|
|
1064
|
+
deps.append(data)
|
|
1065
|
+
else:
|
|
1066
|
+
for k in data:
|
|
1067
|
+
deps.append(k)
|
|
1068
|
+
return tuple(deps)
|
|
1069
|
+
|
|
1070
|
+
def _get_signature_static(fn: Callable[..., Any]) -> inspect.Signature:
|
|
1071
|
+
return inspect.signature(fn)
|
|
1072
|
+
|
|
1073
|
+
def _compile_argplan_static(callable_obj: Callable[..., Any], locator: ComponentLocator) -> Tuple[Tuple[str, str, Any], ...]:
|
|
1074
|
+
sig = _get_signature_static(callable_obj)
|
|
1075
|
+
items: List[Tuple[str, str, Any]] = []
|
|
1076
|
+
for name, param in sig.parameters.items():
|
|
1077
|
+
if name in ("self", "cls"):
|
|
1078
|
+
continue
|
|
1079
|
+
if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
1080
|
+
continue
|
|
1081
|
+
ann = param.annotation
|
|
1082
|
+
is_list, elem_t, qual = _extract_list_req(ann)
|
|
1083
|
+
if is_list and locator is not None and isinstance(elem_t, type):
|
|
1084
|
+
keys = _collect_by_type(locator, elem_t, qual)
|
|
1085
|
+
items.append(("list", name, tuple(keys)))
|
|
1086
|
+
continue
|
|
1087
|
+
if ann is not inspect._empty and isinstance(ann, type):
|
|
1088
|
+
key: KeyT = ann
|
|
1089
|
+
elif ann is not inspect._empty and isinstance(ann, str):
|
|
1090
|
+
mapped = _find_key_by_name(locator, ann)
|
|
1091
|
+
key = mapped if mapped is not None else ann
|
|
1092
|
+
else:
|
|
1093
|
+
key = name
|
|
1094
|
+
items.append(("key", name, key))
|
|
1095
|
+
return tuple(items)
|
|
1096
|
+
|
|
1097
|
+
def _dependency_keys_for_static(md: ProviderMetadata, locator: ComponentLocator) -> Tuple[KeyT, ...]:
|
|
1098
|
+
if md.concrete_class is not None:
|
|
1099
|
+
fn = md.concrete_class.__init__
|
|
1100
|
+
elif md.factory_class is not None and md.factory_method is not None:
|
|
1101
|
+
fn = getattr(md.factory_class, md.factory_method)
|
|
1102
|
+
elif md.infra == "provides":
|
|
1103
|
+
fn = getattr(locator, "_provides_functions", {}).get(md.key)
|
|
1104
|
+
if not callable(fn):
|
|
1105
|
+
return ()
|
|
1106
|
+
else:
|
|
1107
|
+
return ()
|
|
1108
|
+
plan = _compile_argplan_static(fn, locator)
|
|
1109
|
+
deps: List[KeyT] = []
|
|
1110
|
+
for kind, _, data in plan:
|
|
1111
|
+
if kind == "key":
|
|
1112
|
+
deps.append(data)
|
|
1113
|
+
else:
|
|
1114
|
+
for k in data:
|
|
1115
|
+
deps.append(k)
|
|
1116
|
+
return tuple(deps)
|
|
1117
|
+
|
|
1118
|
+
def _build_resolution_graph(pico: "PicoContainer") -> Dict[KeyT, Tuple[KeyT, ...]]:
|
|
1119
|
+
loc = pico._locator
|
|
1120
|
+
if not loc:
|
|
1121
|
+
return {}
|
|
1122
|
+
|
|
1123
|
+
def _map_dep_to_bound_key(dep_key: KeyT) -> KeyT:
|
|
1124
|
+
if dep_key in loc._metadata:
|
|
1125
|
+
return dep_key
|
|
1126
|
+
if isinstance(dep_key, type):
|
|
1127
|
+
for k, md in loc._metadata.items():
|
|
1128
|
+
typ = md.provided_type or md.concrete_class
|
|
1129
|
+
if isinstance(typ, type):
|
|
1130
|
+
try:
|
|
1131
|
+
if issubclass(typ, dep_key):
|
|
1132
|
+
return k
|
|
1133
|
+
except Exception:
|
|
1134
|
+
continue
|
|
1135
|
+
return dep_key
|
|
1136
|
+
|
|
1137
|
+
graph: Dict[KeyT, Tuple[KeyT, ...]] = {}
|
|
1138
|
+
for key, md in list(loc._metadata.items()):
|
|
1139
|
+
deps: List[KeyT] = []
|
|
1140
|
+
for d in _dependency_keys_for_static(md, loc):
|
|
1141
|
+
mapped = _map_dep_to_bound_key(d)
|
|
1142
|
+
deps.append(mapped)
|
|
1143
|
+
graph[key] = tuple(deps)
|
|
1144
|
+
return graph
|
|
1145
|
+
|
|
1146
|
+
|
|
1147
|
+
def _find_cycle(graph: Dict[KeyT, Tuple[KeyT, ...]]) -> Optional[Tuple[KeyT, ...]]:
|
|
1148
|
+
temp: Set[KeyT] = set()
|
|
1149
|
+
perm: Set[KeyT] = set()
|
|
1150
|
+
stack: List[KeyT] = []
|
|
1151
|
+
def visit(n: KeyT) -> Optional[Tuple[KeyT, ...]]:
|
|
1152
|
+
if n in perm:
|
|
1153
|
+
return None
|
|
1154
|
+
if n in temp:
|
|
1155
|
+
try:
|
|
1156
|
+
idx = stack.index(n)
|
|
1157
|
+
return tuple(stack[idx:] + [n])
|
|
1158
|
+
except ValueError:
|
|
1159
|
+
return tuple([n, n])
|
|
1160
|
+
temp.add(n)
|
|
1161
|
+
stack.append(n)
|
|
1162
|
+
for m in graph.get(n, ()):
|
|
1163
|
+
c = visit(m)
|
|
1164
|
+
if c:
|
|
1165
|
+
return c
|
|
1166
|
+
stack.pop()
|
|
1167
|
+
temp.remove(n)
|
|
1168
|
+
perm.add(n)
|
|
1169
|
+
return None
|
|
1170
|
+
for node in graph.keys():
|
|
1171
|
+
c = visit(node)
|
|
1172
|
+
if c:
|
|
1173
|
+
return c
|
|
1174
|
+
return None
|
|
1175
|
+
|
|
1176
|
+
def _format_key(k: KeyT) -> str:
|
|
1177
|
+
return getattr(k, "__name__", str(k))
|
|
1178
|
+
|
|
1179
|
+
def _fail_fast_cycle_check(pico: "PicoContainer") -> None:
|
|
1180
|
+
graph = _build_resolution_graph(pico)
|
|
1181
|
+
cyc = _find_cycle(graph)
|
|
1182
|
+
if not cyc:
|
|
1183
|
+
return
|
|
1184
|
+
path = " -> ".join(_format_key(k) for k in cyc)
|
|
1185
|
+
raise InvalidBindingError([f"Circular dependency detected: {path}"])
|
|
1186
|
+
|