pico-ioc 1.1.0__py3-none-any.whl → 1.3.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/__init__.py +18 -4
- pico_ioc/_state.py +30 -0
- pico_ioc/_version.py +1 -1
- pico_ioc/api.py +206 -106
- pico_ioc/builder.py +242 -0
- pico_ioc/container.py +62 -34
- pico_ioc/decorators.py +76 -18
- pico_ioc/interceptors.py +50 -0
- pico_ioc/plugins.py +17 -1
- pico_ioc/policy.py +332 -0
- pico_ioc/proxy.py +41 -1
- pico_ioc/resolver.py +52 -33
- pico_ioc/scanner.py +76 -111
- pico_ioc/utils.py +25 -0
- pico_ioc-1.3.0.dist-info/METADATA +235 -0
- pico_ioc-1.3.0.dist-info/RECORD +20 -0
- pico_ioc/typing_utils.py +0 -29
- pico_ioc-1.1.0.dist-info/METADATA +0 -166
- pico_ioc-1.1.0.dist-info/RECORD +0 -17
- {pico_ioc-1.1.0.dist-info → pico_ioc-1.3.0.dist-info}/WHEEL +0 -0
- {pico_ioc-1.1.0.dist-info → pico_ioc-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {pico_ioc-1.1.0.dist-info → pico_ioc-1.3.0.dist-info}/top_level.txt +0 -0
pico_ioc/policy.py
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
# pico_ioc/policy.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import inspect
|
|
5
|
+
import os
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
from typing import Any, Dict, Iterable, List, Tuple, Optional
|
|
8
|
+
from .utils import create_alias_provider
|
|
9
|
+
from .decorators import (
|
|
10
|
+
CONDITIONAL_META,
|
|
11
|
+
PRIMARY_FLAG,
|
|
12
|
+
ON_MISSING_META,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# ---------------- helpers ----------------
|
|
16
|
+
|
|
17
|
+
def _target_from_provider(provider):
|
|
18
|
+
"""
|
|
19
|
+
Best-effort: find the real target behind a provider closure.
|
|
20
|
+
Priority: bound method > plain function > class. Fallback to the provider itself.
|
|
21
|
+
"""
|
|
22
|
+
fn = provider
|
|
23
|
+
try:
|
|
24
|
+
cells = getattr(fn, "__closure__", None) or ()
|
|
25
|
+
first_func = None
|
|
26
|
+
first_cls = None
|
|
27
|
+
for cell in cells:
|
|
28
|
+
cc = getattr(cell, "cell_contents", None)
|
|
29
|
+
if inspect.ismethod(cc):
|
|
30
|
+
return cc # highest priority: bound method
|
|
31
|
+
if first_func is None and inspect.isfunction(cc):
|
|
32
|
+
first_func = cc # keep first function seen
|
|
33
|
+
elif first_cls is None and inspect.isclass(cc):
|
|
34
|
+
first_cls = cc # keep first class seen
|
|
35
|
+
if first_func is not None:
|
|
36
|
+
return first_func
|
|
37
|
+
if first_cls is not None:
|
|
38
|
+
return first_cls
|
|
39
|
+
except Exception:
|
|
40
|
+
pass
|
|
41
|
+
return fn
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _owner_func(obj):
|
|
45
|
+
"""
|
|
46
|
+
If obj is a bound method, return the unbound function owned by the class (if resolvable).
|
|
47
|
+
This lets us read decorator flags placed on the original function.
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
if inspect.ismethod(obj) and getattr(obj, "__self__", None) is not None:
|
|
51
|
+
owner = obj.__self__.__class__
|
|
52
|
+
name = getattr(obj, "__name__", None)
|
|
53
|
+
if name and hasattr(owner, name):
|
|
54
|
+
cand = getattr(owner, name)
|
|
55
|
+
if inspect.isfunction(cand):
|
|
56
|
+
return cand
|
|
57
|
+
except Exception:
|
|
58
|
+
pass
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _find_attribute_on_target(target: Any, attr_name: str) -> Any:
|
|
63
|
+
"""
|
|
64
|
+
Finds an attribute on a target object, searching the object itself,
|
|
65
|
+
its __func__ (for bound methods), or the owning class's function.
|
|
66
|
+
"""
|
|
67
|
+
# 1. Check the object itself
|
|
68
|
+
value = getattr(target, attr_name, None)
|
|
69
|
+
if value is not None:
|
|
70
|
+
return value
|
|
71
|
+
|
|
72
|
+
# 2. Check the underlying function for bound methods
|
|
73
|
+
base_func = getattr(target, "__func__", None)
|
|
74
|
+
if base_func is not None:
|
|
75
|
+
value = getattr(base_func, attr_name, None)
|
|
76
|
+
if value is not None:
|
|
77
|
+
return value
|
|
78
|
+
|
|
79
|
+
# 3. Check the function on the owner class
|
|
80
|
+
owner_func = _owner_func(target)
|
|
81
|
+
if owner_func is not None:
|
|
82
|
+
value = getattr(owner_func, attr_name, None)
|
|
83
|
+
if value is not None:
|
|
84
|
+
return value
|
|
85
|
+
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _has_flag(obj, flag_name: str) -> bool:
|
|
90
|
+
"""Reads a boolean decorator flag from the target."""
|
|
91
|
+
return bool(_find_attribute_on_target(obj, flag_name))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _get_meta(obj, meta_name: str) -> Any:
|
|
95
|
+
"""Reads metadata (e.g., a dict) from the target."""
|
|
96
|
+
return _find_attribute_on_target(obj, meta_name)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _on_missing_meta(target):
|
|
100
|
+
"""
|
|
101
|
+
Normalize @on_missing metadata.
|
|
102
|
+
|
|
103
|
+
IMPORTANT: The decorator stores {"selector": <T>, "priority": <int>}.
|
|
104
|
+
"""
|
|
105
|
+
meta = _get_meta(target, ON_MISSING_META)
|
|
106
|
+
if not meta:
|
|
107
|
+
return None
|
|
108
|
+
selector = meta.get("selector")
|
|
109
|
+
prio = int(meta.get("priority", 0))
|
|
110
|
+
return (selector, prio)
|
|
111
|
+
|
|
112
|
+
def _conditional_active(target, *, profiles: List[str]) -> bool:
|
|
113
|
+
"""
|
|
114
|
+
Returns True if the target is active given profiles/env/predicate.
|
|
115
|
+
Activation logic (conjunctive across what's provided):
|
|
116
|
+
- If `profiles` present on target → at least one must match requested profiles.
|
|
117
|
+
- If `require_env` present → all listed env vars must be non-empty.
|
|
118
|
+
- If `predicate` present → must return True; exceptions → inactive (fail-closed).
|
|
119
|
+
- If none provided → active by default.
|
|
120
|
+
"""
|
|
121
|
+
meta = _get_meta(target, CONDITIONAL_META)
|
|
122
|
+
if not meta:
|
|
123
|
+
return True
|
|
124
|
+
|
|
125
|
+
profs = tuple(meta.get("profiles", ())) or ()
|
|
126
|
+
req_env = tuple(meta.get("require_env", ())) or ()
|
|
127
|
+
pred = meta.get("predicate", None)
|
|
128
|
+
|
|
129
|
+
# 1) profiles (if declared)
|
|
130
|
+
if profs:
|
|
131
|
+
if not profiles or not any(p in profs for p in profiles):
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
# 2) require_env (if declared)
|
|
135
|
+
if req_env:
|
|
136
|
+
if not all(os.getenv(k) not in (None, "") for k in req_env):
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
# 3) predicate (if declared)
|
|
140
|
+
if callable(pred):
|
|
141
|
+
try:
|
|
142
|
+
if not bool(pred()):
|
|
143
|
+
return False
|
|
144
|
+
except Exception:
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
# default: active
|
|
148
|
+
return True
|
|
149
|
+
|
|
150
|
+
# ---------------- public API ----------------
|
|
151
|
+
|
|
152
|
+
def apply_policy(container, *, profiles: Optional[List[str]] = None) -> None:
|
|
153
|
+
profiles = list(profiles or [])
|
|
154
|
+
|
|
155
|
+
_filter_inactive_factory_candidates(container, profiles=profiles)
|
|
156
|
+
_collapse_identical_keys_preferring_primary(container)
|
|
157
|
+
_create_active_component_base_aliases(container, profiles=profiles)
|
|
158
|
+
apply_defaults(container)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def apply_defaults(container) -> None:
|
|
162
|
+
"""
|
|
163
|
+
Bind default providers declared via @on_missing when no binding exists for the selector.
|
|
164
|
+
|
|
165
|
+
Supports:
|
|
166
|
+
- Class components decorated with @on_missing(Selector, priority=...)
|
|
167
|
+
- Factory @provides(...) where the provided method (or its owner) carries @on_missing
|
|
168
|
+
AND the provider got tagged with _pico_alias_for (the base type key).
|
|
169
|
+
"""
|
|
170
|
+
defaults: dict[Any, list[tuple[int, Any]]] = {}
|
|
171
|
+
|
|
172
|
+
# (1) Class components with @on_missing
|
|
173
|
+
for prov_key, meta in list(container._providers.items()): # type: ignore[attr-defined]
|
|
174
|
+
if not isinstance(prov_key, type):
|
|
175
|
+
continue
|
|
176
|
+
target = _target_from_provider(meta.get("factory"))
|
|
177
|
+
om = _on_missing_meta(target)
|
|
178
|
+
if not om:
|
|
179
|
+
continue
|
|
180
|
+
selector, prio = om
|
|
181
|
+
defaults.setdefault(selector, []).append((prio, prov_key))
|
|
182
|
+
|
|
183
|
+
# (2) Factory @provides(...) with @on_missing on the provided method/owner
|
|
184
|
+
for prov_key, meta in list(container._providers.items()): # type: ignore[attr-defined]
|
|
185
|
+
prov = meta.get("factory")
|
|
186
|
+
base = getattr(prov, "_pico_alias_for", None)
|
|
187
|
+
if base is None:
|
|
188
|
+
continue
|
|
189
|
+
target = _target_from_provider(prov)
|
|
190
|
+
om = _on_missing_meta(target)
|
|
191
|
+
if not om:
|
|
192
|
+
continue
|
|
193
|
+
_selector_from_flag, prio = om
|
|
194
|
+
defaults.setdefault(base, []).append((prio, prov_key))
|
|
195
|
+
|
|
196
|
+
# Bind highest priority default for each selector if not already bound
|
|
197
|
+
for base, candidates in defaults.items():
|
|
198
|
+
if container.has(base):
|
|
199
|
+
continue
|
|
200
|
+
candidates.sort(key=lambda t: t[0], reverse=True)
|
|
201
|
+
chosen_key = candidates[0][1]
|
|
202
|
+
|
|
203
|
+
def _make_delegate(_chosen_key=chosen_key):
|
|
204
|
+
def _factory():
|
|
205
|
+
return container.get(_chosen_key)
|
|
206
|
+
return _factory
|
|
207
|
+
|
|
208
|
+
container.bind(base, _make_delegate(), lazy=True)
|
|
209
|
+
|
|
210
|
+
# ---------------- stages ----------------
|
|
211
|
+
|
|
212
|
+
def _filter_inactive_factory_candidates(container, *, profiles: List[str]) -> None:
|
|
213
|
+
"""
|
|
214
|
+
Remove factory-provided candidates whose target is inactive under the given profiles/env/predicate.
|
|
215
|
+
This trims the candidate set early so later selection/aliasing runs on active options only.
|
|
216
|
+
"""
|
|
217
|
+
to_delete = []
|
|
218
|
+
for prov_key, meta in list(container._providers.items()): # type: ignore[attr-defined]
|
|
219
|
+
prov = meta.get("factory")
|
|
220
|
+
base = getattr(prov, "_pico_alias_for", None)
|
|
221
|
+
if base is None:
|
|
222
|
+
continue
|
|
223
|
+
target = _target_from_provider(prov)
|
|
224
|
+
active = _conditional_active(target, profiles=profiles)
|
|
225
|
+
if not active:
|
|
226
|
+
to_delete.append(prov_key)
|
|
227
|
+
for k in to_delete:
|
|
228
|
+
container._providers.pop(k, None) # type: ignore[attr-defined]
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _collapse_identical_keys_preferring_primary(container) -> None:
|
|
232
|
+
"""
|
|
233
|
+
For factory-provided products of the same base type, collapse to a single alias:
|
|
234
|
+
- If any candidate is marked @primary -> pick the first primary.
|
|
235
|
+
- Else leave multiple to be decided by defaults or later alias logic.
|
|
236
|
+
"""
|
|
237
|
+
alias_groups: dict[Any, list[tuple[Any, dict]]] = defaultdict(list)
|
|
238
|
+
for k, m in list(container._providers.items()): # type: ignore[attr-defined]
|
|
239
|
+
prov = m.get("factory")
|
|
240
|
+
base_key = getattr(prov, "_pico_alias_for", None)
|
|
241
|
+
if base_key is not None:
|
|
242
|
+
alias_groups[base_key].append((k, m))
|
|
243
|
+
|
|
244
|
+
for base, entries in alias_groups.items():
|
|
245
|
+
if not entries:
|
|
246
|
+
continue
|
|
247
|
+
|
|
248
|
+
if len(entries) == 1:
|
|
249
|
+
keep_key, _ = entries[0]
|
|
250
|
+
if (not container.has(base)) or (base != keep_key):
|
|
251
|
+
factory = create_alias_provider(container, keep_key)
|
|
252
|
+
container.bind(base, factory, lazy=True)
|
|
253
|
+
continue
|
|
254
|
+
|
|
255
|
+
primaries: list[tuple[Any, dict]] = []
|
|
256
|
+
for (kk, mm) in entries:
|
|
257
|
+
tgt = _target_from_provider(mm.get("factory"))
|
|
258
|
+
if _has_flag(tgt, PRIMARY_FLAG):
|
|
259
|
+
primaries.append((kk, mm))
|
|
260
|
+
|
|
261
|
+
if primaries:
|
|
262
|
+
keep_key, _ = primaries[0]
|
|
263
|
+
if (not container.has(base)) or (base != keep_key):
|
|
264
|
+
factory = create_alias_provider(container, keep_key)
|
|
265
|
+
container.bind(base, factory, lazy=True)
|
|
266
|
+
for (kk, _mm) in entries:
|
|
267
|
+
if kk != keep_key and kk != base:
|
|
268
|
+
container._providers.pop(kk, None) # type: ignore[attr-defined]
|
|
269
|
+
else:
|
|
270
|
+
# multiple, no @primary -> leave for defaults
|
|
271
|
+
pass
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _create_active_component_base_aliases(container, *, profiles: List[str]) -> None:
|
|
275
|
+
"""
|
|
276
|
+
For class components (not factory-bound), create base->impl aliases among ACTIVE implementations.
|
|
277
|
+
|
|
278
|
+
Preference order:
|
|
279
|
+
1) Regular active implementations (non-@on_missing), prefer @primary; else first.
|
|
280
|
+
2) If none, fall back to @on_missing implementations, prefer @primary; else first.
|
|
281
|
+
"""
|
|
282
|
+
base_to_impls: Dict[Any, List[Tuple[Any, Dict[str, Any]]]] = defaultdict(list)
|
|
283
|
+
|
|
284
|
+
# Collect active implementations
|
|
285
|
+
impls: List[Tuple[type, Dict[str, Any]]] = []
|
|
286
|
+
for key, meta in list(container._providers.items()): # type: ignore[attr-defined]
|
|
287
|
+
if not isinstance(key, type):
|
|
288
|
+
continue
|
|
289
|
+
tgt = _target_from_provider(meta.get("factory"))
|
|
290
|
+
if _conditional_active(tgt, profiles=profiles):
|
|
291
|
+
impls.append((key, meta))
|
|
292
|
+
|
|
293
|
+
# Map each impl to all bases in its MRO (excluding itself and object)
|
|
294
|
+
for impl_key, impl_meta in impls:
|
|
295
|
+
for base in getattr(impl_key, "__mro__", ())[1:]:
|
|
296
|
+
if base is object:
|
|
297
|
+
break
|
|
298
|
+
base_to_impls.setdefault(base, []).append((impl_key, impl_meta))
|
|
299
|
+
|
|
300
|
+
# Choose per-base implementation with correct priority
|
|
301
|
+
for base, impl_list in base_to_impls.items():
|
|
302
|
+
if container.has(base) or not impl_list:
|
|
303
|
+
continue
|
|
304
|
+
|
|
305
|
+
regular: List[Tuple[Any, Dict[str, Any]]] = []
|
|
306
|
+
fallbacks: List[Tuple[Any, Dict[str, Any]]] = []
|
|
307
|
+
|
|
308
|
+
for (impl_key, impl_meta) in impl_list:
|
|
309
|
+
tgt = _target_from_provider(impl_meta.get("factory"))
|
|
310
|
+
if _on_missing_meta(tgt):
|
|
311
|
+
fallbacks.append((impl_key, impl_meta))
|
|
312
|
+
else:
|
|
313
|
+
regular.append((impl_key, impl_meta))
|
|
314
|
+
|
|
315
|
+
def pick(cands: List[Tuple[Any, Dict[str, Any]]]) -> Optional[Any]:
|
|
316
|
+
if not cands:
|
|
317
|
+
return None
|
|
318
|
+
primaries = []
|
|
319
|
+
for (ik, im) in cands:
|
|
320
|
+
tgt = _target_from_provider(im.get("factory"))
|
|
321
|
+
if _has_flag(tgt, PRIMARY_FLAG):
|
|
322
|
+
primaries.append((ik, im))
|
|
323
|
+
chosen_key, _ = primaries[0] if primaries else cands[0]
|
|
324
|
+
return chosen_key
|
|
325
|
+
|
|
326
|
+
chosen_key = pick(regular) or pick(fallbacks)
|
|
327
|
+
if chosen_key is None:
|
|
328
|
+
continue
|
|
329
|
+
|
|
330
|
+
factory = create_alias_provider(container, chosen_key)
|
|
331
|
+
container.bind(base, factory, lazy=True)
|
|
332
|
+
|
pico_ioc/proxy.py
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# pico_ioc/proxy.py
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
from typing import Any, Callable, Sequence
|
|
6
|
+
import inspect
|
|
7
|
+
|
|
8
|
+
from .interceptors import Invocation, dispatch, MethodInterceptor
|
|
4
9
|
|
|
5
10
|
class ComponentProxy:
|
|
6
11
|
def __init__(self, object_creator: Callable[[], Any]):
|
|
@@ -74,4 +79,39 @@ class ComponentProxy:
|
|
|
74
79
|
def __call__(self, *args, **kwargs): return self._get_real_object()(*args, **kwargs)
|
|
75
80
|
def __enter__(self): return self._get_real_object().__enter__()
|
|
76
81
|
def __exit__(self, exc_type, exc_val, exc_tb): return self._get_real_object().__exit__(exc_type, exc_val, exc_tb)
|
|
82
|
+
|
|
83
|
+
class IoCProxy:
|
|
84
|
+
__slots__ = ("_target", "_interceptors")
|
|
85
|
+
|
|
86
|
+
def __init__(self, target: object, interceptors: Sequence[MethodInterceptor]):
|
|
87
|
+
self._target = target
|
|
88
|
+
self._interceptors = tuple(interceptors)
|
|
89
|
+
|
|
90
|
+
def __getattr__(self, name: str) -> Any:
|
|
91
|
+
attr = getattr(self._target, name)
|
|
92
|
+
if not callable(attr):
|
|
93
|
+
return attr
|
|
94
|
+
if hasattr(attr, "__get__"):
|
|
95
|
+
fn = attr.__get__(self._target, type(self._target))
|
|
96
|
+
else:
|
|
97
|
+
fn = attr
|
|
98
|
+
|
|
99
|
+
@lru_cache(maxsize=None)
|
|
100
|
+
def _wrap(bound_fn: Callable[..., Any]):
|
|
101
|
+
if inspect.iscoroutinefunction(bound_fn):
|
|
102
|
+
async def aw(*args, **kwargs):
|
|
103
|
+
inv = Invocation(self._target, name, bound_fn, args, kwargs)
|
|
104
|
+
# dispatch returns a coroutine for async methods
|
|
105
|
+
return await dispatch(self._interceptors, inv)
|
|
106
|
+
return aw
|
|
107
|
+
else:
|
|
108
|
+
def sw(*args, **kwargs):
|
|
109
|
+
inv = Invocation(self._target, name, bound_fn, args, kwargs)
|
|
110
|
+
# dispatch returns a *value* for sync methods
|
|
111
|
+
res = dispatch(self._interceptors, inv)
|
|
112
|
+
if inspect.isawaitable(res):
|
|
113
|
+
raise RuntimeError(f"Async interceptor on sync method: {name}")
|
|
114
|
+
return res
|
|
115
|
+
return sw
|
|
116
|
+
return _wrap(fn)
|
|
77
117
|
|
pico_ioc/resolver.py
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
import inspect
|
|
5
|
-
from typing import Any, Annotated, get_args, get_origin, get_type_hints
|
|
5
|
+
from typing import Any, Annotated, get_args, get_origin, get_type_hints, Callable
|
|
6
|
+
from contextvars import ContextVar
|
|
6
7
|
|
|
7
8
|
|
|
9
|
+
_path: ContextVar[list[tuple[str, str]]] = ContextVar("pico_resolve_path", default=[])
|
|
10
|
+
|
|
8
11
|
def _get_hints(obj, owner_cls=None) -> dict:
|
|
9
12
|
"""type hints with include_extras=True and correct globals/locals."""
|
|
10
13
|
mod = inspect.getmodule(obj)
|
|
@@ -45,66 +48,82 @@ class Resolver:
|
|
|
45
48
|
self.c = container
|
|
46
49
|
self._prefer_name_first = bool(prefer_name_first)
|
|
47
50
|
|
|
48
|
-
def create_instance(self, cls):
|
|
49
|
-
sig = inspect.signature(cls.__init__)
|
|
50
|
-
hints = _get_hints(cls.__init__, owner_cls=cls)
|
|
51
|
-
kwargs = {}
|
|
52
|
-
for name, param in sig.parameters.items():
|
|
53
|
-
if name == "self":
|
|
54
|
-
continue
|
|
55
|
-
if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
56
|
-
continue
|
|
57
|
-
ann = hints.get(name, param.annotation)
|
|
58
|
-
try:
|
|
59
|
-
value = self._resolve_param(name, ann)
|
|
60
|
-
except NameError:
|
|
61
|
-
if param.default is not inspect._empty:
|
|
62
|
-
continue
|
|
63
|
-
raise
|
|
64
|
-
kwargs[name] = value
|
|
65
|
-
return cls(**kwargs)
|
|
66
51
|
|
|
67
|
-
def
|
|
52
|
+
def _resolve_dependencies_for_callable(self, fn: Callable, owner_cls: Any = None) -> dict:
|
|
68
53
|
sig = inspect.signature(fn)
|
|
69
54
|
hints = _get_hints(fn, owner_cls=owner_cls)
|
|
70
55
|
kwargs = {}
|
|
56
|
+
|
|
57
|
+
path_owner = getattr(owner_cls, "__name__", getattr(fn, "__qualname__", "callable"))
|
|
58
|
+
if fn.__name__ == "__init__" and owner_cls:
|
|
59
|
+
path_owner = f"{path_owner}.__init__"
|
|
60
|
+
|
|
71
61
|
for name, param in sig.parameters.items():
|
|
72
|
-
if name == "self":
|
|
73
|
-
continue
|
|
74
|
-
if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
62
|
+
if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD) or name == "self":
|
|
75
63
|
continue
|
|
64
|
+
|
|
76
65
|
ann = hints.get(name, param.annotation)
|
|
66
|
+
st = _path.get()
|
|
67
|
+
_path.set(st + [(path_owner, name)])
|
|
77
68
|
try:
|
|
78
69
|
value = self._resolve_param(name, ann)
|
|
79
|
-
|
|
80
|
-
|
|
70
|
+
kwargs[name] = value
|
|
71
|
+
except NameError as e:
|
|
72
|
+
if param.default is not inspect.Parameter.empty:
|
|
73
|
+
_path.set(st)
|
|
81
74
|
continue
|
|
82
|
-
|
|
83
|
-
|
|
75
|
+
|
|
76
|
+
# If the error is already formatted with a chain, re-raise to preserve the full context.
|
|
77
|
+
if "(required by" in str(e):
|
|
78
|
+
raise
|
|
79
|
+
|
|
80
|
+
# Otherwise, this is a fresh error; add the full chain for the first time.
|
|
81
|
+
chain = " -> ".join(f"{owner}.{param}" for owner, param in _path.get())
|
|
82
|
+
raise NameError(f"{e} (required by {chain})") from e
|
|
83
|
+
finally:
|
|
84
|
+
cur = _path.get()
|
|
85
|
+
if cur:
|
|
86
|
+
_path.set(cur[:-1])
|
|
84
87
|
return kwargs
|
|
85
88
|
|
|
89
|
+
def create_instance(self, cls: type) -> Any:
|
|
90
|
+
"""Creates an instance of a class by resolving its __init__ dependencies."""
|
|
91
|
+
constructor_kwargs = self._resolve_dependencies_for_callable(cls.__init__, owner_cls=cls)
|
|
92
|
+
return cls(**constructor_kwargs)
|
|
93
|
+
|
|
94
|
+
def kwargs_for_callable(self, fn: Callable, *, owner_cls: Any = None) -> dict:
|
|
95
|
+
"""Resolves all keyword arguments for any callable."""
|
|
96
|
+
return self._resolve_dependencies_for_callable(fn, owner_cls=owner_cls)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _notify_resolve(self, key, ann, quals=()):
|
|
100
|
+
for ci in getattr(self.c, "_container_interceptors", ()):
|
|
101
|
+
try: ci.on_resolve(key, ann, tuple(quals) if quals else ())
|
|
102
|
+
except Exception: pass
|
|
103
|
+
|
|
86
104
|
def _resolve_param(self, name: str, ann: Any):
|
|
87
|
-
#
|
|
105
|
+
# Colecciones (list/tuple)
|
|
88
106
|
if _is_collection_hint(ann):
|
|
89
107
|
base, quals, container_kind = _base_and_qualifiers_from_hint(ann)
|
|
108
|
+
self._notify_resolve(base, ann, quals)
|
|
90
109
|
items = self.c._resolve_all_for_base(base, qualifiers=quals)
|
|
91
110
|
return list(items) if container_kind is list else tuple(items)
|
|
92
111
|
|
|
93
|
-
#
|
|
112
|
+
# Precedencias
|
|
94
113
|
if self._prefer_name_first and self.c.has(name):
|
|
114
|
+
self._notify_resolve(name, ann, ())
|
|
95
115
|
return self.c.get(name)
|
|
96
|
-
|
|
97
116
|
if ann is not inspect._empty and self.c.has(ann):
|
|
117
|
+
self._notify_resolve(ann, ann, ())
|
|
98
118
|
return self.c.get(ann)
|
|
99
|
-
|
|
100
119
|
if ann is not inspect._empty and isinstance(ann, type):
|
|
101
120
|
for base in ann.__mro__[1:]:
|
|
102
121
|
if self.c.has(base):
|
|
122
|
+
self._notify_resolve(base, ann, ())
|
|
103
123
|
return self.c.get(base)
|
|
104
|
-
|
|
105
124
|
if self.c.has(name):
|
|
125
|
+
self._notify_resolve(name, ann, ())
|
|
106
126
|
return self.c.get(name)
|
|
107
127
|
|
|
108
128
|
missing = ann if ann is not inspect._empty else name
|
|
109
129
|
raise NameError(f"No provider found for key {missing!r}")
|
|
110
|
-
|