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/__init__.py +28 -5
- pico_ioc/_state.py +60 -25
- pico_ioc/_version.py +1 -1
- pico_ioc/api.py +38 -56
- pico_ioc/builder.py +68 -100
- pico_ioc/config.py +332 -0
- pico_ioc/container.py +26 -44
- pico_ioc/decorators.py +15 -29
- pico_ioc/infra.py +196 -0
- pico_ioc/interceptors.py +59 -33
- pico_ioc/policy.py +102 -189
- pico_ioc/proxy.py +22 -24
- pico_ioc/resolver.py +12 -40
- pico_ioc/scanner.py +42 -67
- pico_ioc/scope.py +41 -0
- {pico_ioc-1.3.0.dist-info → pico_ioc-1.5.0.dist-info}/METADATA +15 -1
- pico_ioc-1.5.0.dist-info/RECORD +23 -0
- pico_ioc-1.3.0.dist-info/RECORD +0 -20
- {pico_ioc-1.3.0.dist-info → pico_ioc-1.5.0.dist-info}/WHEEL +0 -0
- {pico_ioc-1.3.0.dist-info → pico_ioc-1.5.0.dist-info}/licenses/LICENSE +0 -0
- {pico_ioc-1.3.0.dist-info → pico_ioc-1.5.0.dist-info}/top_level.txt +0 -0
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,
|
|
7
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
8
|
+
|
|
8
9
|
from .utils import create_alias_provider
|
|
9
|
-
from .decorators import
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
ON_MISSING_META,
|
|
13
|
-
)
|
|
10
|
+
from .decorators import CONDITIONAL_META, PRIMARY_FLAG, ON_MISSING_META
|
|
11
|
+
from . import _state
|
|
12
|
+
|
|
14
13
|
|
|
15
|
-
#
|
|
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
|
|
25
|
+
return cc
|
|
31
26
|
if first_func is None and inspect.isfunction(cc):
|
|
32
|
-
first_func = cc
|
|
27
|
+
first_func = cc
|
|
33
28
|
elif first_cls is None and inspect.isclass(cc):
|
|
34
|
-
first_cls = cc
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
|
75
|
-
|
|
76
|
-
if
|
|
77
|
-
return
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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", ()))
|
|
126
|
-
req_env = tuple(meta.get("require_env", ()))
|
|
127
|
-
pred = meta.get("predicate"
|
|
90
|
+
profs = tuple(meta.get("profiles", ()))
|
|
91
|
+
req_env = tuple(meta.get("require_env", ()))
|
|
92
|
+
pred = meta.get("predicate")
|
|
128
93
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
173
|
-
for prov_key, meta in list(container._providers.items()): # type: ignore
|
|
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
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
#
|
|
184
|
-
for prov_key, meta in list(container._providers.items()): # type: ignore
|
|
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
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
#
|
|
197
|
-
for base,
|
|
145
|
+
# bind highest priority candidate
|
|
146
|
+
for base, cands in defaults.items():
|
|
198
147
|
if container.has(base):
|
|
199
148
|
continue
|
|
200
|
-
|
|
201
|
-
chosen_key =
|
|
149
|
+
cands.sort(key=lambda t: t[0], reverse=True)
|
|
150
|
+
chosen_key = cands[0][1]
|
|
202
151
|
|
|
203
|
-
def
|
|
204
|
-
def
|
|
205
|
-
return container.get(
|
|
206
|
-
return
|
|
152
|
+
def _delegate(_k=chosen_key):
|
|
153
|
+
def _f():
|
|
154
|
+
return container.get(_k)
|
|
155
|
+
return _f
|
|
207
156
|
|
|
208
|
-
container.bind(base,
|
|
157
|
+
container.bind(base, _delegate(), lazy=True)
|
|
209
158
|
|
|
210
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
174
|
+
container._providers.pop(k, None) # type: ignore
|
|
229
175
|
|
|
230
176
|
|
|
231
177
|
def _collapse_identical_keys_preferring_primary(container) -> None:
|
|
232
|
-
"""
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
241
|
-
if
|
|
242
|
-
|
|
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
|
|
186
|
+
for base, entries in groups.items():
|
|
245
187
|
if not entries:
|
|
246
188
|
continue
|
|
247
|
-
|
|
248
189
|
if len(entries) == 1:
|
|
249
|
-
|
|
250
|
-
if (not container.has(base)) or (base !=
|
|
251
|
-
factory = create_alias_provider(container,
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
if
|
|
259
|
-
|
|
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
|
|
267
|
-
if kk !=
|
|
268
|
-
container._providers.pop(kk, None) # type: ignore
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
306
|
-
|
|
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
|
-
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
17
|
-
if
|
|
18
|
-
|
|
19
|
-
object.__setattr__(self, "__real_object",
|
|
20
|
-
return
|
|
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
|
-
|
|
96
|
+
bound_fn = attr.__get__(self._target, type(self._target))
|
|
96
97
|
else:
|
|
97
|
-
|
|
98
|
-
|
|
98
|
+
bound_fn = attr
|
|
99
99
|
@lru_cache(maxsize=None)
|
|
100
|
-
def _wrap(
|
|
101
|
-
if inspect.iscoroutinefunction(
|
|
100
|
+
def _wrap(fn: Callable[..., Any]):
|
|
101
|
+
if inspect.iscoroutinefunction(fn):
|
|
102
102
|
async def aw(*args, **kwargs):
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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,
|
|
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
|
|
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
|
+
|