pico-ioc 1.2.0__py3-none-any.whl → 1.4.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 +30 -4
- pico_ioc/_state.py +69 -4
- pico_ioc/_version.py +1 -1
- pico_ioc/api.py +183 -251
- pico_ioc/builder.py +294 -0
- pico_ioc/config.py +332 -0
- pico_ioc/container.py +73 -26
- pico_ioc/decorators.py +88 -9
- pico_ioc/interceptors.py +56 -0
- pico_ioc/plugins.py +17 -1
- pico_ioc/policy.py +245 -0
- pico_ioc/proxy.py +59 -7
- pico_ioc/resolver.py +54 -46
- pico_ioc/scanner.py +75 -102
- pico_ioc/scope.py +46 -0
- pico_ioc/utils.py +25 -0
- {pico_ioc-1.2.0.dist-info → pico_ioc-1.4.0.dist-info}/METADATA +65 -16
- pico_ioc-1.4.0.dist-info/RECORD +22 -0
- pico_ioc/typing_utils.py +0 -29
- pico_ioc-1.2.0.dist-info/RECORD +0 -17
- {pico_ioc-1.2.0.dist-info → pico_ioc-1.4.0.dist-info}/WHEEL +0 -0
- {pico_ioc-1.2.0.dist-info → pico_ioc-1.4.0.dist-info}/licenses/LICENSE +0 -0
- {pico_ioc-1.2.0.dist-info → pico_ioc-1.4.0.dist-info}/top_level.txt +0 -0
pico_ioc/__init__.py
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
# pico_ioc/__init__.py
|
|
2
|
-
|
|
3
2
|
try:
|
|
4
3
|
from ._version import __version__
|
|
5
4
|
except Exception:
|
|
6
5
|
__version__ = "0.0.0"
|
|
7
6
|
|
|
8
7
|
from .container import PicoContainer, Binder
|
|
9
|
-
from .
|
|
8
|
+
from .scope import ScopedContainer
|
|
9
|
+
from .decorators import (
|
|
10
|
+
component, factory_component, provides, plugin,
|
|
11
|
+
Qualifier, qualifier,
|
|
12
|
+
on_missing, primary, conditional, interceptor,
|
|
13
|
+
)
|
|
10
14
|
from .plugins import PicoPlugin
|
|
11
15
|
from .resolver import Resolver
|
|
12
|
-
from .api import init, reset, scope
|
|
13
|
-
from .proxy import ComponentProxy
|
|
16
|
+
from .api import init, reset, scope, container_fingerprint
|
|
17
|
+
from .proxy import ComponentProxy, IoCProxy
|
|
18
|
+
from .interceptors import Invocation, MethodInterceptor, ContainerInterceptor
|
|
19
|
+
from .config import (
|
|
20
|
+
config_component, EnvSource, FileSource,
|
|
21
|
+
Env, File, Path, Value,
|
|
22
|
+
)
|
|
14
23
|
|
|
15
24
|
__all__ = [
|
|
16
25
|
"__version__",
|
|
@@ -18,15 +27,32 @@ __all__ = [
|
|
|
18
27
|
"Binder",
|
|
19
28
|
"PicoPlugin",
|
|
20
29
|
"ComponentProxy",
|
|
30
|
+
"IoCProxy",
|
|
31
|
+
"Invocation",
|
|
32
|
+
"MethodInterceptor",
|
|
33
|
+
"ContainerInterceptor",
|
|
21
34
|
"init",
|
|
22
35
|
"scope",
|
|
23
36
|
"reset",
|
|
37
|
+
"container_fingerprint",
|
|
24
38
|
"component",
|
|
25
39
|
"factory_component",
|
|
26
40
|
"provides",
|
|
27
41
|
"plugin",
|
|
28
42
|
"Qualifier",
|
|
29
43
|
"qualifier",
|
|
44
|
+
"on_missing",
|
|
45
|
+
"primary",
|
|
46
|
+
"conditional",
|
|
47
|
+
"interceptor",
|
|
30
48
|
"Resolver",
|
|
49
|
+
"ScopedContainer",
|
|
50
|
+
"config_component",
|
|
51
|
+
"EnvSource",
|
|
52
|
+
"FileSource",
|
|
53
|
+
"Env",
|
|
54
|
+
"File",
|
|
55
|
+
"Path",
|
|
56
|
+
"Value",
|
|
31
57
|
]
|
|
32
58
|
|
pico_ioc/_state.py
CHANGED
|
@@ -1,10 +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
|
|
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) ----
|
|
4
62
|
|
|
5
63
|
_scanning: ContextVar[bool] = ContextVar("pico_scanning", default=False)
|
|
6
64
|
_resolving: ContextVar[bool] = ContextVar("pico_resolving", default=False)
|
|
7
65
|
|
|
8
|
-
|
|
9
|
-
|
|
66
|
+
|
|
67
|
+
@contextmanager
|
|
68
|
+
def scanning_flag():
|
|
69
|
+
"""Mark scanning=True within the block."""
|
|
70
|
+
tok = _scanning.set(True)
|
|
71
|
+
try:
|
|
72
|
+
yield
|
|
73
|
+
finally:
|
|
74
|
+
_scanning.reset(tok)
|
|
10
75
|
|
pico_ioc/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = '1.
|
|
1
|
+
__version__ = '1.4.0'
|
pico_ioc/api.py
CHANGED
|
@@ -1,289 +1,221 @@
|
|
|
1
|
-
# pico_ioc/api.py
|
|
2
|
-
|
|
3
1
|
from __future__ import annotations
|
|
4
2
|
|
|
5
|
-
import inspect
|
|
3
|
+
import inspect as _inspect
|
|
4
|
+
import importlib
|
|
6
5
|
import logging
|
|
7
|
-
from
|
|
8
|
-
from typing import Callable, Optional, Tuple, Any, Dict, Iterable
|
|
6
|
+
from types import ModuleType
|
|
7
|
+
from typing import Callable, Optional, Tuple, Any, Dict, Iterable, Sequence
|
|
9
8
|
|
|
10
|
-
from .container import PicoContainer
|
|
9
|
+
from .container import PicoContainer
|
|
11
10
|
from .plugins import PicoPlugin
|
|
12
|
-
from .scanner import scan_and_configure
|
|
13
11
|
from . import _state
|
|
12
|
+
from .builder import PicoContainerBuilder
|
|
13
|
+
from .scope import ScopedContainer
|
|
14
|
+
from .config import ConfigRegistry, ConfigSource
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
def reset() -> None:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
_state.set_context(None)
|
|
19
|
+
|
|
20
|
+
def _combine_excludes(a: Optional[Callable[[str], bool]], b: Optional[Callable[[str], bool]]):
|
|
21
|
+
if not a and not b: return None
|
|
22
|
+
if a and not b: return a
|
|
23
|
+
if b and not a: return b
|
|
24
|
+
return lambda mod, _a=a, _b=b: _a(mod) or _b(mod)
|
|
25
|
+
|
|
26
|
+
# -------- fingerprint helpers --------
|
|
27
|
+
def _callable_id(cb) -> tuple:
|
|
28
|
+
try:
|
|
29
|
+
mod = getattr(cb, "__module__", None)
|
|
30
|
+
qn = getattr(cb, "__qualname__", None)
|
|
31
|
+
code = getattr(cb, "__code__", None)
|
|
32
|
+
fn_line = getattr(code, "co_firstlineno", None) if code else None
|
|
33
|
+
return (mod, qn, fn_line)
|
|
34
|
+
except Exception:
|
|
35
|
+
return (repr(cb),)
|
|
36
|
+
|
|
37
|
+
def _plugins_id(plugins: Tuple[PicoPlugin, ...]) -> tuple:
|
|
38
|
+
out = [(type(p).__module__, type(p).__qualname__) for p in plugins or ()]
|
|
39
|
+
return tuple(sorted(out))
|
|
40
|
+
|
|
41
|
+
def _normalize_for_fp(value):
|
|
42
|
+
if isinstance(value, ModuleType):
|
|
43
|
+
return getattr(value, "__name__", repr(value))
|
|
44
|
+
if isinstance(value, (tuple, list)):
|
|
45
|
+
return tuple(_normalize_for_fp(v) for v in value)
|
|
46
|
+
if isinstance(value, set):
|
|
47
|
+
return tuple(sorted(_normalize_for_fp(v) for v in value))
|
|
48
|
+
if callable(value):
|
|
49
|
+
return ("callable",) + _callable_id(value)
|
|
50
|
+
return value
|
|
51
|
+
|
|
52
|
+
_FP_EXCLUDE_KEYS = set()
|
|
53
|
+
|
|
54
|
+
def _normalize_overrides_for_fp(overrides: Optional[Dict[Any, Any]]) -> tuple:
|
|
55
|
+
if not overrides:
|
|
56
|
+
return ()
|
|
57
|
+
items = []
|
|
58
|
+
for k, v in overrides.items():
|
|
59
|
+
nk = _normalize_for_fp(k)
|
|
60
|
+
nv = _normalize_for_fp(v)
|
|
61
|
+
items.append((nk, nv))
|
|
62
|
+
return tuple(sorted(items))
|
|
63
|
+
|
|
64
|
+
def _make_fingerprint_from_signature(locals_in_init: dict) -> tuple:
|
|
65
|
+
sig = _inspect.signature(init)
|
|
66
|
+
entries = []
|
|
67
|
+
for name in sig.parameters.keys():
|
|
68
|
+
if name in _FP_EXCLUDE_KEYS: continue
|
|
69
|
+
if name == "root_package":
|
|
70
|
+
rp = locals_in_init.get("root_package")
|
|
71
|
+
root_name = rp if isinstance(rp, str) else getattr(rp, "__name__", None)
|
|
72
|
+
entries.append(("root", root_name))
|
|
73
|
+
continue
|
|
74
|
+
val = locals_in_init.get(name, None)
|
|
75
|
+
if name == "plugins":
|
|
76
|
+
val = _plugins_id(val or ())
|
|
77
|
+
elif name in ("profiles", "auto_scan"):
|
|
78
|
+
val = tuple(val or ())
|
|
79
|
+
elif name in ("exclude", "auto_scan_exclude"):
|
|
80
|
+
val = _callable_id(val) if val else None
|
|
81
|
+
elif name == "overrides":
|
|
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)
|
|
97
|
+
else:
|
|
98
|
+
val = _normalize_for_fp(val)
|
|
99
|
+
entries.append((name, val))
|
|
100
|
+
return tuple(sorted(entries))
|
|
101
|
+
|
|
102
|
+
# -------- container reuse and caller exclusion helpers --------
|
|
103
|
+
def _maybe_reuse_existing(fp: tuple, overrides: Optional[Dict[Any, Any]]) -> Optional[PicoContainer]:
|
|
104
|
+
ctx = _state.get_context()
|
|
105
|
+
if ctx and ctx.fingerprint == fp:
|
|
106
|
+
return ctx.container
|
|
107
|
+
return None
|
|
20
108
|
|
|
109
|
+
def _build_exclude(
|
|
110
|
+
exclude: Optional[Callable[[str], bool]], auto_exclude_caller: bool, *, root_name: Optional[str] = None
|
|
111
|
+
) -> Optional[Callable[[str], bool]]:
|
|
112
|
+
if not auto_exclude_caller: return exclude
|
|
113
|
+
caller = _get_caller_module_name()
|
|
114
|
+
if not caller: return exclude
|
|
115
|
+
def _under_root(mod: str) -> bool:
|
|
116
|
+
return bool(root_name) and (mod == root_name or mod.startswith(root_name + "."))
|
|
117
|
+
if exclude is None:
|
|
118
|
+
return lambda mod, _caller=caller: (mod == _caller) and not _under_root(mod)
|
|
119
|
+
return lambda mod, _caller=caller, _prev=exclude: (((mod == _caller) and not _under_root(mod)) or _prev(mod))
|
|
21
120
|
|
|
121
|
+
def _get_caller_module_name() -> Optional[str]:
|
|
122
|
+
try:
|
|
123
|
+
f = _inspect.currentframe()
|
|
124
|
+
# Stack: _get_caller -> _build_exclude -> init -> caller
|
|
125
|
+
if f and f.f_back and f.f_back.f_back and f.f_back.f_back.f_back:
|
|
126
|
+
mod = _inspect.getmodule(f.f_back.f_back.f_back)
|
|
127
|
+
return getattr(mod, "__name__", None)
|
|
128
|
+
except Exception:
|
|
129
|
+
pass
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
# ---------------- public API ----------------
|
|
22
133
|
def init(
|
|
23
|
-
root_package,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
reuse: bool = True,
|
|
29
|
-
overrides: Optional[Dict[Any, Any]] = None,
|
|
134
|
+
root_package, *, profiles: Optional[list[str]] = None, exclude: Optional[Callable[[str], bool]] = None,
|
|
135
|
+
auto_exclude_caller: bool = True, plugins: Tuple[PicoPlugin, ...] = (), reuse: bool = True,
|
|
136
|
+
overrides: Optional[Dict[Any, Any]] = None, auto_scan: Sequence[str] = (),
|
|
137
|
+
auto_scan_exclude: Optional[Callable[[str], bool]] = None, strict_autoscan: bool = False,
|
|
138
|
+
config: Sequence[ConfigSource] = (),
|
|
30
139
|
) -> PicoContainer:
|
|
31
|
-
|
|
32
140
|
root_name = root_package if isinstance(root_package, str) else getattr(root_package, "__name__", None)
|
|
141
|
+
fp = _make_fingerprint_from_signature(locals())
|
|
33
142
|
|
|
34
|
-
if reuse
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
143
|
+
if reuse:
|
|
144
|
+
reused = _maybe_reuse_existing(fp, overrides)
|
|
145
|
+
if reused is not None:
|
|
146
|
+
return reused
|
|
38
147
|
|
|
39
|
-
|
|
148
|
+
builder = (PicoContainerBuilder()
|
|
149
|
+
.with_plugins(plugins)
|
|
150
|
+
.with_profiles(profiles)
|
|
151
|
+
.with_overrides(overrides)
|
|
152
|
+
.with_config(ConfigRegistry(config or ())))
|
|
40
153
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
logging.info("Initializing pico-ioc...")
|
|
154
|
+
combined_exclude = _build_exclude(exclude, auto_exclude_caller, root_name=root_name)
|
|
155
|
+
builder.add_scan_package(root_package, exclude=combined_exclude)
|
|
44
156
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
157
|
+
if auto_scan:
|
|
158
|
+
for pkg in auto_scan:
|
|
159
|
+
try:
|
|
160
|
+
mod = importlib.import_module(pkg)
|
|
161
|
+
scan_exclude = _combine_excludes(exclude, auto_scan_exclude)
|
|
162
|
+
builder.add_scan_package(mod, exclude=scan_exclude)
|
|
163
|
+
except ImportError as e:
|
|
164
|
+
msg = f"pico-ioc: auto_scan package not found: {pkg}"
|
|
165
|
+
if strict_autoscan:
|
|
166
|
+
logging.error(msg)
|
|
167
|
+
raise e
|
|
168
|
+
logging.warning(msg)
|
|
169
|
+
|
|
170
|
+
container = builder.build()
|
|
171
|
+
|
|
172
|
+
# Activate new context atomically
|
|
173
|
+
new_ctx = _state.ContainerContext(container=container, fingerprint=fp, root_name=root_name)
|
|
174
|
+
_state.set_context(new_ctx)
|
|
175
|
+
return container
|
|
52
176
|
|
|
53
|
-
|
|
54
|
-
|
|
177
|
+
def scope(
|
|
178
|
+
*, modules: Iterable[Any] = (), roots: Iterable[type] = (), profiles: Optional[list[str]] = None,
|
|
179
|
+
overrides: Optional[Dict[Any, Any]] = None, base: Optional[PicoContainer] = None,
|
|
180
|
+
include_tags: Optional[set[str]] = None, exclude_tags: Optional[set[str]] = None,
|
|
181
|
+
strict: bool = True, lazy: bool = True,
|
|
182
|
+
) -> PicoContainer:
|
|
183
|
+
builder = PicoContainerBuilder()
|
|
55
184
|
|
|
56
|
-
|
|
57
|
-
|
|
185
|
+
if base is not None and not strict:
|
|
186
|
+
base_providers = getattr(base, "_providers", {})
|
|
187
|
+
builder._providers.update(base_providers)
|
|
188
|
+
if profiles is None:
|
|
189
|
+
builder.with_profiles(list(getattr(base, "_active_profiles", ())))
|
|
58
190
|
|
|
59
|
-
|
|
191
|
+
builder.with_profiles(profiles)\
|
|
192
|
+
.with_overrides(overrides)\
|
|
193
|
+
.with_tag_filters(include=include_tags, exclude=exclude_tags)\
|
|
194
|
+
.with_roots(roots)
|
|
60
195
|
|
|
61
|
-
|
|
196
|
+
for m in modules:
|
|
197
|
+
builder.add_scan_package(m)
|
|
62
198
|
|
|
63
|
-
|
|
64
|
-
_state._container = container
|
|
65
|
-
_state._root_name = root_name
|
|
66
|
-
return container
|
|
199
|
+
built_container = builder.with_eager(not lazy).build()
|
|
67
200
|
|
|
201
|
+
scoped_container = ScopedContainer(base=base, strict=strict, built_container=built_container)
|
|
68
202
|
|
|
69
|
-
def scope(
|
|
70
|
-
*,
|
|
71
|
-
modules: Iterable[Any] = (),
|
|
72
|
-
roots: Iterable[type] = (),
|
|
73
|
-
overrides: Optional[Dict[Any, Any]] = None,
|
|
74
|
-
base: Optional[PicoContainer] = None,
|
|
75
|
-
include: Optional[set[str]] = None, # tag include (any-match)
|
|
76
|
-
exclude: Optional[set[str]] = None, # tag exclude (any-match)
|
|
77
|
-
strict: bool = True,
|
|
78
|
-
lazy: bool = True, # if True -> do NOT instantiate roots here
|
|
79
|
-
) -> PicoContainer:
|
|
80
|
-
"""
|
|
81
|
-
Build a lightweight container: scan, apply overrides, filter by tags, prune
|
|
82
|
-
to the dependency subgraph reachable from `roots`, and (optionally) instantiate roots.
|
|
83
|
-
- No global eager.
|
|
84
|
-
- If strict=False and base is provided, missing keys fall back to base.
|
|
85
|
-
"""
|
|
86
|
-
c = _ScopedContainer(base=base, strict=strict)
|
|
87
|
-
|
|
88
|
-
logging.info("Initializing pico-ioc scope...")
|
|
89
|
-
with _scanning_flag():
|
|
90
|
-
for m in modules:
|
|
91
|
-
scan_and_configure(m, c, exclude=None, plugins=())
|
|
92
|
-
|
|
93
|
-
if overrides:
|
|
94
|
-
_apply_overrides(c, overrides)
|
|
95
|
-
|
|
96
|
-
# Tag filter (apply BEFORE reachability pruning)
|
|
97
|
-
def _tag_ok(meta: dict) -> bool:
|
|
98
|
-
if include and not set(include).intersection(meta.get("tags", ())):
|
|
99
|
-
return False
|
|
100
|
-
if exclude and set(exclude).intersection(meta.get("tags", ())):
|
|
101
|
-
return False
|
|
102
|
-
return True
|
|
103
|
-
|
|
104
|
-
c._providers = {k: v for k, v in c._providers.items() if _tag_ok(v)} # type: ignore[attr-defined]
|
|
105
|
-
|
|
106
|
-
# Reachability from roots (subgraph) + keep overrides
|
|
107
|
-
allowed = _compute_allowed_subgraph(c, roots)
|
|
108
|
-
keep_keys: set[Any] = set(allowed) | (set(overrides.keys()) if overrides else set())
|
|
109
|
-
c._providers = {k: v for k, v in c._providers.items() if k in keep_keys} # type: ignore[attr-defined]
|
|
110
|
-
|
|
111
|
-
# Instantiate roots only when NOT lazy
|
|
112
203
|
if not lazy:
|
|
113
204
|
from .proxy import ComponentProxy
|
|
114
205
|
for rk in roots or ():
|
|
115
206
|
try:
|
|
116
|
-
obj =
|
|
207
|
+
obj = scoped_container.get(rk)
|
|
117
208
|
if isinstance(obj, ComponentProxy):
|
|
118
209
|
_ = obj._get_real_object()
|
|
119
210
|
except NameError:
|
|
120
|
-
if strict:
|
|
121
|
-
raise
|
|
122
|
-
# non-strict: skip missing root
|
|
123
|
-
continue
|
|
211
|
+
if strict: raise
|
|
124
212
|
|
|
125
213
|
logging.info("Scope container ready.")
|
|
126
|
-
return
|
|
127
|
-
|
|
128
|
-
# -------------------- helpers --------------------
|
|
129
|
-
|
|
130
|
-
def _apply_overrides(container: PicoContainer, overrides: Dict[Any, Any]) -> None:
|
|
131
|
-
for key, val in overrides.items():
|
|
132
|
-
lazy = False
|
|
133
|
-
if isinstance(val, tuple) and len(val) == 2 and callable(val[0]) and isinstance(val[1], bool):
|
|
134
|
-
provider = val[0]
|
|
135
|
-
lazy = val[1]
|
|
136
|
-
elif callable(val):
|
|
137
|
-
provider = val
|
|
138
|
-
else:
|
|
139
|
-
def provider(v=val):
|
|
140
|
-
return v
|
|
141
|
-
container.bind(key, provider, lazy=lazy)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def _build_exclude(
|
|
145
|
-
exclude: Optional[Callable[[str], bool]],
|
|
146
|
-
auto_exclude_caller: bool,
|
|
147
|
-
*,
|
|
148
|
-
root_name: Optional[str] = None,
|
|
149
|
-
) -> Optional[Callable[[str], bool]]:
|
|
150
|
-
if not auto_exclude_caller:
|
|
151
|
-
return exclude
|
|
152
|
-
|
|
153
|
-
caller = _get_caller_module_name()
|
|
154
|
-
if not caller:
|
|
155
|
-
return exclude
|
|
156
|
-
|
|
157
|
-
def _under_root(mod: str) -> bool:
|
|
158
|
-
return bool(root_name) and (mod == root_name or mod.startswith(root_name + "."))
|
|
159
|
-
|
|
160
|
-
if exclude is None:
|
|
161
|
-
return lambda mod, _caller=caller: (mod == _caller) and not _under_root(mod)
|
|
214
|
+
return scoped_container
|
|
162
215
|
|
|
163
|
-
prev = exclude
|
|
164
|
-
return lambda mod, _caller=caller, _prev=prev: (((mod == _caller) and not _under_root(mod)) or _prev(mod))
|
|
165
216
|
|
|
166
217
|
|
|
167
|
-
def
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
# frame -> _get_caller_module_name -> _build_exclude -> init
|
|
171
|
-
if f and f.f_back and f.f_back.f_back and f.f_back.f_back.f_back:
|
|
172
|
-
mod = inspect.getmodule(f.f_back.f_back.f_back)
|
|
173
|
-
return getattr(mod, "__name__", None)
|
|
174
|
-
except Exception:
|
|
175
|
-
pass
|
|
176
|
-
return None
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
def _run_hooks(
|
|
180
|
-
plugins: Tuple[PicoPlugin, ...],
|
|
181
|
-
hook_name: str,
|
|
182
|
-
container: PicoContainer,
|
|
183
|
-
binder: Binder,
|
|
184
|
-
) -> None:
|
|
185
|
-
for pl in plugins:
|
|
186
|
-
try:
|
|
187
|
-
fn = getattr(pl, hook_name, None)
|
|
188
|
-
if fn:
|
|
189
|
-
fn(container, binder)
|
|
190
|
-
except Exception:
|
|
191
|
-
logging.exception("Plugin %s failed", hook_name)
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
@contextmanager
|
|
195
|
-
def _scanning_flag():
|
|
196
|
-
tok = _state._scanning.set(True)
|
|
197
|
-
try:
|
|
198
|
-
yield
|
|
199
|
-
finally:
|
|
200
|
-
_state._scanning.reset(tok)
|
|
201
|
-
|
|
202
|
-
def _compute_allowed_subgraph(container: PicoContainer, roots: Iterable[type]) -> set:
|
|
203
|
-
"""
|
|
204
|
-
Traverse constructor annotations from roots to collect reachable provider keys.
|
|
205
|
-
Includes implementations for collection injections (list[T]/tuple[T]).
|
|
206
|
-
"""
|
|
207
|
-
from .resolver import _get_hints
|
|
208
|
-
from .container import _is_compatible # structural / subclass check
|
|
209
|
-
import inspect
|
|
210
|
-
from typing import get_origin, get_args, Annotated
|
|
211
|
-
|
|
212
|
-
allowed: set[Any] = set()
|
|
213
|
-
stack = list(roots or ())
|
|
214
|
-
|
|
215
|
-
# Helper: add all provider keys whose class is compatible with `base`
|
|
216
|
-
def _add_impls_for_base(base_t):
|
|
217
|
-
for prov_key, meta in container._providers.items(): # type: ignore[attr-defined]
|
|
218
|
-
cls = prov_key if isinstance(prov_key, type) else None
|
|
219
|
-
if cls is None:
|
|
220
|
-
continue
|
|
221
|
-
if _is_compatible(cls, base_t):
|
|
222
|
-
if prov_key not in allowed:
|
|
223
|
-
allowed.add(prov_key)
|
|
224
|
-
stack.append(prov_key)
|
|
225
|
-
|
|
226
|
-
while stack:
|
|
227
|
-
k = stack.pop()
|
|
228
|
-
if k in allowed:
|
|
229
|
-
continue
|
|
230
|
-
allowed.add(k)
|
|
231
|
-
|
|
232
|
-
cls = k if isinstance(k, type) else None
|
|
233
|
-
if cls is None or not container.has(k):
|
|
234
|
-
# not a class or not currently bound → no edges to follow
|
|
235
|
-
continue
|
|
236
|
-
|
|
237
|
-
try:
|
|
238
|
-
sig = inspect.signature(cls.__init__)
|
|
239
|
-
except Exception:
|
|
240
|
-
continue
|
|
218
|
+
def container_fingerprint() -> Optional[tuple]:
|
|
219
|
+
ctx = _state.get_context()
|
|
220
|
+
return ctx.fingerprint if ctx else None
|
|
241
221
|
|
|
242
|
-
hints = _get_hints(cls.__init__, owner_cls=cls)
|
|
243
|
-
for pname, param in sig.parameters.items():
|
|
244
|
-
if pname == "self":
|
|
245
|
-
continue
|
|
246
|
-
ann = hints.get(pname, param.annotation)
|
|
247
|
-
|
|
248
|
-
origin = get_origin(ann) or ann
|
|
249
|
-
if origin in (list, tuple):
|
|
250
|
-
inner = (get_args(ann) or (object,))[0]
|
|
251
|
-
if get_origin(inner) is Annotated:
|
|
252
|
-
inner = (get_args(inner) or (object,))[0]
|
|
253
|
-
# We don’t know exact impls yet, so:
|
|
254
|
-
if isinstance(inner, type):
|
|
255
|
-
# keep the base “type” in allowed for clarity
|
|
256
|
-
allowed.add(inner)
|
|
257
|
-
# And include ALL implementations present in providers
|
|
258
|
-
_add_impls_for_base(inner)
|
|
259
|
-
continue
|
|
260
|
-
|
|
261
|
-
if isinstance(ann, type):
|
|
262
|
-
stack.append(ann)
|
|
263
|
-
elif container.has(pname):
|
|
264
|
-
stack.append(pname)
|
|
265
|
-
|
|
266
|
-
return allowed
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
class _ScopedContainer(PicoContainer):
|
|
270
|
-
def __init__(self, base: Optional[PicoContainer], strict: bool):
|
|
271
|
-
super().__init__()
|
|
272
|
-
self._base = base
|
|
273
|
-
self._strict = strict
|
|
274
|
-
|
|
275
|
-
# allow `with pico_ioc.scope(...) as c:`
|
|
276
|
-
def __enter__(self):
|
|
277
|
-
return self
|
|
278
|
-
|
|
279
|
-
# no resource suppression; placeholder for future cleanup/shutdown
|
|
280
|
-
def __exit__(self, exc_type, exc, tb):
|
|
281
|
-
return False
|
|
282
|
-
|
|
283
|
-
def get(self, key: Any):
|
|
284
|
-
try:
|
|
285
|
-
return super().get(key)
|
|
286
|
-
except NameError as e:
|
|
287
|
-
if not self._strict and self._base is not None and self._base.has(key):
|
|
288
|
-
return self._base.get(key)
|
|
289
|
-
raise e
|