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/__init__.py
CHANGED
|
@@ -1,20 +1,31 @@
|
|
|
1
|
-
# pico_ioc/__init__.py
|
|
2
1
|
try:
|
|
3
2
|
from ._version import __version__
|
|
4
3
|
except Exception:
|
|
5
4
|
__version__ = "0.0.0"
|
|
6
5
|
|
|
7
6
|
from .container import PicoContainer, Binder
|
|
7
|
+
from .scope import ScopedContainer
|
|
8
8
|
from .decorators import (
|
|
9
9
|
component, factory_component, provides, plugin,
|
|
10
10
|
Qualifier, qualifier,
|
|
11
|
-
on_missing, primary, conditional,
|
|
11
|
+
on_missing, primary, conditional, infrastructure,
|
|
12
12
|
)
|
|
13
13
|
from .plugins import PicoPlugin
|
|
14
14
|
from .resolver import Resolver
|
|
15
15
|
from .api import init, reset, scope, container_fingerprint
|
|
16
16
|
from .proxy import ComponentProxy, IoCProxy
|
|
17
|
-
from .interceptors import
|
|
17
|
+
from .interceptors import (
|
|
18
|
+
MethodInterceptor,
|
|
19
|
+
ContainerInterceptor,
|
|
20
|
+
MethodCtx,
|
|
21
|
+
ResolveCtx,
|
|
22
|
+
CreateCtx,
|
|
23
|
+
)
|
|
24
|
+
from .config import (
|
|
25
|
+
config_component, EnvSource, FileSource,
|
|
26
|
+
Env, File, Path, Value,
|
|
27
|
+
)
|
|
28
|
+
from .infra import Infra, Select
|
|
18
29
|
|
|
19
30
|
__all__ = [
|
|
20
31
|
"__version__",
|
|
@@ -23,9 +34,11 @@ __all__ = [
|
|
|
23
34
|
"PicoPlugin",
|
|
24
35
|
"ComponentProxy",
|
|
25
36
|
"IoCProxy",
|
|
26
|
-
"Invocation",
|
|
27
37
|
"MethodInterceptor",
|
|
28
38
|
"ContainerInterceptor",
|
|
39
|
+
"MethodCtx",
|
|
40
|
+
"ResolveCtx",
|
|
41
|
+
"CreateCtx",
|
|
29
42
|
"init",
|
|
30
43
|
"scope",
|
|
31
44
|
"reset",
|
|
@@ -39,7 +52,17 @@ __all__ = [
|
|
|
39
52
|
"on_missing",
|
|
40
53
|
"primary",
|
|
41
54
|
"conditional",
|
|
42
|
-
"
|
|
55
|
+
"infrastructure",
|
|
43
56
|
"Resolver",
|
|
57
|
+
"ScopedContainer",
|
|
58
|
+
"config_component",
|
|
59
|
+
"EnvSource",
|
|
60
|
+
"FileSource",
|
|
61
|
+
"Env",
|
|
62
|
+
"File",
|
|
63
|
+
"Path",
|
|
64
|
+
"Value",
|
|
65
|
+
"Infra",
|
|
66
|
+
"Select",
|
|
44
67
|
]
|
|
45
68
|
|
pico_ioc/_state.py
CHANGED
|
@@ -1,40 +1,75 @@
|
|
|
1
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from threading import RLock
|
|
2
5
|
from contextvars import ContextVar
|
|
3
|
-
from typing import Optional
|
|
4
6
|
from contextlib import contextmanager
|
|
7
|
+
from typing import Optional, TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
# Type-only import to avoid cycles
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .container import PicoContainer
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# ---- Task/process context for the active container ----
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, slots=True)
|
|
17
|
+
class ContainerContext:
|
|
18
|
+
"""Immutable snapshot for the active container state."""
|
|
19
|
+
container: "PicoContainer"
|
|
20
|
+
fingerprint: tuple
|
|
21
|
+
root_name: Optional[str]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Process-wide fallback (for non-async code) guarded by a lock
|
|
25
|
+
_lock = RLock()
|
|
26
|
+
_current_context: Optional[ContainerContext] = None
|
|
27
|
+
|
|
28
|
+
# Task-local context (for async isolation)
|
|
29
|
+
_ctxvar: ContextVar[Optional[ContainerContext]] = ContextVar("pico_ioc_ctx", default=None)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_context() -> Optional[ContainerContext]:
|
|
33
|
+
"""Return the current context (task-local first, then process-global)."""
|
|
34
|
+
ctx = _ctxvar.get()
|
|
35
|
+
return ctx if ctx is not None else _current_context
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def set_context(ctx: Optional[ContainerContext]) -> None:
|
|
39
|
+
"""Atomically set both task-local and process-global context."""
|
|
40
|
+
with _lock:
|
|
41
|
+
_ctxvar.set(ctx)
|
|
42
|
+
globals()["_current_context"] = ctx
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Optional compatibility helpers (only used by legacy API paths)
|
|
46
|
+
def get_fingerprint() -> Optional[tuple]:
|
|
47
|
+
ctx = get_context()
|
|
48
|
+
return ctx.fingerprint if ctx else None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def set_fingerprint(fp: Optional[tuple]) -> None:
|
|
52
|
+
"""Compatibility shim: setting None clears the active context."""
|
|
53
|
+
if fp is None:
|
|
54
|
+
set_context(None)
|
|
55
|
+
return
|
|
56
|
+
ctx = get_context()
|
|
57
|
+
if ctx is not None:
|
|
58
|
+
set_context(ContainerContext(container=ctx.container, fingerprint=fp, root_name=ctx.root_name))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ---- Scan/resolve guards (kept as-is) ----
|
|
5
62
|
|
|
6
63
|
_scanning: ContextVar[bool] = ContextVar("pico_scanning", default=False)
|
|
7
64
|
_resolving: ContextVar[bool] = ContextVar("pico_resolving", default=False)
|
|
8
65
|
|
|
9
|
-
_container = None
|
|
10
|
-
_root_name: Optional[str] = None
|
|
11
|
-
_fingerprint: Optional[tuple] = None
|
|
12
|
-
_fp_observed: bool = False
|
|
13
66
|
|
|
14
67
|
@contextmanager
|
|
15
68
|
def scanning_flag():
|
|
16
|
-
"""
|
|
69
|
+
"""Mark scanning=True within the block."""
|
|
17
70
|
tok = _scanning.set(True)
|
|
18
71
|
try:
|
|
19
72
|
yield
|
|
20
73
|
finally:
|
|
21
74
|
_scanning.reset(tok)
|
|
22
75
|
|
|
23
|
-
# ---- fingerprint helpers (public via api) ----
|
|
24
|
-
def set_fingerprint(fp: Optional[tuple]) -> None:
|
|
25
|
-
global _fingerprint
|
|
26
|
-
_fingerprint = fp
|
|
27
|
-
|
|
28
|
-
def get_fingerprint() -> Optional[tuple]:
|
|
29
|
-
return _fingerprint
|
|
30
|
-
|
|
31
|
-
def reset_fp_observed() -> None:
|
|
32
|
-
global _fp_observed
|
|
33
|
-
_fp_observed = False
|
|
34
|
-
|
|
35
|
-
def mark_fp_observed() -> None:
|
|
36
|
-
global _fp_observed
|
|
37
|
-
_fp_observed = True
|
|
38
|
-
|
|
39
|
-
def was_fp_observed() -> bool:
|
|
40
|
-
return _fp_observed
|
pico_ioc/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = '1.
|
|
1
|
+
__version__ = '1.5.0'
|
pico_ioc/api.py
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
# src/pico_ioc/api.py
|
|
2
1
|
from __future__ import annotations
|
|
3
2
|
|
|
4
3
|
import inspect as _inspect
|
|
5
4
|
import importlib
|
|
6
5
|
import logging
|
|
7
|
-
import os
|
|
8
6
|
from types import ModuleType
|
|
9
7
|
from typing import Callable, Optional, Tuple, Any, Dict, Iterable, Sequence
|
|
10
8
|
|
|
@@ -12,12 +10,12 @@ from .container import PicoContainer
|
|
|
12
10
|
from .plugins import PicoPlugin
|
|
13
11
|
from . import _state
|
|
14
12
|
from .builder import PicoContainerBuilder
|
|
13
|
+
from .scope import ScopedContainer
|
|
14
|
+
from .config import ConfigRegistry, ConfigSource
|
|
15
|
+
|
|
15
16
|
|
|
16
|
-
# The only helpers left are those directly related to the public API signature or fingerprinting
|
|
17
17
|
def reset() -> None:
|
|
18
|
-
_state.
|
|
19
|
-
_state._root_name = None
|
|
20
|
-
_state.set_fingerprint(None)
|
|
18
|
+
_state.set_context(None)
|
|
21
19
|
|
|
22
20
|
def _combine_excludes(a: Optional[Callable[[str], bool]], b: Optional[Callable[[str], bool]]):
|
|
23
21
|
if not a and not b: return None
|
|
@@ -82,6 +80,20 @@ def _make_fingerprint_from_signature(locals_in_init: dict) -> tuple:
|
|
|
82
80
|
val = _callable_id(val) if val else None
|
|
83
81
|
elif name == "overrides":
|
|
84
82
|
val = _normalize_overrides_for_fp(val)
|
|
83
|
+
elif name == "config":
|
|
84
|
+
cfg = locals_in_init.get("config") or ()
|
|
85
|
+
norm = []
|
|
86
|
+
for s in cfg:
|
|
87
|
+
try:
|
|
88
|
+
if type(s).__name__ == "EnvSource":
|
|
89
|
+
norm.append(("env", getattr(s, "prefix", "")))
|
|
90
|
+
elif type(s).__name__ == "FileSource":
|
|
91
|
+
norm.append(("file", str(getattr(s, "path", ""))))
|
|
92
|
+
else:
|
|
93
|
+
norm.append((type(s).__module__, type(s).__qualname__))
|
|
94
|
+
except Exception:
|
|
95
|
+
norm.append(repr(s))
|
|
96
|
+
val = tuple(norm)
|
|
85
97
|
else:
|
|
86
98
|
val = _normalize_for_fp(val)
|
|
87
99
|
entries.append((name, val))
|
|
@@ -89,8 +101,9 @@ def _make_fingerprint_from_signature(locals_in_init: dict) -> tuple:
|
|
|
89
101
|
|
|
90
102
|
# -------- container reuse and caller exclusion helpers --------
|
|
91
103
|
def _maybe_reuse_existing(fp: tuple, overrides: Optional[Dict[Any, Any]]) -> Optional[PicoContainer]:
|
|
92
|
-
|
|
93
|
-
|
|
104
|
+
ctx = _state.get_context()
|
|
105
|
+
if ctx and ctx.fingerprint == fp:
|
|
106
|
+
return ctx.container
|
|
94
107
|
return None
|
|
95
108
|
|
|
96
109
|
def _build_exclude(
|
|
@@ -116,13 +129,15 @@ def _get_caller_module_name() -> Optional[str]:
|
|
|
116
129
|
pass
|
|
117
130
|
return None
|
|
118
131
|
|
|
119
|
-
# ---------------- public API ----------------
|
|
120
132
|
def init(
|
|
121
133
|
root_package, *, profiles: Optional[list[str]] = None, exclude: Optional[Callable[[str], bool]] = None,
|
|
122
134
|
auto_exclude_caller: bool = True, plugins: Tuple[PicoPlugin, ...] = (), reuse: bool = True,
|
|
123
135
|
overrides: Optional[Dict[Any, Any]] = None, auto_scan: Sequence[str] = (),
|
|
124
136
|
auto_scan_exclude: Optional[Callable[[str], bool]] = None, strict_autoscan: bool = False,
|
|
137
|
+
config: Sequence[ConfigSource] = (),
|
|
125
138
|
) -> PicoContainer:
|
|
139
|
+
if _state._scanning.get():
|
|
140
|
+
logging.info("re-entrant container access during scan")
|
|
126
141
|
root_name = root_package if isinstance(root_package, str) else getattr(root_package, "__name__", None)
|
|
127
142
|
fp = _make_fingerprint_from_signature(locals())
|
|
128
143
|
|
|
@@ -131,7 +146,11 @@ def init(
|
|
|
131
146
|
if reused is not None:
|
|
132
147
|
return reused
|
|
133
148
|
|
|
134
|
-
builder = PicoContainerBuilder()
|
|
149
|
+
builder = (PicoContainerBuilder()
|
|
150
|
+
.with_plugins(plugins)
|
|
151
|
+
.with_profiles(profiles)
|
|
152
|
+
.with_overrides(overrides)
|
|
153
|
+
.with_config(ConfigRegistry(config or ())))
|
|
135
154
|
|
|
136
155
|
combined_exclude = _build_exclude(exclude, auto_exclude_caller, root_name=root_name)
|
|
137
156
|
builder.add_scan_package(root_package, exclude=combined_exclude)
|
|
@@ -151,11 +170,11 @@ def init(
|
|
|
151
170
|
|
|
152
171
|
container = builder.build()
|
|
153
172
|
|
|
154
|
-
_state.
|
|
155
|
-
_state.
|
|
156
|
-
_state.set_fingerprint(fp)
|
|
173
|
+
new_ctx = _state.ContainerContext(container=container, fingerprint=fp, root_name=root_name)
|
|
174
|
+
_state.set_context(new_ctx)
|
|
157
175
|
return container
|
|
158
176
|
|
|
177
|
+
|
|
159
178
|
def scope(
|
|
160
179
|
*, modules: Iterable[Any] = (), roots: Iterable[type] = (), profiles: Optional[list[str]] = None,
|
|
161
180
|
overrides: Optional[Dict[Any, Any]] = None, base: Optional[PicoContainer] = None,
|
|
@@ -178,9 +197,9 @@ def scope(
|
|
|
178
197
|
for m in modules:
|
|
179
198
|
builder.add_scan_package(m)
|
|
180
199
|
|
|
181
|
-
built_container = builder.build()
|
|
200
|
+
built_container = builder.with_eager(not lazy).build()
|
|
182
201
|
|
|
183
|
-
scoped_container =
|
|
202
|
+
scoped_container = ScopedContainer(base=base, strict=strict, built_container=built_container)
|
|
184
203
|
|
|
185
204
|
if not lazy:
|
|
186
205
|
from .proxy import ComponentProxy
|
|
@@ -195,46 +214,9 @@ def scope(
|
|
|
195
214
|
logging.info("Scope container ready.")
|
|
196
215
|
return scoped_container
|
|
197
216
|
|
|
198
|
-
|
|
199
|
-
def __init__(self, built_container: PicoContainer, base: Optional[PicoContainer], strict: bool):
|
|
200
|
-
super().__init__(providers=getattr(built_container, "_providers", {}).copy())
|
|
201
|
-
|
|
202
|
-
self._active_profiles = getattr(built_container, "_active_profiles", ())
|
|
203
|
-
|
|
204
|
-
base_method_its = getattr(base, "_method_interceptors", ()) if base else ()
|
|
205
|
-
base_container_its = getattr(base, "_container_interceptors", ()) if base else ()
|
|
206
|
-
|
|
207
|
-
self._method_interceptors = base_method_its
|
|
208
|
-
self._container_interceptors = base_container_its
|
|
209
|
-
self._seen_interceptor_types = {type(it) for it in (base_method_its + base_container_its)}
|
|
210
|
-
|
|
211
|
-
for it in getattr(built_container, "_method_interceptors", ()):
|
|
212
|
-
self.add_method_interceptor(it)
|
|
213
|
-
for it in getattr(built_container, "_container_interceptors", ()):
|
|
214
|
-
self.add_container_interceptor(it)
|
|
215
|
-
|
|
216
|
-
self._base = base
|
|
217
|
-
self._strict = strict
|
|
218
|
-
|
|
219
|
-
if base:
|
|
220
|
-
self._singletons.update(getattr(base, "_singletons", {}))
|
|
221
|
-
|
|
222
|
-
def __enter__(self): return self
|
|
223
|
-
def __exit__(self, exc_type, exc, tb): return False
|
|
224
|
-
|
|
225
|
-
def has(self, key: Any) -> bool:
|
|
226
|
-
if super().has(key): return True
|
|
227
|
-
if not self._strict and self._base is not None:
|
|
228
|
-
return self._base.has(key)
|
|
229
|
-
return False
|
|
230
|
-
|
|
231
|
-
def get(self, key: Any):
|
|
232
|
-
try:
|
|
233
|
-
return super().get(key)
|
|
234
|
-
except NameError as e:
|
|
235
|
-
if not self._strict and self._base is not None and self._base.has(key):
|
|
236
|
-
return self._base.get(key)
|
|
237
|
-
raise e
|
|
217
|
+
|
|
238
218
|
|
|
239
219
|
def container_fingerprint() -> Optional[tuple]:
|
|
240
|
-
|
|
220
|
+
ctx = _state.get_context()
|
|
221
|
+
return ctx.fingerprint if ctx else None
|
|
222
|
+
|
pico_ioc/builder.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# src/pico_ioc/builder.py
|
|
2
1
|
from __future__ import annotations
|
|
3
2
|
import inspect as _inspect
|
|
4
3
|
import logging
|
|
@@ -6,7 +5,6 @@ import os
|
|
|
6
5
|
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple
|
|
7
6
|
from typing import get_origin, get_args, Annotated
|
|
8
7
|
|
|
9
|
-
# Add missing imports for interceptor types
|
|
10
8
|
from .interceptors import MethodInterceptor, ContainerInterceptor
|
|
11
9
|
from .container import PicoContainer, _is_compatible
|
|
12
10
|
from .policy import apply_policy, _conditional_active
|
|
@@ -14,7 +12,7 @@ from .plugins import PicoPlugin, run_plugin_hook
|
|
|
14
12
|
from .scanner import scan_and_configure
|
|
15
13
|
from .resolver import Resolver, _get_hints
|
|
16
14
|
from . import _state
|
|
17
|
-
|
|
15
|
+
from .config import ConfigRegistry
|
|
18
16
|
|
|
19
17
|
class PicoContainerBuilder:
|
|
20
18
|
def __init__(self):
|
|
@@ -26,72 +24,70 @@ class PicoContainerBuilder:
|
|
|
26
24
|
self._exclude_tags: Optional[set[str]] = None
|
|
27
25
|
self._roots: Iterable[type] = ()
|
|
28
26
|
self._providers: Dict[Any, Dict] = {}
|
|
29
|
-
self.
|
|
27
|
+
self._eager: bool = True
|
|
28
|
+
self._config_registry: ConfigRegistry | None = None
|
|
29
|
+
|
|
30
|
+
def with_config(self, registry: ConfigRegistry) -> "PicoContainerBuilder":
|
|
31
|
+
self._config_registry = registry
|
|
32
|
+
return self
|
|
30
33
|
|
|
31
|
-
def with_plugins(self, plugins: Tuple[PicoPlugin, ...]) -> PicoContainerBuilder:
|
|
32
|
-
self._plugins = plugins
|
|
34
|
+
def with_plugins(self, plugins: Tuple[PicoPlugin, ...]) -> "PicoContainerBuilder":
|
|
35
|
+
self._plugins = plugins or ()
|
|
33
36
|
return self
|
|
34
37
|
|
|
35
|
-
def with_profiles(self, profiles: Optional[List[str]]) -> PicoContainerBuilder:
|
|
38
|
+
def with_profiles(self, profiles: Optional[List[str]]) -> "PicoContainerBuilder":
|
|
36
39
|
self._profiles = profiles
|
|
37
40
|
return self
|
|
38
41
|
|
|
39
|
-
def add_scan_package(self, package: Any, exclude: Optional[Callable[[str], bool]] = None) -> PicoContainerBuilder:
|
|
42
|
+
def add_scan_package(self, package: Any, exclude: Optional[Callable[[str], bool]] = None) -> "PicoContainerBuilder":
|
|
40
43
|
self._scan_plan.append((package, exclude, self._plugins))
|
|
41
44
|
return self
|
|
42
45
|
|
|
43
|
-
def with_overrides(self, overrides: Optional[Dict[Any, Any]]) -> PicoContainerBuilder:
|
|
46
|
+
def with_overrides(self, overrides: Optional[Dict[Any, Any]]) -> "PicoContainerBuilder":
|
|
44
47
|
self._overrides = overrides or {}
|
|
45
48
|
return self
|
|
46
49
|
|
|
47
|
-
def with_tag_filters(self, include: Optional[set[str]], exclude: Optional[set[str]]) -> PicoContainerBuilder:
|
|
50
|
+
def with_tag_filters(self, include: Optional[set[str]], exclude: Optional[set[str]]) -> "PicoContainerBuilder":
|
|
48
51
|
self._include_tags = include
|
|
49
52
|
self._exclude_tags = exclude
|
|
50
53
|
return self
|
|
51
54
|
|
|
52
|
-
def with_roots(self, roots: Iterable[type]) -> PicoContainerBuilder:
|
|
53
|
-
self._roots = roots
|
|
55
|
+
def with_roots(self, roots: Iterable[type]) -> "PicoContainerBuilder":
|
|
56
|
+
self._roots = roots or ()
|
|
57
|
+
return self
|
|
58
|
+
|
|
59
|
+
def with_eager(self, eager: bool) -> "PicoContainerBuilder":
|
|
60
|
+
self._eager = bool(eager)
|
|
54
61
|
return self
|
|
55
62
|
|
|
56
63
|
def build(self) -> PicoContainer:
|
|
57
64
|
requested_profiles = _resolve_profiles(self._profiles)
|
|
58
|
-
|
|
59
|
-
# We now create a single container instance upfront and configure it.
|
|
60
65
|
container = PicoContainer(providers=self._providers)
|
|
61
66
|
container._active_profiles = tuple(requested_profiles)
|
|
62
|
-
|
|
67
|
+
setattr(container, "_config_registry", self._config_registry)
|
|
68
|
+
all_infras: list[tuple[type, dict]] = []
|
|
63
69
|
for pkg, exclude, scan_plugins in self._scan_plan:
|
|
64
70
|
with _state.scanning_flag():
|
|
65
|
-
c, f,
|
|
71
|
+
c, f, infra_decls = scan_and_configure(pkg, container, exclude=exclude, plugins=scan_plugins)
|
|
66
72
|
logging.info("Scanned '%s' (components: %d, factories: %d)", getattr(pkg, "__name__", pkg), c, f)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
_activate_and_build_interceptors(
|
|
70
|
-
container=container,
|
|
71
|
-
interceptor_decls=self._interceptor_decls,
|
|
72
|
-
profiles=requested_profiles
|
|
73
|
-
)
|
|
74
|
-
|
|
73
|
+
all_infras.extend(infra_decls)
|
|
74
|
+
_run_infrastructure(container=container, infra_decls=all_infras, profiles=requested_profiles)
|
|
75
75
|
binder = container.binder()
|
|
76
|
-
|
|
77
76
|
if self._overrides:
|
|
78
77
|
_apply_overrides(container, self._overrides)
|
|
79
|
-
|
|
80
78
|
run_plugin_hook(self._plugins, "after_bind", container, binder)
|
|
81
79
|
run_plugin_hook(self._plugins, "before_eager", container, binder)
|
|
82
80
|
apply_policy(container, profiles=requested_profiles)
|
|
83
81
|
_filter_by_tags(container, self._include_tags, self._exclude_tags)
|
|
84
82
|
if self._roots:
|
|
85
83
|
_restrict_to_subgraph(container, self._roots, self._overrides)
|
|
86
|
-
|
|
87
84
|
run_plugin_hook(self._plugins, "after_ready", container, binder)
|
|
88
|
-
|
|
85
|
+
if self._eager:
|
|
86
|
+
container.eager_instantiate_all()
|
|
89
87
|
logging.info("Container configured and ready.")
|
|
90
88
|
return container
|
|
91
89
|
|
|
92
|
-
|
|
93
|
-
# --- Start of moved helpers ---
|
|
94
|
-
def _resolve_profiles(profiles: Optional[list[str]]) -> list[str]:
|
|
90
|
+
def _resolve_profiles(profiles: Optional[List[str]]) -> List[str]:
|
|
95
91
|
if profiles is not None:
|
|
96
92
|
return list(profiles)
|
|
97
93
|
env_val = os.getenv("PICO_PROFILE", "")
|
|
@@ -112,7 +108,6 @@ def _apply_overrides(container: PicoContainer, overrides: Dict[Any, Any]) -> Non
|
|
|
112
108
|
def _filter_by_tags(container: PicoContainer, include_tags: Optional[set[str]], exclude_tags: Optional[set[str]]) -> None:
|
|
113
109
|
if not include_tags and not exclude_tags:
|
|
114
110
|
return
|
|
115
|
-
|
|
116
111
|
def _tag_ok(meta: dict) -> bool:
|
|
117
112
|
tags = set(meta.get("tags", ()))
|
|
118
113
|
if include_tags and not tags.intersection(include_tags):
|
|
@@ -123,9 +118,8 @@ def _filter_by_tags(container: PicoContainer, include_tags: Optional[set[str]],
|
|
|
123
118
|
container._providers = {k: v for k, v in container._providers.items() if _tag_ok(v)}
|
|
124
119
|
|
|
125
120
|
def _compute_allowed_subgraph(container: PicoContainer, roots: Iterable[type]) -> set:
|
|
126
|
-
allowed: set[Any] = set(roots)
|
|
121
|
+
allowed: set[Any] = set(roots)
|
|
127
122
|
stack = list(roots or ())
|
|
128
|
-
# ... (rest of the function is the same, just ensure it's here)
|
|
129
123
|
def _add_impls_for_base(base_t):
|
|
130
124
|
for prov_key, meta in container._providers.items():
|
|
131
125
|
cls = prov_key if isinstance(prov_key, type) else None
|
|
@@ -133,26 +127,28 @@ def _compute_allowed_subgraph(container: PicoContainer, roots: Iterable[type]) -
|
|
|
133
127
|
if prov_key not in allowed:
|
|
134
128
|
allowed.add(prov_key)
|
|
135
129
|
stack.append(prov_key)
|
|
136
|
-
|
|
137
130
|
while stack:
|
|
138
131
|
k = stack.pop()
|
|
139
|
-
# if k in allowed: continue # Redundant, add() handles it
|
|
140
132
|
allowed.add(k)
|
|
141
|
-
if isinstance(k, type):
|
|
133
|
+
if isinstance(k, type):
|
|
134
|
+
_add_impls_for_base(k)
|
|
142
135
|
cls = k if isinstance(k, type) else None
|
|
143
|
-
if cls is None or not container.has(k):
|
|
136
|
+
if cls is None or not container.has(k):
|
|
137
|
+
continue
|
|
144
138
|
try:
|
|
145
139
|
sig = _inspect.signature(cls.__init__)
|
|
146
140
|
hints = _get_hints(cls.__init__, owner_cls=cls)
|
|
147
141
|
except Exception:
|
|
148
142
|
continue
|
|
149
143
|
for pname, param in sig.parameters.items():
|
|
150
|
-
if pname == "self":
|
|
144
|
+
if pname == "self":
|
|
145
|
+
continue
|
|
151
146
|
ann = hints.get(pname, param.annotation)
|
|
152
147
|
origin = get_origin(ann) or ann
|
|
153
148
|
if origin in (list, tuple):
|
|
154
149
|
inner = (get_args(ann) or (object,))[0]
|
|
155
|
-
if get_origin(inner) is Annotated:
|
|
150
|
+
if get_origin(inner) is Annotated:
|
|
151
|
+
inner = (get_args(inner) or (object,))[0]
|
|
156
152
|
if isinstance(inner, type):
|
|
157
153
|
if inner not in allowed:
|
|
158
154
|
stack.append(inner)
|
|
@@ -163,80 +159,52 @@ def _compute_allowed_subgraph(container: PicoContainer, roots: Iterable[type]) -
|
|
|
163
159
|
stack.append(pname)
|
|
164
160
|
return allowed
|
|
165
161
|
|
|
166
|
-
|
|
167
162
|
def _restrict_to_subgraph(container: PicoContainer, roots: Iterable[type], overrides: Optional[Dict[Any, Any]]) -> None:
|
|
168
163
|
allowed = _compute_allowed_subgraph(container, roots)
|
|
169
164
|
keep_keys: set[Any] = allowed | (set(overrides.keys()) if overrides else set())
|
|
170
165
|
container._providers = {k: v for k, v in container._providers.items() if k in keep_keys}
|
|
171
166
|
|
|
172
|
-
def
|
|
173
|
-
|
|
174
|
-
) -> None:
|
|
175
|
-
resolver = Resolver(container)
|
|
176
|
-
active: list[tuple[int, str, str, Any]] = []
|
|
177
|
-
activated_method_names: list[str] = []
|
|
178
|
-
activated_container_names: list[str] = []
|
|
179
|
-
skipped_debug: list[str] = []
|
|
180
|
-
|
|
181
|
-
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:
|
|
182
169
|
profs = tuple(meta.get("profiles", ())) or ()
|
|
183
|
-
if profs and (not profiles or not any(p in profs for p in profiles)):
|
|
170
|
+
if profs and (not profiles or not any(p in profs for p in profiles)):
|
|
171
|
+
return False
|
|
184
172
|
req_env = tuple(meta.get("require_env", ())) or ()
|
|
185
|
-
if req_env
|
|
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
|
|
186
177
|
pred = meta.get("predicate", None)
|
|
187
178
|
if callable(pred):
|
|
188
179
|
try:
|
|
189
|
-
if not bool(pred()):
|
|
180
|
+
if not bool(pred()):
|
|
181
|
+
return False
|
|
190
182
|
except Exception:
|
|
191
|
-
logging.exception("Interceptor predicate failed; skipping")
|
|
192
183
|
return False
|
|
193
184
|
return True
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
for
|
|
199
|
-
|
|
200
|
-
qn = getattr(obj, "__qualname__", repr(obj))
|
|
201
|
-
if not _conditional_active(obj, profiles=profiles) or not _interceptor_meta_active(meta):
|
|
202
|
-
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):
|
|
203
191
|
continue
|
|
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:
|
|
204
196
|
try:
|
|
205
|
-
|
|
206
|
-
inst = resolver.create_instance(obj)
|
|
207
|
-
elif owner_cls is not None:
|
|
208
|
-
owner_inst = resolver.create_instance(owner_cls)
|
|
209
|
-
bound = obj.__get__(owner_inst, owner_cls)
|
|
210
|
-
kwargs = resolver.kwargs_for_callable(bound, owner_cls=owner_cls)
|
|
211
|
-
inst = bound(**kwargs)
|
|
212
|
-
else:
|
|
213
|
-
kwargs = resolver.kwargs_for_callable(obj, owner_cls=None)
|
|
214
|
-
inst = obj(**kwargs)
|
|
197
|
+
inst = resolver.create_instance(cls)
|
|
215
198
|
except Exception:
|
|
216
|
-
logging
|
|
217
|
-
|
|
218
|
-
kind = meta.get("kind", "method")
|
|
219
|
-
if kind == "method" and not callable(inst):
|
|
220
|
-
logging.error("Interceptor %s is not valid for kind %s; skipping", qn, kind)
|
|
199
|
+
import logging
|
|
200
|
+
logging.exception("Failed to construct infrastructure %r", cls)
|
|
221
201
|
continue
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if kind == "container":
|
|
232
|
-
container.add_container_interceptor(inst)
|
|
233
|
-
activated_container_names.append(_qn)
|
|
234
|
-
else:
|
|
235
|
-
container.add_method_interceptor(inst)
|
|
236
|
-
activated_method_names.append(_qn)
|
|
237
|
-
|
|
238
|
-
if activated_method_names or activated_container_names:
|
|
239
|
-
logging.info("Interceptors activated: method=%d, container=%d", len(activated_method_names), len(activated_container_names))
|
|
240
|
-
logging.debug("Activated method=%s; Activated container=%s", ", ".join(activated_method_names) or "-", ", ".join(activated_container_names) or "-")
|
|
241
|
-
if skipped_debug:
|
|
242
|
-
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)
|
|
210
|
+
|