pico-ioc 1.3.0__py3-none-any.whl → 1.5.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/policy.py CHANGED
@@ -1,51 +1,39 @@
1
- # pico_ioc/policy.py
1
+ # src/pico_ioc/policy.py
2
2
  from __future__ import annotations
3
3
 
4
4
  import inspect
5
5
  import os
6
6
  from collections import defaultdict
7
- from typing import Any, Dict, Iterable, List, Tuple, Optional
7
+ from typing import Any, Dict, List, Optional, Tuple
8
+
8
9
  from .utils import create_alias_provider
9
- from .decorators import (
10
- CONDITIONAL_META,
11
- PRIMARY_FLAG,
12
- ON_MISSING_META,
13
- )
10
+ from .decorators import CONDITIONAL_META, PRIMARY_FLAG, ON_MISSING_META
11
+ from . import _state
12
+
14
13
 
15
- # ---------------- helpers ----------------
14
+ # ------------------- helpers -------------------
16
15
 
17
16
  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
- """
17
+ """Try to resolve the 'real' target behind a provider closure (class, function or bound method)."""
22
18
  fn = provider
23
19
  try:
24
20
  cells = getattr(fn, "__closure__", None) or ()
25
- first_func = None
26
- first_cls = None
21
+ first_func, first_cls = None, None
27
22
  for cell in cells:
28
23
  cc = getattr(cell, "cell_contents", None)
29
24
  if inspect.ismethod(cc):
30
- return cc # highest priority: bound method
25
+ return cc
31
26
  if first_func is None and inspect.isfunction(cc):
32
- first_func = cc # keep first function seen
27
+ first_func = cc
33
28
  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
29
+ first_cls = cc
30
+ return first_func or first_cls or fn
39
31
  except Exception:
40
- pass
41
- return fn
32
+ return fn
42
33
 
43
34
 
44
35
  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
- """
36
+ """If obj is a bound method, return the unbound function on its owner class."""
49
37
  try:
50
38
  if inspect.ismethod(obj) and getattr(obj, "__self__", None) is not None:
51
39
  owner = obj.__self__.__class__
@@ -60,96 +48,66 @@ def _owner_func(obj):
60
48
 
61
49
 
62
50
  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
51
+ """Look for metadata on object, its underlying function, or owner class method."""
52
+ val = getattr(target, attr_name, None)
53
+ if val is not None:
54
+ return val
73
55
  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
-
56
+ if base_func:
57
+ val = getattr(base_func, attr_name, None)
58
+ if val is not None:
59
+ return val
60
+ of = _owner_func(target)
61
+ if of:
62
+ val = getattr(of, attr_name, None)
63
+ if val is not None:
64
+ return val
86
65
  return None
87
66
 
88
67
 
89
68
  def _has_flag(obj, flag_name: str) -> bool:
90
- """Reads a boolean decorator flag from the target."""
91
69
  return bool(_find_attribute_on_target(obj, flag_name))
92
70
 
93
71
 
94
72
  def _get_meta(obj, meta_name: str) -> Any:
95
- """Reads metadata (e.g., a dict) from the target."""
96
73
  return _find_attribute_on_target(obj, meta_name)
97
74
 
98
75
 
99
76
  def _on_missing_meta(target):
100
- """
101
- Normalize @on_missing metadata.
102
-
103
- IMPORTANT: The decorator stores {"selector": <T>, "priority": <int>}.
104
- """
77
+ """Normalize @on_missing metadata."""
105
78
  meta = _get_meta(target, ON_MISSING_META)
106
79
  if not meta:
107
80
  return None
108
- selector = meta.get("selector")
109
- prio = int(meta.get("priority", 0))
110
- return (selector, prio)
81
+ return (meta.get("selector"), int(meta.get("priority", 0)))
82
+
111
83
 
112
84
  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
