pico-ioc 1.4.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 +21 -11
- pico_ioc/_version.py +1 -1
- pico_ioc/api.py +3 -2
- pico_ioc/builder.py +31 -115
- pico_ioc/container.py +18 -55
- pico_ioc/decorators.py +11 -49
- pico_ioc/infra.py +196 -0
- pico_ioc/interceptors.py +59 -39
- pico_ioc/proxy.py +9 -23
- pico_ioc/resolver.py +4 -35
- pico_ioc/scanner.py +30 -55
- pico_ioc/scope.py +2 -7
- {pico_ioc-1.4.0.dist-info → pico_ioc-1.5.0.dist-info}/METADATA +10 -2
- pico_ioc-1.5.0.dist-info/RECORD +23 -0
- pico_ioc-1.4.0.dist-info/RECORD +0 -22
- {pico_ioc-1.4.0.dist-info → pico_ioc-1.5.0.dist-info}/WHEEL +0 -0
- {pico_ioc-1.4.0.dist-info → pico_ioc-1.5.0.dist-info}/licenses/LICENSE +0 -0
- {pico_ioc-1.4.0.dist-info → pico_ioc-1.5.0.dist-info}/top_level.txt +0 -0
pico_ioc/__init__.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# pico_ioc/__init__.py
|
|
2
1
|
try:
|
|
3
2
|
from ._version import __version__
|
|
4
3
|
except Exception:
|
|
@@ -9,17 +8,24 @@ from .scope import ScopedContainer
|
|
|
9
8
|
from .decorators import (
|
|
10
9
|
component, factory_component, provides, plugin,
|
|
11
10
|
Qualifier, qualifier,
|
|
12
|
-
on_missing, primary, conditional,
|
|
11
|
+
on_missing, primary, conditional, infrastructure,
|
|
13
12
|
)
|
|
14
13
|
from .plugins import PicoPlugin
|
|
15
14
|
from .resolver import Resolver
|
|
16
15
|
from .api import init, reset, scope, container_fingerprint
|
|
17
16
|
from .proxy import ComponentProxy, IoCProxy
|
|
18
|
-
from .interceptors import
|
|
17
|
+
from .interceptors import (
|
|
18
|
+
MethodInterceptor,
|
|
19
|
+
ContainerInterceptor,
|
|
20
|
+
MethodCtx,
|
|
21
|
+
ResolveCtx,
|
|
22
|
+
CreateCtx,
|
|
23
|
+
)
|
|
19
24
|
from .config import (
|
|
20
25
|
config_component, EnvSource, FileSource,
|
|
21
26
|
Env, File, Path, Value,
|
|
22
27
|
)
|
|
28
|
+
from .infra import Infra, Select
|
|
23
29
|
|
|
24
30
|
__all__ = [
|
|
25
31
|
"__version__",
|
|
@@ -28,9 +34,11 @@ __all__ = [
|
|
|
28
34
|
"PicoPlugin",
|
|
29
35
|
"ComponentProxy",
|
|
30
36
|
"IoCProxy",
|
|
31
|
-
"Invocation",
|
|
32
37
|
"MethodInterceptor",
|
|
33
38
|
"ContainerInterceptor",
|
|
39
|
+
"MethodCtx",
|
|
40
|
+
"ResolveCtx",
|
|
41
|
+
"CreateCtx",
|
|
34
42
|
"init",
|
|
35
43
|
"scope",
|
|
36
44
|
"reset",
|
|
@@ -44,15 +52,17 @@ __all__ = [
|
|
|
44
52
|
"on_missing",
|
|
45
53
|
"primary",
|
|
46
54
|
"conditional",
|
|
47
|
-
"
|
|
55
|
+
"infrastructure",
|
|
48
56
|
"Resolver",
|
|
49
57
|
"ScopedContainer",
|
|
50
|
-
"config_component",
|
|
51
|
-
"EnvSource",
|
|
52
|
-
"FileSource",
|
|
53
|
-
"Env",
|
|
54
|
-
"File",
|
|
55
|
-
"Path",
|
|
58
|
+
"config_component",
|
|
59
|
+
"EnvSource",
|
|
60
|
+
"FileSource",
|
|
61
|
+
"Env",
|
|
62
|
+
"File",
|
|
63
|
+
"Path",
|
|
56
64
|
"Value",
|
|
65
|
+
"Infra",
|
|
66
|
+
"Select",
|
|
57
67
|
]
|
|
58
68
|
|
pico_ioc/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = '1.
|
|
1
|
+
__version__ = '1.5.0'
|
pico_ioc/api.py
CHANGED
|
@@ -129,7 +129,6 @@ def _get_caller_module_name() -> Optional[str]:
|
|
|
129
129
|
pass
|
|
130
130
|
return None
|
|
131
131
|
|
|
132
|
-
# ---------------- public API ----------------
|
|
133
132
|
def init(
|
|
134
133
|
root_package, *, profiles: Optional[list[str]] = None, exclude: Optional[Callable[[str], bool]] = None,
|
|
135
134
|
auto_exclude_caller: bool = True, plugins: Tuple[PicoPlugin, ...] = (), reuse: bool = True,
|
|
@@ -137,6 +136,8 @@ def init(
|
|
|
137
136
|
auto_scan_exclude: Optional[Callable[[str], bool]] = None, strict_autoscan: bool = False,
|
|
138
137
|
config: Sequence[ConfigSource] = (),
|
|
139
138
|
) -> PicoContainer:
|
|
139
|
+
if _state._scanning.get():
|
|
140
|
+
logging.info("re-entrant container access during scan")
|
|
140
141
|
root_name = root_package if isinstance(root_package, str) else getattr(root_package, "__name__", None)
|
|
141
142
|
fp = _make_fingerprint_from_signature(locals())
|
|
142
143
|
|
|
@@ -169,11 +170,11 @@ def init(
|
|
|
169
170
|
|
|
170
171
|
container = builder.build()
|
|
171
172
|
|
|
172
|
-
# Activate new context atomically
|
|
173
173
|
new_ctx = _state.ContainerContext(container=container, fingerprint=fp, root_name=root_name)
|
|
174
174
|
_state.set_context(new_ctx)
|
|
175
175
|
return container
|
|
176
176
|
|
|
177
|
+
|
|
177
178
|
def scope(
|
|
178
179
|
*, modules: Iterable[Any] = (), roots: Iterable[type] = (), profiles: Optional[list[str]] = None,
|
|
179
180
|
overrides: Optional[Dict[Any, Any]] = None, base: Optional[PicoContainer] = None,
|
pico_ioc/builder.py
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
# src/pico_ioc/builder.py
|
|
2
1
|
from __future__ import annotations
|
|
3
|
-
|
|
4
2
|
import inspect as _inspect
|
|
5
3
|
import logging
|
|
6
4
|
import os
|
|
@@ -17,8 +15,6 @@ from . import _state
|
|
|
17
15
|
from .config import ConfigRegistry
|
|
18
16
|
|
|
19
17
|
class PicoContainerBuilder:
|
|
20
|
-
"""Configures and builds a PicoContainer. Does not touch global context."""
|
|
21
|
-
|
|
22
18
|
def __init__(self):
|
|
23
19
|
self._scan_plan: List[Tuple[Any, Optional[Callable[[str], bool]], Tuple[PicoPlugin, ...]]] = []
|
|
24
20
|
self._overrides: Dict[Any, Any] = {}
|
|
@@ -28,12 +24,9 @@ class PicoContainerBuilder:
|
|
|
28
24
|
self._exclude_tags: Optional[set[str]] = None
|
|
29
25
|
self._roots: Iterable[type] = ()
|
|
30
26
|
self._providers: Dict[Any, Dict] = {}
|
|
31
|
-
self._interceptor_decls: List[Tuple[Any, dict]] = []
|
|
32
27
|
self._eager: bool = True
|
|
33
28
|
self._config_registry: ConfigRegistry | None = None
|
|
34
29
|
|
|
35
|
-
# -------- fluent config --------
|
|
36
|
-
|
|
37
30
|
def with_config(self, registry: ConfigRegistry) -> "PicoContainerBuilder":
|
|
38
31
|
self._config_registry = registry
|
|
39
32
|
return self
|
|
@@ -67,59 +60,39 @@ class PicoContainerBuilder:
|
|
|
67
60
|
self._eager = bool(eager)
|
|
68
61
|
return self
|
|
69
62
|
|
|
70
|
-
# -------- build --------
|
|
71
|
-
|
|
72
63
|
def build(self) -> PicoContainer:
|
|
73
|
-
"""Build and return a fully configured container."""
|
|
74
64
|
requested_profiles = _resolve_profiles(self._profiles)
|
|
75
|
-
|
|
76
65
|
container = PicoContainer(providers=self._providers)
|
|
77
66
|
container._active_profiles = tuple(requested_profiles)
|
|
78
67
|
setattr(container, "_config_registry", self._config_registry)
|
|
79
|
-
|
|
68
|
+
all_infras: list[tuple[type, dict]] = []
|
|
80
69
|
for pkg, exclude, scan_plugins in self._scan_plan:
|
|
81
70
|
with _state.scanning_flag():
|
|
82
|
-
c, f,
|
|
71
|
+
c, f, infra_decls = scan_and_configure(pkg, container, exclude=exclude, plugins=scan_plugins)
|
|
83
72
|
logging.info("Scanned '%s' (components: %d, factories: %d)", getattr(pkg, "__name__", pkg), c, f)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
_activate_and_build_interceptors(
|
|
87
|
-
container=container,
|
|
88
|
-
interceptor_decls=self._interceptor_decls,
|
|
89
|
-
profiles=requested_profiles,
|
|
90
|
-
)
|
|
91
|
-
|
|
73
|
+
all_infras.extend(infra_decls)
|
|
74
|
+
_run_infrastructure(container=container, infra_decls=all_infras, profiles=requested_profiles)
|
|
92
75
|
binder = container.binder()
|
|
93
|
-
|
|
94
76
|
if self._overrides:
|
|
95
77
|
_apply_overrides(container, self._overrides)
|
|
96
|
-
|
|
97
78
|
run_plugin_hook(self._plugins, "after_bind", container, binder)
|
|
98
79
|
run_plugin_hook(self._plugins, "before_eager", container, binder)
|
|
99
|
-
|
|
100
80
|
apply_policy(container, profiles=requested_profiles)
|
|
101
81
|
_filter_by_tags(container, self._include_tags, self._exclude_tags)
|
|
102
|
-
|
|
103
82
|
if self._roots:
|
|
104
83
|
_restrict_to_subgraph(container, self._roots, self._overrides)
|
|
105
|
-
|
|
106
84
|
run_plugin_hook(self._plugins, "after_ready", container, binder)
|
|
107
|
-
|
|
108
85
|
if self._eager:
|
|
109
86
|
container.eager_instantiate_all()
|
|
110
87
|
logging.info("Container configured and ready.")
|
|
111
88
|
return container
|
|
112
89
|
|
|
113
|
-
|
|
114
|
-
# ---------------- helpers ----------------
|
|
115
|
-
|
|
116
90
|
def _resolve_profiles(profiles: Optional[List[str]]) -> List[str]:
|
|
117
91
|
if profiles is not None:
|
|
118
92
|
return list(profiles)
|
|
119
93
|
env_val = os.getenv("PICO_PROFILE", "")
|
|
120
94
|
return [p.strip() for p in env_val.split(",") if p.strip()]
|
|
121
95
|
|
|
122
|
-
|
|
123
96
|
def _as_provider(val):
|
|
124
97
|
if isinstance(val, tuple) and len(val) == 2 and callable(val[0]) and isinstance(val[1], bool):
|
|
125
98
|
return val[0], val[1]
|
|
@@ -127,17 +100,14 @@ def _as_provider(val):
|
|
|
127
100
|
return val, False
|
|
128
101
|
return (lambda v=val: v), False
|
|
129
102
|
|
|
130
|
-
|
|
131
103
|
def _apply_overrides(container: PicoContainer, overrides: Dict[Any, Any]) -> None:
|
|
132
104
|
for key, val in overrides.items():
|
|
133
105
|
provider, lazy = _as_provider(val)
|
|
134
106
|
container.bind(key, provider, lazy=lazy)
|
|
135
107
|
|
|
136
|
-
|
|
137
108
|
def _filter_by_tags(container: PicoContainer, include_tags: Optional[set[str]], exclude_tags: Optional[set[str]]) -> None:
|
|
138
109
|
if not include_tags and not exclude_tags:
|
|
139
110
|
return
|
|
140
|
-
|
|
141
111
|
def _tag_ok(meta: dict) -> bool:
|
|
142
112
|
tags = set(meta.get("tags", ()))
|
|
143
113
|
if include_tags and not tags.intersection(include_tags):
|
|
@@ -145,14 +115,11 @@ def _filter_by_tags(container: PicoContainer, include_tags: Optional[set[str]],
|
|
|
145
115
|
if exclude_tags and tags.intersection(exclude_tags):
|
|
146
116
|
return False
|
|
147
117
|
return True
|
|
148
|
-
|
|
149
118
|
container._providers = {k: v for k, v in container._providers.items() if _tag_ok(v)}
|
|
150
119
|
|
|
151
|
-
|
|
152
120
|
def _compute_allowed_subgraph(container: PicoContainer, roots: Iterable[type]) -> set:
|
|
153
121
|
allowed: set[Any] = set(roots)
|
|
154
122
|
stack = list(roots or ())
|
|
155
|
-
|
|
156
123
|
def _add_impls_for_base(base_t):
|
|
157
124
|
for prov_key, meta in container._providers.items():
|
|
158
125
|
cls = prov_key if isinstance(prov_key, type) else None
|
|
@@ -160,23 +127,19 @@ def _compute_allowed_subgraph(container: PicoContainer, roots: Iterable[type]) -
|
|
|
160
127
|
if prov_key not in allowed:
|
|
161
128
|
allowed.add(prov_key)
|
|
162
129
|
stack.append(prov_key)
|
|
163
|
-
|
|
164
130
|
while stack:
|
|
165
131
|
k = stack.pop()
|
|
166
132
|
allowed.add(k)
|
|
167
133
|
if isinstance(k, type):
|
|
168
134
|
_add_impls_for_base(k)
|
|
169
|
-
|
|
170
135
|
cls = k if isinstance(k, type) else None
|
|
171
136
|
if cls is None or not container.has(k):
|
|
172
137
|
continue
|
|
173
|
-
|
|
174
138
|
try:
|
|
175
139
|
sig = _inspect.signature(cls.__init__)
|
|
176
140
|
hints = _get_hints(cls.__init__, owner_cls=cls)
|
|
177
141
|
except Exception:
|
|
178
142
|
continue
|
|
179
|
-
|
|
180
143
|
for pname, param in sig.parameters.items():
|
|
181
144
|
if pname == "self":
|
|
182
145
|
continue
|
|
@@ -196,99 +159,52 @@ def _compute_allowed_subgraph(container: PicoContainer, roots: Iterable[type]) -
|
|
|
196
159
|
stack.append(pname)
|
|
197
160
|
return allowed
|
|
198
161
|
|
|
199
|
-
|
|
200
162
|
def _restrict_to_subgraph(container: PicoContainer, roots: Iterable[type], overrides: Optional[Dict[Any, Any]]) -> None:
|
|
201
163
|
allowed = _compute_allowed_subgraph(container, roots)
|
|
202
164
|
keep_keys: set[Any] = allowed | (set(overrides.keys()) if overrides else set())
|
|
203
165
|
container._providers = {k: v for k, v in container._providers.items() if k in keep_keys}
|
|
204
166
|
|
|
205
|
-
|
|
206
|
-
def
|
|
207
|
-
*, container: PicoContainer, interceptor_decls: List[Tuple[Any, dict]], profiles: List[str],
|
|
208
|
-
) -> None:
|
|
209
|
-
resolver = Resolver(container)
|
|
210
|
-
active: List[Tuple[int, str, str, Any]] = []
|
|
211
|
-
activated_method_names: List[str] = []
|
|
212
|
-
activated_container_names: List[str] = []
|
|
213
|
-
skipped_debug: List[str] = []
|
|
214
|
-
|
|
215
|
-
def _interceptor_meta_active(meta: dict) -> bool:
|
|
167
|
+
def _run_infrastructure(*, container: PicoContainer, infra_decls: List[tuple[type, dict]], profiles: List[str]) -> None:
|
|
168
|
+
def _active(meta: dict) -> bool:
|
|
216
169
|
profs = tuple(meta.get("profiles", ())) or ()
|
|
217
170
|
if profs and (not profiles or not any(p in profs for p in profiles)):
|
|
218
171
|
return False
|
|
219
172
|
req_env = tuple(meta.get("require_env", ())) or ()
|
|
220
|
-
if req_env
|
|
221
|
-
|
|
173
|
+
if req_env:
|
|
174
|
+
import os
|
|
175
|
+
if not all(os.getenv(k) not in (None, "") for k in req_env):
|
|
176
|
+
return False
|
|
222
177
|
pred = meta.get("predicate", None)
|
|
223
178
|
if callable(pred):
|
|
224
179
|
try:
|
|
225
180
|
if not bool(pred()):
|
|
226
181
|
return False
|
|
227
182
|
except Exception:
|
|
228
|
-
logging.exception("Interceptor predicate failed; skipping")
|
|
229
183
|
return False
|
|
230
184
|
return True
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
for raw_obj, meta in interceptor_decls:
|
|
238
|
-
owner_cls, obj = (raw_obj[0], raw_obj[1]) if isinstance(raw_obj, tuple) and len(raw_obj) == 2 else (None, raw_obj)
|
|
239
|
-
qn = getattr(obj, "__qualname__", repr(obj))
|
|
240
|
-
|
|
241
|
-
if not _conditional_active(obj, profiles=profiles) or not _interceptor_meta_active(meta):
|
|
242
|
-
skipped_debug.append(f"skip:{qn}")
|
|
185
|
+
from .resolver import Resolver
|
|
186
|
+
from .infra import Infra
|
|
187
|
+
resolver = Resolver(container)
|
|
188
|
+
active_infras: List[tuple[int, type]] = []
|
|
189
|
+
for cls, meta in infra_decls:
|
|
190
|
+
if not _active(meta):
|
|
243
191
|
continue
|
|
244
|
-
|
|
192
|
+
order = int(meta.get("order", 0))
|
|
193
|
+
active_infras.append((order, cls))
|
|
194
|
+
active_infras.sort(key=lambda t: (t[0], getattr(t[1], "__qualname__", "")))
|
|
195
|
+
for _ord, cls in active_infras:
|
|
245
196
|
try:
|
|
246
|
-
|
|
247
|
-
inst = resolver.create_instance(obj)
|
|
248
|
-
elif owner_cls is not None:
|
|
249
|
-
owner_inst = resolver.create_instance(owner_cls)
|
|
250
|
-
bound = obj.__get__(owner_inst, owner_cls)
|
|
251
|
-
kwargs = resolver.kwargs_for_callable(bound, owner_cls=owner_cls)
|
|
252
|
-
inst = bound(**kwargs)
|
|
253
|
-
else:
|
|
254
|
-
kwargs = resolver.kwargs_for_callable(obj, owner_cls=None)
|
|
255
|
-
inst = obj(**kwargs)
|
|
197
|
+
inst = resolver.create_instance(cls)
|
|
256
198
|
except Exception:
|
|
257
|
-
logging
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
kind = meta.get("kind", "method")
|
|
261
|
-
if kind == "method" and not callable(inst):
|
|
262
|
-
logging.error("Interceptor %s is not valid for kind %s; skipping", qn, kind)
|
|
263
|
-
continue
|
|
264
|
-
if kind == "container" and not _looks_like_container_interceptor(inst):
|
|
265
|
-
logging.error("Container interceptor %s lacks required methods; skipping", qn)
|
|
199
|
+
import logging
|
|
200
|
+
logging.exception("Failed to construct infrastructure %r", cls)
|
|
266
201
|
continue
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
container.add_container_interceptor(inst) # type: ignore[arg-type]
|
|
276
|
-
activated_container_names.append(_qn)
|
|
277
|
-
else:
|
|
278
|
-
container.add_method_interceptor(inst) # type: ignore[arg-type]
|
|
279
|
-
activated_method_names.append(_qn)
|
|
280
|
-
|
|
281
|
-
if activated_method_names or activated_container_names:
|
|
282
|
-
logging.info(
|
|
283
|
-
"Interceptors activated: method=%d, container=%d",
|
|
284
|
-
len(activated_method_names),
|
|
285
|
-
len(activated_container_names),
|
|
286
|
-
)
|
|
287
|
-
logging.debug(
|
|
288
|
-
"Activated method=%s; Activated container=%s",
|
|
289
|
-
", ".join(activated_method_names) or "-",
|
|
290
|
-
", ".join(activated_container_names) or "-",
|
|
291
|
-
)
|
|
292
|
-
if skipped_debug:
|
|
293
|
-
logging.debug("Skipped interceptors: %s", ", ".join(skipped_debug))
|
|
202
|
+
infra = Infra(container=container, profiles=tuple(profiles))
|
|
203
|
+
fn = getattr(inst, "configure", None)
|
|
204
|
+
if callable(fn):
|
|
205
|
+
try:
|
|
206
|
+
fn(infra)
|
|
207
|
+
except Exception:
|
|
208
|
+
import logging
|
|
209
|
+
logging.exception("Infrastructure configure() failed for %r", cls)
|
|
294
210
|
|
pico_ioc/container.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# src/pico_ioc/container.py
|
|
2
1
|
from __future__ import annotations
|
|
3
2
|
|
|
4
3
|
import inspect
|
|
@@ -6,11 +5,10 @@ from typing import Any, Dict, get_origin, get_args, Annotated
|
|
|
6
5
|
import typing as _t
|
|
7
6
|
|
|
8
7
|
from .proxy import IoCProxy
|
|
9
|
-
from .interceptors import MethodInterceptor, ContainerInterceptor
|
|
8
|
+
from .interceptors import MethodInterceptor, ContainerInterceptor, MethodCtx, ResolveCtx, CreateCtx, run_resolve_chain, run_create_chain
|
|
10
9
|
from .decorators import QUALIFIERS_KEY
|
|
11
10
|
from . import _state
|
|
12
11
|
|
|
13
|
-
|
|
14
12
|
class Binder:
|
|
15
13
|
def __init__(self, container: PicoContainer):
|
|
16
14
|
self._c = container
|
|
@@ -24,7 +22,6 @@ class Binder:
|
|
|
24
22
|
def get(self, key: Any):
|
|
25
23
|
return self._c.get(key)
|
|
26
24
|
|
|
27
|
-
|
|
28
25
|
class PicoContainer:
|
|
29
26
|
def __init__(self, providers: Dict[Any, Dict[str, Any]] | None = None):
|
|
30
27
|
self._providers = providers or {}
|
|
@@ -33,14 +30,9 @@ class PicoContainer:
|
|
|
33
30
|
self._container_interceptors: tuple[ContainerInterceptor, ...] = ()
|
|
34
31
|
self._active_profiles: tuple[str, ...] = ()
|
|
35
32
|
self._seen_interceptor_types: set[type] = set()
|
|
36
|
-
|
|
37
|
-
# --- interceptors ---
|
|
33
|
+
self._method_cap: int | None = None
|
|
38
34
|
|
|
39
35
|
def add_method_interceptor(self, it: MethodInterceptor) -> None:
|
|
40
|
-
t = type(it)
|
|
41
|
-
if t in self._seen_interceptor_types:
|
|
42
|
-
return
|
|
43
|
-
self._seen_interceptor_types.add(t)
|
|
44
36
|
self._method_interceptors = self._method_interceptors + (it,)
|
|
45
37
|
|
|
46
38
|
def add_container_interceptor(self, it: ContainerInterceptor) -> None:
|
|
@@ -50,7 +42,8 @@ class PicoContainer:
|
|
|
50
42
|
self._seen_interceptor_types.add(t)
|
|
51
43
|
self._container_interceptors = self._container_interceptors + (it,)
|
|
52
44
|
|
|
53
|
-
|
|
45
|
+
def set_method_cap(self, n: int | None) -> None:
|
|
46
|
+
self._method_cap = (int(n) if n is not None else None)
|
|
54
47
|
|
|
55
48
|
def binder(self) -> Binder:
|
|
56
49
|
return Binder(self)
|
|
@@ -66,65 +59,44 @@ class PicoContainer:
|
|
|
66
59
|
meta["tags"] = tuple(tags) if tags else ()
|
|
67
60
|
self._providers[key] = meta
|
|
68
61
|
|
|
69
|
-
# --- resolution ---
|
|
70
|
-
|
|
71
62
|
def has(self, key: Any) -> bool:
|
|
72
63
|
return key in self._providers
|
|
73
64
|
|
|
65
|
+
def _notify_resolve(self, key: Any, ann: Any, quals: tuple[str, ...] | tuple()):
|
|
66
|
+
ctx = ResolveCtx(key=key, qualifiers={q: True for q in quals or ()}, requested_by=None, profiles=self._active_profiles)
|
|
67
|
+
run_resolve_chain(self._container_interceptors, ctx)
|
|
68
|
+
|
|
74
69
|
def get(self, key: Any):
|
|
75
70
|
if _state._scanning.get() and not _state._resolving.get():
|
|
76
71
|
raise RuntimeError("re-entrant container access during scan")
|
|
77
|
-
|
|
78
72
|
prov = self._providers.get(key)
|
|
79
73
|
if prov is None:
|
|
80
74
|
raise NameError(f"No provider found for key {key!r}")
|
|
81
|
-
|
|
82
75
|
if key in self._singletons:
|
|
83
76
|
return self._singletons[key]
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
except Exception:
|
|
89
|
-
pass
|
|
90
|
-
|
|
77
|
+
def base_provider():
|
|
78
|
+
return prov["factory"]()
|
|
79
|
+
cls = key if isinstance(key, type) else None
|
|
80
|
+
ctx = CreateCtx(key=key, component=cls, provider=base_provider, profiles=self._active_profiles)
|
|
91
81
|
tok = _state._resolving.set(True)
|
|
92
82
|
try:
|
|
93
|
-
|
|
94
|
-
instance = prov["factory"]()
|
|
95
|
-
except BaseException as exc:
|
|
96
|
-
for ci in self._container_interceptors:
|
|
97
|
-
try:
|
|
98
|
-
ci.on_exception(key, exc)
|
|
99
|
-
except Exception:
|
|
100
|
-
pass
|
|
101
|
-
raise
|
|
83
|
+
instance = run_create_chain(self._container_interceptors, ctx)
|
|
102
84
|
finally:
|
|
103
85
|
_state._resolving.reset(tok)
|
|
104
|
-
|
|
105
86
|
if self._method_interceptors and not isinstance(instance, IoCProxy):
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if maybe is not None:
|
|
112
|
-
instance = maybe
|
|
113
|
-
except Exception:
|
|
114
|
-
pass
|
|
115
|
-
|
|
87
|
+
chain = self._method_interceptors
|
|
88
|
+
cap = getattr(self, "_method_cap", None)
|
|
89
|
+
if isinstance(cap, int) and cap >= 0:
|
|
90
|
+
chain = chain[:cap]
|
|
91
|
+
instance = IoCProxy(instance, chain, container=self, request_key=key)
|
|
116
92
|
self._singletons[key] = instance
|
|
117
93
|
return instance
|
|
118
94
|
|
|
119
|
-
# --- lifecycle ---
|
|
120
|
-
|
|
121
95
|
def eager_instantiate_all(self):
|
|
122
96
|
for key, prov in list(self._providers.items()):
|
|
123
97
|
if not prov["lazy"]:
|
|
124
98
|
self.get(key)
|
|
125
99
|
|
|
126
|
-
# --- helpers for multiples ---
|
|
127
|
-
|
|
128
100
|
def get_all(self, base_type: Any):
|
|
129
101
|
return tuple(self._resolve_all_for_base(base_type, qualifiers=()))
|
|
130
102
|
|
|
@@ -149,20 +121,15 @@ class PicoContainer:
|
|
|
149
121
|
def get_providers(self) -> Dict[Any, Dict]:
|
|
150
122
|
return self._providers.copy()
|
|
151
123
|
|
|
152
|
-
|
|
153
|
-
# --- compatibility helpers ---
|
|
154
|
-
|
|
155
124
|
def _is_protocol(t) -> bool:
|
|
156
125
|
return getattr(t, "_is_protocol", False) is True
|
|
157
126
|
|
|
158
|
-
|
|
159
127
|
def _is_compatible(cls, base) -> bool:
|
|
160
128
|
try:
|
|
161
129
|
if isinstance(base, type) and issubclass(cls, base):
|
|
162
130
|
return True
|
|
163
131
|
except TypeError:
|
|
164
132
|
pass
|
|
165
|
-
|
|
166
133
|
if _is_protocol(base):
|
|
167
134
|
names = set(getattr(base, "__annotations__", {}).keys())
|
|
168
135
|
names.update(n for n in getattr(base, "__dict__", {}).keys() if not n.startswith("_"))
|
|
@@ -172,22 +139,18 @@ def _is_compatible(cls, base) -> bool:
|
|
|
172
139
|
if not hasattr(cls, n):
|
|
173
140
|
return False
|
|
174
141
|
return True
|
|
175
|
-
|
|
176
142
|
return False
|
|
177
143
|
|
|
178
|
-
|
|
179
144
|
def _requires_collection_of_base(cls, base) -> bool:
|
|
180
145
|
try:
|
|
181
146
|
sig = inspect.signature(cls.__init__)
|
|
182
147
|
except Exception:
|
|
183
148
|
return False
|
|
184
|
-
|
|
185
149
|
try:
|
|
186
150
|
from .resolver import _get_hints
|
|
187
151
|
hints = _get_hints(cls.__init__, owner_cls=cls)
|
|
188
152
|
except Exception:
|
|
189
153
|
hints = {}
|
|
190
|
-
|
|
191
154
|
for name, param in sig.parameters.items():
|
|
192
155
|
if name == "self":
|
|
193
156
|
continue
|