pico-ioc 1.2.0__py3-none-any.whl → 1.3.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 +17 -4
- pico_ioc/_state.py +30 -0
- pico_ioc/_version.py +1 -1
- pico_ioc/api.py +186 -235
- pico_ioc/builder.py +242 -0
- pico_ioc/container.py +57 -29
- pico_ioc/decorators.py +63 -8
- pico_ioc/interceptors.py +50 -0
- pico_ioc/plugins.py +17 -1
- pico_ioc/policy.py +332 -0
- pico_ioc/proxy.py +41 -1
- pico_ioc/resolver.py +45 -40
- pico_ioc/scanner.py +74 -101
- pico_ioc/utils.py +25 -0
- {pico_ioc-1.2.0.dist-info → pico_ioc-1.3.0.dist-info}/METADATA +59 -16
- pico_ioc-1.3.0.dist-info/RECORD +20 -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.3.0.dist-info}/WHEEL +0 -0
- {pico_ioc-1.2.0.dist-info → pico_ioc-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {pico_ioc-1.2.0.dist-info → pico_ioc-1.3.0.dist-info}/top_level.txt +0 -0
pico_ioc/__init__.py
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
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 .decorators import
|
|
8
|
+
from .decorators import (
|
|
9
|
+
component, factory_component, provides, plugin,
|
|
10
|
+
Qualifier, qualifier,
|
|
11
|
+
on_missing, primary, conditional, interceptor,
|
|
12
|
+
)
|
|
10
13
|
from .plugins import PicoPlugin
|
|
11
14
|
from .resolver import Resolver
|
|
12
|
-
from .api import init, reset, scope
|
|
13
|
-
from .proxy import ComponentProxy
|
|
15
|
+
from .api import init, reset, scope, container_fingerprint
|
|
16
|
+
from .proxy import ComponentProxy, IoCProxy
|
|
17
|
+
from .interceptors import Invocation, MethodInterceptor, ContainerInterceptor
|
|
14
18
|
|
|
15
19
|
__all__ = [
|
|
16
20
|
"__version__",
|
|
@@ -18,15 +22,24 @@ __all__ = [
|
|
|
18
22
|
"Binder",
|
|
19
23
|
"PicoPlugin",
|
|
20
24
|
"ComponentProxy",
|
|
25
|
+
"IoCProxy",
|
|
26
|
+
"Invocation",
|
|
27
|
+
"MethodInterceptor",
|
|
28
|
+
"ContainerInterceptor",
|
|
21
29
|
"init",
|
|
22
30
|
"scope",
|
|
23
31
|
"reset",
|
|
32
|
+
"container_fingerprint",
|
|
24
33
|
"component",
|
|
25
34
|
"factory_component",
|
|
26
35
|
"provides",
|
|
27
36
|
"plugin",
|
|
28
37
|
"Qualifier",
|
|
29
38
|
"qualifier",
|
|
39
|
+
"on_missing",
|
|
40
|
+
"primary",
|
|
41
|
+
"conditional",
|
|
42
|
+
"interceptor",
|
|
30
43
|
"Resolver",
|
|
31
44
|
]
|
|
32
45
|
|
pico_ioc/_state.py
CHANGED
|
@@ -1,10 +1,40 @@
|
|
|
1
1
|
# pico_ioc/_state.py
|
|
2
2
|
from contextvars import ContextVar
|
|
3
3
|
from typing import Optional
|
|
4
|
+
from contextlib import contextmanager
|
|
4
5
|
|
|
5
6
|
_scanning: ContextVar[bool] = ContextVar("pico_scanning", default=False)
|
|
6
7
|
_resolving: ContextVar[bool] = ContextVar("pico_resolving", default=False)
|
|
7
8
|
|
|
8
9
|
_container = None
|
|
9
10
|
_root_name: Optional[str] = None
|
|
11
|
+
_fingerprint: Optional[tuple] = None
|
|
12
|
+
_fp_observed: bool = False
|
|
10
13
|
|
|
14
|
+
@contextmanager
|
|
15
|
+
def scanning_flag():
|
|
16
|
+
"""Context manager: mark scanning=True within the block."""
|
|
17
|
+
tok = _scanning.set(True)
|
|
18
|
+
try:
|
|
19
|
+
yield
|
|
20
|
+
finally:
|
|
21
|
+
_scanning.reset(tok)
|
|
22
|
+
|
|
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.3.0'
|
pico_ioc/api.py
CHANGED
|
@@ -1,283 +1,231 @@
|
|
|
1
|
-
# pico_ioc/api.py
|
|
2
|
-
|
|
1
|
+
# src/pico_ioc/api.py
|
|
3
2
|
from __future__ import annotations
|
|
4
3
|
|
|
5
|
-
import inspect
|
|
4
|
+
import inspect as _inspect
|
|
5
|
+
import importlib
|
|
6
6
|
import logging
|
|
7
|
-
|
|
8
|
-
from
|
|
7
|
+
import os
|
|
8
|
+
from types import ModuleType
|
|
9
|
+
from typing import Callable, Optional, Tuple, Any, Dict, Iterable, Sequence
|
|
9
10
|
|
|
10
|
-
from .container import PicoContainer
|
|
11
|
+
from .container import PicoContainer
|
|
11
12
|
from .plugins import PicoPlugin
|
|
12
|
-
from .scanner import scan_and_configure
|
|
13
13
|
from . import _state
|
|
14
|
+
from .builder import PicoContainerBuilder
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
# The only helpers left are those directly related to the public API signature or fingerprinting
|
|
16
17
|
def reset() -> None:
|
|
17
|
-
"""Reset the global container."""
|
|
18
18
|
_state._container = None
|
|
19
19
|
_state._root_name = None
|
|
20
|
+
_state.set_fingerprint(None)
|
|
20
21
|
|
|
22
|
+
def _combine_excludes(a: Optional[Callable[[str], bool]], b: Optional[Callable[[str], bool]]):
|
|
23
|
+
if not a and not b: return None
|
|
24
|
+
if a and not b: return a
|
|
25
|
+
if b and not a: return b
|
|
26
|
+
return lambda mod, _a=a, _b=b: _a(mod) or _b(mod)
|
|
21
27
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
# -------- fingerprint helpers --------
|
|
29
|
+
def _callable_id(cb) -> tuple:
|
|
30
|
+
try:
|
|
31
|
+
mod = getattr(cb, "__module__", None)
|
|
32
|
+
qn = getattr(cb, "__qualname__", None)
|
|
33
|
+
code = getattr(cb, "__code__", None)
|
|
34
|
+
fn_line = getattr(code, "co_firstlineno", None) if code else None
|
|
35
|
+
return (mod, qn, fn_line)
|
|
36
|
+
except Exception:
|
|
37
|
+
return (repr(cb),)
|
|
38
|
+
|
|
39
|
+
def _plugins_id(plugins: Tuple[PicoPlugin, ...]) -> tuple:
|
|
40
|
+
out = [(type(p).__module__, type(p).__qualname__) for p in plugins or ()]
|
|
41
|
+
return tuple(sorted(out))
|
|
42
|
+
|
|
43
|
+
def _normalize_for_fp(value):
|
|
44
|
+
if isinstance(value, ModuleType):
|
|
45
|
+
return getattr(value, "__name__", repr(value))
|
|
46
|
+
if isinstance(value, (tuple, list)):
|
|
47
|
+
return tuple(_normalize_for_fp(v) for v in value)
|
|
48
|
+
if isinstance(value, set):
|
|
49
|
+
return tuple(sorted(_normalize_for_fp(v) for v in value))
|
|
50
|
+
if callable(value):
|
|
51
|
+
return ("callable",) + _callable_id(value)
|
|
52
|
+
return value
|
|
53
|
+
|
|
54
|
+
_FP_EXCLUDE_KEYS = set()
|
|
55
|
+
|
|
56
|
+
def _normalize_overrides_for_fp(overrides: Optional[Dict[Any, Any]]) -> tuple:
|
|
57
|
+
if not overrides:
|
|
58
|
+
return ()
|
|
59
|
+
items = []
|
|
60
|
+
for k, v in overrides.items():
|
|
61
|
+
nk = _normalize_for_fp(k)
|
|
62
|
+
nv = _normalize_for_fp(v)
|
|
63
|
+
items.append((nk, nv))
|
|
64
|
+
return tuple(sorted(items))
|
|
65
|
+
|
|
66
|
+
def _make_fingerprint_from_signature(locals_in_init: dict) -> tuple:
|
|
67
|
+
sig = _inspect.signature(init)
|
|
68
|
+
entries = []
|
|
69
|
+
for name in sig.parameters.keys():
|
|
70
|
+
if name in _FP_EXCLUDE_KEYS: continue
|
|
71
|
+
if name == "root_package":
|
|
72
|
+
rp = locals_in_init.get("root_package")
|
|
73
|
+
root_name = rp if isinstance(rp, str) else getattr(rp, "__name__", None)
|
|
74
|
+
entries.append(("root", root_name))
|
|
75
|
+
continue
|
|
76
|
+
val = locals_in_init.get(name, None)
|
|
77
|
+
if name == "plugins":
|
|
78
|
+
val = _plugins_id(val or ())
|
|
79
|
+
elif name in ("profiles", "auto_scan"):
|
|
80
|
+
val = tuple(val or ())
|
|
81
|
+
elif name in ("exclude", "auto_scan_exclude"):
|
|
82
|
+
val = _callable_id(val) if val else None
|
|
83
|
+
elif name == "overrides":
|
|
84
|
+
val = _normalize_overrides_for_fp(val)
|
|
85
|
+
else:
|
|
86
|
+
val = _normalize_for_fp(val)
|
|
87
|
+
entries.append((name, val))
|
|
88
|
+
return tuple(sorted(entries))
|
|
33
89
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
90
|
+
# -------- container reuse and caller exclusion helpers --------
|
|
91
|
+
def _maybe_reuse_existing(fp: tuple, overrides: Optional[Dict[Any, Any]]) -> Optional[PicoContainer]:
|
|
92
|
+
if _state.get_fingerprint() == fp:
|
|
37
93
|
return _state._container
|
|
94
|
+
return None
|
|
38
95
|
|
|
39
|
-
|
|
96
|
+
def _build_exclude(
|
|
97
|
+
exclude: Optional[Callable[[str], bool]], auto_exclude_caller: bool, *, root_name: Optional[str] = None
|
|
98
|
+
) -> Optional[Callable[[str], bool]]:
|
|
99
|
+
if not auto_exclude_caller: return exclude
|
|
100
|
+
caller = _get_caller_module_name()
|
|
101
|
+
if not caller: return exclude
|
|
102
|
+
def _under_root(mod: str) -> bool:
|
|
103
|
+
return bool(root_name) and (mod == root_name or mod.startswith(root_name + "."))
|
|
104
|
+
if exclude is None:
|
|
105
|
+
return lambda mod, _caller=caller: (mod == _caller) and not _under_root(mod)
|
|
106
|
+
return lambda mod, _caller=caller, _prev=exclude: (((mod == _caller) and not _under_root(mod)) or _prev(mod))
|
|
40
107
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
108
|
+
def _get_caller_module_name() -> Optional[str]:
|
|
109
|
+
try:
|
|
110
|
+
f = _inspect.currentframe()
|
|
111
|
+
# Stack: _get_caller -> _build_exclude -> init -> caller
|
|
112
|
+
if f and f.f_back and f.f_back.f_back and f.f_back.f_back.f_back:
|
|
113
|
+
mod = _inspect.getmodule(f.f_back.f_back.f_back)
|
|
114
|
+
return getattr(mod, "__name__", None)
|
|
115
|
+
except Exception:
|
|
116
|
+
pass
|
|
117
|
+
return None
|
|
44
118
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
119
|
+
# ---------------- public API ----------------
|
|
120
|
+
def init(
|
|
121
|
+
root_package, *, profiles: Optional[list[str]] = None, exclude: Optional[Callable[[str], bool]] = None,
|
|
122
|
+
auto_exclude_caller: bool = True, plugins: Tuple[PicoPlugin, ...] = (), reuse: bool = True,
|
|
123
|
+
overrides: Optional[Dict[Any, Any]] = None, auto_scan: Sequence[str] = (),
|
|
124
|
+
auto_scan_exclude: Optional[Callable[[str], bool]] = None, strict_autoscan: bool = False,
|
|
125
|
+
) -> PicoContainer:
|
|
126
|
+
root_name = root_package if isinstance(root_package, str) else getattr(root_package, "__name__", None)
|
|
127
|
+
fp = _make_fingerprint_from_signature(locals())
|
|
52
128
|
|
|
53
|
-
if
|
|
54
|
-
|
|
129
|
+
if reuse:
|
|
130
|
+
reused = _maybe_reuse_existing(fp, overrides)
|
|
131
|
+
if reused is not None:
|
|
132
|
+
return reused
|
|
55
133
|
|
|
56
|
-
|
|
57
|
-
_run_hooks(plugins, "before_eager", container, binder)
|
|
134
|
+
builder = PicoContainerBuilder().with_plugins(plugins).with_profiles(profiles).with_overrides(overrides)
|
|
58
135
|
|
|
59
|
-
|
|
136
|
+
combined_exclude = _build_exclude(exclude, auto_exclude_caller, root_name=root_name)
|
|
137
|
+
builder.add_scan_package(root_package, exclude=combined_exclude)
|
|
60
138
|
|
|
61
|
-
|
|
139
|
+
if auto_scan:
|
|
140
|
+
for pkg in auto_scan:
|
|
141
|
+
try:
|
|
142
|
+
mod = importlib.import_module(pkg)
|
|
143
|
+
scan_exclude = _combine_excludes(exclude, auto_scan_exclude)
|
|
144
|
+
builder.add_scan_package(mod, exclude=scan_exclude)
|
|
145
|
+
except ImportError as e:
|
|
146
|
+
msg = f"pico-ioc: auto_scan package not found: {pkg}"
|
|
147
|
+
if strict_autoscan:
|
|
148
|
+
logging.error(msg)
|
|
149
|
+
raise e
|
|
150
|
+
logging.warning(msg)
|
|
151
|
+
|
|
152
|
+
container = builder.build()
|
|
62
153
|
|
|
63
|
-
logging.info("Container configured and ready.")
|
|
64
154
|
_state._container = container
|
|
65
155
|
_state._root_name = root_name
|
|
156
|
+
_state.set_fingerprint(fp)
|
|
66
157
|
return container
|
|
67
158
|
|
|
68
|
-
|
|
69
159
|
def scope(
|
|
70
|
-
*,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
160
|
+
*, modules: Iterable[Any] = (), roots: Iterable[type] = (), profiles: Optional[list[str]] = None,
|
|
161
|
+
overrides: Optional[Dict[Any, Any]] = None, base: Optional[PicoContainer] = None,
|
|
162
|
+
include_tags: Optional[set[str]] = None, exclude_tags: Optional[set[str]] = None,
|
|
163
|
+
strict: bool = True, lazy: bool = True,
|
|
79
164
|
) -> PicoContainer:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
165
|
+
builder = PicoContainerBuilder()
|
|
166
|
+
|
|
167
|
+
if base is not None and not strict:
|
|
168
|
+
base_providers = getattr(base, "_providers", {})
|
|
169
|
+
builder._providers.update(base_providers)
|
|
170
|
+
if profiles is None:
|
|
171
|
+
builder.with_profiles(list(getattr(base, "_active_profiles", ())))
|
|
172
|
+
|
|
173
|
+
builder.with_profiles(profiles)\
|
|
174
|
+
.with_overrides(overrides)\
|
|
175
|
+
.with_tag_filters(include=include_tags, exclude=exclude_tags)\
|
|
176
|
+
.with_roots(roots)
|
|
177
|
+
|
|
178
|
+
for m in modules:
|
|
179
|
+
builder.add_scan_package(m)
|
|
180
|
+
|
|
181
|
+
built_container = builder.build()
|
|
182
|
+
|
|
183
|
+
scoped_container = _ScopedContainer(base=base, strict=strict, built_container=built_container)
|
|
184
|
+
|
|
112
185
|
if not lazy:
|
|
113
186
|
from .proxy import ComponentProxy
|
|
114
187
|
for rk in roots or ():
|
|
115
188
|
try:
|
|
116
|
-
obj =
|
|
189
|
+
obj = scoped_container.get(rk)
|
|
117
190
|
if isinstance(obj, ComponentProxy):
|
|
118
191
|
_ = obj._get_real_object()
|
|
119
192
|
except NameError:
|
|
120
|
-
if strict:
|
|
121
|
-
raise
|
|
122
|
-
# non-strict: skip missing root
|
|
123
|
-
continue
|
|
193
|
+
if strict: raise
|
|
124
194
|
|
|
125
195
|
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
|
-
|
|
196
|
+
return scoped_container
|
|
143
197
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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)
|
|
162
|
-
|
|
163
|
-
prev = exclude
|
|
164
|
-
return lambda mod, _caller=caller, _prev=prev: (((mod == _caller) and not _under_root(mod)) or _prev(mod))
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
def _get_caller_module_name() -> Optional[str]:
|
|
168
|
-
try:
|
|
169
|
-
f = inspect.currentframe()
|
|
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)
|
|
198
|
+
class _ScopedContainer(PicoContainer):
|
|
199
|
+
def __init__(self, built_container: PicoContainer, base: Optional[PicoContainer], strict: bool):
|
|
200
|
+
super().__init__(providers=getattr(built_container, "_providers", {}).copy())
|
|
201
201
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
|
241
|
-
|
|
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
|
|
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)}
|
|
267
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)
|
|
268
215
|
|
|
269
|
-
class _ScopedContainer(PicoContainer):
|
|
270
|
-
def __init__(self, base: Optional[PicoContainer], strict: bool):
|
|
271
|
-
super().__init__()
|
|
272
216
|
self._base = base
|
|
273
217
|
self._strict = strict
|
|
274
218
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
return self
|
|
219
|
+
if base:
|
|
220
|
+
self._singletons.update(getattr(base, "_singletons", {}))
|
|
278
221
|
|
|
279
|
-
|
|
280
|
-
def __exit__(self, exc_type, exc, tb):
|
|
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)
|
|
281
229
|
return False
|
|
282
230
|
|
|
283
231
|
def get(self, key: Any):
|
|
@@ -287,3 +235,6 @@ class _ScopedContainer(PicoContainer):
|
|
|
287
235
|
if not self._strict and self._base is not None and self._base.has(key):
|
|
288
236
|
return self._base.get(key)
|
|
289
237
|
raise e
|
|
238
|
+
|
|
239
|
+
def container_fingerprint() -> Optional[tuple]:
|
|
240
|
+
return _state.get_fingerprint()
|