- """
85
+ """Check if target is active given profiles/env/predicate."""
121
86
  meta = _get_meta(target, CONDITIONAL_META)
122
87
  if not meta:
123
88
  return True
124
89
 
125
- profs = tuple(meta.get("profiles", ())) or ()
126
- req_env = tuple(meta.get("require_env", ())) or ()
127
- pred = meta.get("predicate", None)
90
+ profs = tuple(meta.get("profiles", ()))
91
+ req_env = tuple(meta.get("require_env", ()))
92
+ pred = meta.get("predicate")
128
93
 
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)
94
+ if profs and (not profiles or not any(p in profs for p in profiles)):
95
+ return False
96
+ if req_env and not all(os.getenv(k) not in (None, "") for k in req_env):
97
+ return False
140
98
  if callable(pred):
141
99
  try:
142
100
  if not bool(pred()):
143
101
  return False
144
102
  except Exception:
145
103
  return False
146
-
147
- # default: active
148
104
  return True
149
105
 
150
- # ---------------- public API ----------------
106
+
107
+ # ------------------- public API -------------------
151
108
 
152
109
  def apply_policy(container, *, profiles: Optional[List[str]] = None) -> None:
110
+ """Run all policy stages on the given container."""
153
111
  profiles = list(profiles or [])
154
112
 
155
113
  _filter_inactive_factory_candidates(container, profiles=profiles)
@@ -159,174 +117,129 @@ def apply_policy(container, *, profiles: Optional[List[str]] = None) -> None:
159
117
 
160
118
 
161
119
  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
- """
120
+ """Bind defaults declared with @on_missing if no binding exists for selector."""
170
121
  defaults: dict[Any, list[tuple[int, Any]]] = {}
171
122
 
172
- # (1) Class components with @on_missing
173
- for prov_key, meta in list(container._providers.items()): # type: ignore[attr-defined]
123
+ # class components
124
+ for prov_key, meta in list(container._providers.items()): # type: ignore
174
125
  if not isinstance(prov_key, type):
175
126
  continue
176
127
  target = _target_from_provider(meta.get("factory"))
177
128
  om = _on_missing_meta(target)
178
- if not om:
179
- continue
180
- selector, prio = om
181
- defaults.setdefault(selector, []).append((prio, prov_key))
129
+ if om:
130
+ selector, prio = om
131
+ defaults.setdefault(selector, []).append((prio, prov_key))
182
132
 
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]
133
+ # factory provides
134
+ for prov_key, meta in list(container._providers.items()): # type: ignore
185
135
  prov = meta.get("factory")
186
136
  base = getattr(prov, "_pico_alias_for", None)
187
137
  if base is None:
188
138
  continue
189
139
  target = _target_from_provider(prov)
190
140
  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))
141
+ if om:
142
+ _sel, prio = om
143
+ defaults.setdefault(base, []).append((prio, prov_key))
195
144
 
196
- # Bind highest priority default for each selector if not already bound
197
- for base, candidates in defaults.items():
145
+ # bind highest priority candidate
146
+ for base, cands in defaults.items():
198
147
  if container.has(base):
199
148
  continue
200
- candidates.sort(key=lambda t: t[0], reverse=True)
201
- chosen_key = candidates[0][1]
149
+ cands.sort(key=lambda t: t[0], reverse=True)
150
+ chosen_key = cands[0][1]
202
151
 
203
- def _make_delegate(_chosen_key=chosen_key):
204
- def _factory():
205
- return container.get(_chosen_key)
206
- return _factory
152
+ def _delegate(_k=chosen_key):
153
+ def _f():
154
+ return container.get(_k)
155
+ return _f
207
156
 
208
- container.bind(base, _make_delegate(), lazy=True)
157
+ container.bind(base, _delegate(), lazy=True)
209
158
 
210
- # ---------------- stages ----------------
159
+
160
+ # ------------------- stages -------------------
211
161
 
212
162
  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
- """
163
+ """Remove factories inactive under profiles/env/predicate."""
217
164
  to_delete = []
218
- for prov_key, meta in list(container._providers.items()): # type: ignore[attr-defined]
165
+ for prov_key, meta in list(container._providers.items()): # type: ignore
219
166
  prov = meta.get("factory")
220
167
  base = getattr(prov, "_pico_alias_for", None)
221
168
  if base is None:
222
169
  continue
223
170
  target = _target_from_provider(prov)
224
- active = _conditional_active(target, profiles=profiles)
225
- if not active:
171
+ if not _conditional_active(target, profiles=profiles):
226
172
  to_delete.append(prov_key)
227
173
  for k in to_delete:
228
- container._providers.pop(k, None) # type: ignore[attr-defined]
174
+ container._providers.pop(k, None) # type: ignore
229
175
 
230
176
 
231
177
  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]
178
+ """For multiple factory candidates of same base, keep one (prefer @primary)."""
179
+ groups: dict[Any, list[tuple[Any, dict]]] = defaultdict(list)
180
+ for k, m in list(container._providers.items()): # type: ignore
239
181
  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))
182
+ base = getattr(prov, "_pico_alias_for", None)
183
+ if base is not None:
184
+ groups[base].append((k, m))
243
185
 
244
- for base, entries in alias_groups.items():
186
+ for base, entries in groups.items():
245
187
  if not entries:
246
188
  continue
247
-
248
189
  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)
190
+ keep, _ = entries[0]
191
+ if (not container.has(base)) or (base != keep):
192
+ factory = create_alias_provider(container, keep)
252
193
  container.bind(base, factory, lazy=True)
253
194
  continue
254
195
 
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)
196
+ prims = [(kk, mm) for (kk, mm) in entries if _has_flag(_target_from_provider(mm["factory"]), PRIMARY_FLAG)]
197
+ if prims:
198
+ keep, _ = prims[0]
199
+ if (not container.has(base)) or (base != keep):
200
+ factory = create_alias_provider(container, keep)
265
201
  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
202
+ for kk, _mm in entries:
203
+ if kk != keep and kk != base:
204
+ container._providers.pop(kk, None) # type: ignore
272
205
 
273
206
 
274
207
  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]
208
+ """For active class components, create base->impl aliases (prefer @primary)."""
209
+ impls: List[Tuple[type, dict]] = []
210
+ for key, meta in list(container._providers.items()): # type: ignore
287
211
  if not isinstance(key, type):
288
212
  continue
289
213
  tgt = _target_from_provider(meta.get("factory"))
290
214
  if _conditional_active(tgt, profiles=profiles):
291
215
  impls.append((key, meta))
292
216
 
293
- # Map each impl to all bases in its MRO (excluding itself and object)
217
+ base_to_impls: Dict[Any, List[Tuple[Any, dict]]] = defaultdict(list)
294
218
  for impl_key, impl_meta in impls:
295
219
  for base in getattr(impl_key, "__mro__", ())[1:]:
296
220
  if base is object:
297
221
  break
298
- base_to_impls.setdefault(base, []).append((impl_key, impl_meta))
222
+ base_to_impls[base].append((impl_key, impl_meta))
299
223
 
300
- # Choose per-base implementation with correct priority
301
224
  for base, impl_list in base_to_impls.items():
302
225
  if container.has(base) or not impl_list:
303
226
  continue
304
227
 
305
- regular: List[Tuple[Any, Dict[str, Any]]] = []
306
- fallbacks: List[Tuple[Any, Dict[str, Any]]] = []
228
+ regular, fallbacks = [], []
229
+ for ik, im in impl_list:
230
+ tgt = _target_from_provider(im["factory"])
231
+ (fallbacks if _on_missing_meta(tgt) else regular).append((ik, im))
307
232
 
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]:
233
+ def pick(cands: List[Tuple[Any, dict]]) -> Optional[Any]:
316
234
  if not cands:
317
235
  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:
236
+ prims = [(ik, im) for ik, im in cands if _has_flag(_target_from_provider(im["factory"]), PRIMARY_FLAG)]
237
+ return prims[0][0] if prims else cands[0][0]
238
+
239
+ chosen = pick(regular) or pick(fallbacks)
240
+ if not chosen:
328
241
  continue
329
242
 
330
- factory = create_alias_provider(container, chosen_key)
243
+ factory = create_alias_provider(container, chosen)
331
244
  container.bind(base, factory, lazy=True)
332
245
 
pico_ioc/proxy.py CHANGED
@@ -1,11 +1,10 @@
1
- # pico_ioc/proxy.py
2
-
3
1
  from __future__ import annotations
2
+
3
+ import inspect
4
4
  from functools import lru_cache
5
5
  from typing import Any, Callable, Sequence
6
- import inspect
7
6
 
8
- from .interceptors import Invocation, dispatch, MethodInterceptor
7
+ from .interceptors import MethodCtx, MethodInterceptor, dispatch_method
9
8
 
10
9
  class ComponentProxy:
11
10
  def __init__(self, object_creator: Callable[[], Any]):
@@ -13,11 +12,11 @@ class ComponentProxy:
13
12
  object.__setattr__(self, "__real_object", None)
14
13
 
15
14
  def _get_real_object(self) -> Any:
16
- real_obj = object.__getattribute__(self, "__real_object")
17
- if real_obj is None:
18
- real_obj = object.__getattribute__(self, "_object_creator")()
19
- object.__setattr__(self, "__real_object", real_obj)
20
- return real_obj
15
+ real = object.__getattribute__(self, "__real_object")
16
+ if real is None:
17
+ real = object.__getattribute__(self, "_object_creator")()
18
+ object.__setattr__(self, "__real_object", real)
19
+ return real
21
20
 
22
21
  @property
23
22
  def __class__(self):
@@ -79,39 +78,38 @@ class ComponentProxy:
79
78
  def __call__(self, *args, **kwargs): return self._get_real_object()(*args, **kwargs)
80
79
  def __enter__(self): return self._get_real_object().__enter__()
81
80
  def __exit__(self, exc_type, exc_val, exc_tb): return self._get_real_object().__exit__(exc_type, exc_val, exc_tb)
82
-
81
+
83
82
  class IoCProxy:
84
- __slots__ = ("_target", "_interceptors")
83
+ __slots__ = ("_target", "_interceptors", "_container", "_request_key")
85
84
 
86
- def __init__(self, target: object, interceptors: Sequence[MethodInterceptor]):
85
+ def __init__(self, target: object, interceptors: Sequence[MethodInterceptor], container: Any = None, request_key: Any = None):
87
86
  self._target = target
88
87
  self._interceptors = tuple(interceptors)
88
+ self._container = container
89
+ self._request_key = request_key
89
90
 
90
91
  def __getattr__(self, name: str) -> Any:
91
92
  attr = getattr(self._target, name)
92
93
  if not callable(attr):
93
94
  return attr
94
95
  if hasattr(attr, "__get__"):
95
- fn = attr.__get__(self._target, type(self._target))
96
+ bound_fn = attr.__get__(self._target, type(self._target))
96
97
  else:
97
- fn = attr
98
-
98
+ bound_fn = attr
99
99
  @lru_cache(maxsize=None)
100
- def _wrap(bound_fn: Callable[..., Any]):
101
- if inspect.iscoroutinefunction(bound_fn):
100
+ def _wrap(fn: Callable[..., Any]):
101
+ if inspect.iscoroutinefunction(fn):
102
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)
103
+ ctx = MethodCtx(instance=self._target, cls=type(self._target), method=fn, name=name, args=args, kwargs=kwargs, container=self._container, request_key=self._request_key)
104
+ return await dispatch_method(self._interceptors, ctx)
106
105
  return aw
107
106
  else:
108
107
  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)
108
+ ctx = MethodCtx(instance=self._target, cls=type(self._target), method=fn, name=name, args=args, kwargs=kwargs, container=self._container, request_key=self._request_key)
109
+ res = dispatch_method(self._interceptors, ctx)
112
110
  if inspect.isawaitable(res):
113
111
  raise RuntimeError(f"Async interceptor on sync method: {name}")
114
112
  return res
115
113
  return sw
116
- return _wrap(fn)
114
+ return _wrap(bound_fn)
117
115
 
pico_ioc/resolver.py CHANGED
@@ -1,83 +1,60 @@
1
- # pico_ioc/resolver.py (Python 3.10+)
2
-
3
1
  from __future__ import annotations
2
+
4
3
  import inspect
5
- from typing import Any, Annotated, get_args, get_origin, get_type_hints, Callable
4
+ from typing import Any, Annotated, Callable, get_args, get_origin, get_type_hints
6
5
  from contextvars import ContextVar
7
6
 
8
-
9
7
  _path: ContextVar[list[tuple[str, str]]] = ContextVar("pico_resolve_path", default=[])
10
8
 
11
9
  def _get_hints(obj, owner_cls=None) -> dict:
12
- """type hints with include_extras=True and correct globals/locals."""
13
10
  mod = inspect.getmodule(obj)
14
11
  g = getattr(mod, "__dict__", {})
15
12
  l = vars(owner_cls) if owner_cls is not None else None
16
13
  return get_type_hints(obj, globalns=g, localns=l, include_extras=True)
17
14
 
18
-
19
15
  def _is_collection_hint(tp) -> bool:
20
- """True if tp is a list[...] or tuple[...]."""
21
16
  origin = get_origin(tp) or tp
22
17
  return origin in (list, tuple)
23
18
 
24
-
25
19
  def _base_and_qualifiers_from_hint(tp):
26
- """
27
- Extract (base, qualifiers, container_kind) from a collection hint.
28
- Supports list[T] / tuple[T] and Annotated[T, "qual1", ...].
29
- """
30
20
  origin = get_origin(tp) or tp
31
21
  args = get_args(tp) or ()
32
22
  container_kind = list if origin is list else tuple
33
-
34
23
  if not args:
35
24
  return (object, (), container_kind)
36
-
37
25
  inner = args[0]
38
26
  if get_origin(inner) is Annotated:
39
27
  base, *extras = get_args(inner)
40
28
  quals = tuple(a for a in extras if isinstance(a, str))
41
29
  return (base, quals, container_kind)
42
-
43
30
  return (inner, (), container_kind)
44
31
 
45
-
46
32
  class Resolver:
47
33
  def __init__(self, container, *, prefer_name_first: bool = True):
48
34
  self.c = container
49
35
  self._prefer_name_first = bool(prefer_name_first)
50
36
 
51
-
52
37
  def _resolve_dependencies_for_callable(self, fn: Callable, owner_cls: Any = None) -> dict:
53
38
  sig = inspect.signature(fn)
54
39
  hints = _get_hints(fn, owner_cls=owner_cls)
55
40
  kwargs = {}
56
-
57
41
  path_owner = getattr(owner_cls, "__name__", getattr(fn, "__qualname__", "callable"))
58
42
  if fn.__name__ == "__init__" and owner_cls:
59
43
  path_owner = f"{path_owner}.__init__"
60
-
61
44
  for name, param in sig.parameters.items():
62
45
  if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD) or name == "self":
63
46
  continue
64
-
65
47
  ann = hints.get(name, param.annotation)
66
48
  st = _path.get()
67
49
  _path.set(st + [(path_owner, name)])
68
50
  try:
69
- value = self._resolve_param(name, ann)
70
- kwargs[name] = value
51
+ kwargs[name] = self._resolve_param(name, ann)
71
52
  except NameError as e:
72
53
  if param.default is not inspect.Parameter.empty:
73
54
  _path.set(st)
74
55
  continue
75
-
76
- # If the error is already formatted with a chain, re-raise to preserve the full context.
77
56
  if "(required by" in str(e):
78
57
  raise
79
-
80
- # Otherwise, this is a fresh error; add the full chain for the first time.
81
58
  chain = " -> ".join(f"{owner}.{param}" for owner, param in _path.get())
82
59
  raise NameError(f"{e} (required by {chain})") from e
83
60
  finally:
@@ -87,29 +64,24 @@ class Resolver:
87
64
  return kwargs
88
65
 
89
66
  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)
67
+ ctor_kwargs = self._resolve_dependencies_for_callable(cls.__init__, owner_cls=cls)
68
+ return cls(**ctor_kwargs)
93
69
 
94
70
  def kwargs_for_callable(self, fn: Callable, *, owner_cls: Any = None) -> dict:
95
- """Resolves all keyword arguments for any callable."""
96
71
  return self._resolve_dependencies_for_callable(fn, owner_cls=owner_cls)
97
72
 
98
-
99
73
  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
74
+ try:
75
+ self.c._notify_resolve(key, ann, quals)
76
+ except Exception:
77
+ pass
103
78
 
104
79
  def _resolve_param(self, name: str, ann: Any):
105
- # Colecciones (list/tuple)
106
80
  if _is_collection_hint(ann):
107
- base, quals, container_kind = _base_and_qualifiers_from_hint(ann)
81
+ base, quals, kind = _base_and_qualifiers_from_hint(ann)
108
82
  self._notify_resolve(base, ann, quals)
109
83
  items = self.c._resolve_all_for_base(base, qualifiers=quals)
110
- return list(items) if container_kind is list else tuple(items)
111
-
112
- # Precedencias
84
+ return list(items) if kind is list else tuple(items)
113
85
  if self._prefer_name_first and self.c.has(name):
114
86
  self._notify_resolve(name, ann, ())
115
87
  return self.c.get(name)
@@ -124,6 +96,6 @@ class Resolver:
124
96
  if self.c.has(name):
125
97
  self._notify_resolve(name, ann, ())
126
98
  return self.c.get(name)
127
-
128
99
  missing = ann if ann is not inspect._empty else name
129
100
  raise NameError(f"No provider found for key {missing!r}")
101
+