pico-ioc 1.3.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 +13 -0
- pico_ioc/_state.py +60 -25
- pico_ioc/_version.py +1 -1
- pico_ioc/api.py +36 -55
- pico_ioc/builder.py +93 -41
- pico_ioc/config.py +332 -0
- pico_ioc/container.py +30 -11
- pico_ioc/decorators.py +30 -6
- pico_ioc/interceptors.py +13 -7
- pico_ioc/policy.py +102 -189
- pico_ioc/proxy.py +30 -18
- pico_ioc/resolver.py +28 -25
- pico_ioc/scanner.py +21 -21
- pico_ioc/scope.py +46 -0
- {pico_ioc-1.3.0.dist-info → pico_ioc-1.4.0.dist-info}/METADATA +7 -1
- pico_ioc-1.4.0.dist-info/RECORD +22 -0
- pico_ioc-1.3.0.dist-info/RECORD +0 -20
- {pico_ioc-1.3.0.dist-info → pico_ioc-1.4.0.dist-info}/WHEEL +0 -0
- {pico_ioc-1.3.0.dist-info → pico_ioc-1.4.0.dist-info}/licenses/LICENSE +0 -0
- {pico_ioc-1.3.0.dist-info → pico_ioc-1.4.0.dist-info}/top_level.txt +0 -0
pico_ioc/__init__.py
CHANGED
|
@@ -5,6 +5,7 @@ except Exception:
|
|
|
5
5
|
__version__ = "0.0.0"
|
|
6
6
|
|
|
7
7
|
from .container import PicoContainer, Binder
|
|
8
|
+
from .scope import ScopedContainer
|
|
8
9
|
from .decorators import (
|
|
9
10
|
component, factory_component, provides, plugin,
|
|
10
11
|
Qualifier, qualifier,
|
|
@@ -15,6 +16,10 @@ from .resolver import Resolver
|
|
|
15
16
|
from .api import init, reset, scope, container_fingerprint
|
|
16
17
|
from .proxy import ComponentProxy, IoCProxy
|
|
17
18
|
from .interceptors import Invocation, MethodInterceptor, ContainerInterceptor
|
|
19
|
+
from .config import (
|
|
20
|
+
config_component, EnvSource, FileSource,
|
|
21
|
+
Env, File, Path, Value,
|
|
22
|
+
)
|
|
18
23
|
|
|
19
24
|
__all__ = [
|
|
20
25
|
"__version__",
|
|
@@ -41,5 +46,13 @@ __all__ = [
|
|
|
41
46
|
"conditional",
|
|
42
47
|
"interceptor",
|
|
43
48
|
"Resolver",
|
|
49
|
+
"ScopedContainer",
|
|
50
|
+
"config_component",
|
|
51
|
+
"EnvSource",
|
|
52
|
+
"FileSource",
|
|
53
|
+
"Env",
|
|
54
|
+
"File",
|
|
55
|
+
"Path",
|
|
56
|
+
"Value",
|
|
44
57
|
]
|
|
45
58
|
|
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.4.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(
|
|
@@ -122,6 +135,7 @@ def init(
|
|
|
122
135
|
auto_exclude_caller: bool = True, plugins: Tuple[PicoPlugin, ...] = (), reuse: bool = True,
|
|
123
136
|
overrides: Optional[Dict[Any, Any]] = None, auto_scan: Sequence[str] = (),
|
|
124
137
|
auto_scan_exclude: Optional[Callable[[str], bool]] = None, strict_autoscan: bool = False,
|
|
138
|
+
config: Sequence[ConfigSource] = (),
|
|
125
139
|
) -> PicoContainer:
|
|
126
140
|
root_name = root_package if isinstance(root_package, str) else getattr(root_package, "__name__", None)
|
|
127
141
|
fp = _make_fingerprint_from_signature(locals())
|
|
@@ -131,7 +145,11 @@ def init(
|
|
|
131
145
|
if reused is not None:
|
|
132
146
|
return reused
|
|
133
147
|
|
|
134
|
-
builder = PicoContainerBuilder()
|
|
148
|
+
builder = (PicoContainerBuilder()
|
|
149
|
+
.with_plugins(plugins)
|
|
150
|
+
.with_profiles(profiles)
|
|
151
|
+
.with_overrides(overrides)
|
|
152
|
+
.with_config(ConfigRegistry(config or ())))
|
|
135
153
|
|
|
136
154
|
combined_exclude = _build_exclude(exclude, auto_exclude_caller, root_name=root_name)
|
|
137
155
|
builder.add_scan_package(root_package, exclude=combined_exclude)
|
|
@@ -151,9 +169,9 @@ def init(
|
|
|
151
169
|
|
|
152
170
|
container = builder.build()
|
|
153
171
|
|
|
154
|
-
|
|
155
|
-
_state.
|
|
156
|
-
_state.
|
|
172
|
+
# Activate new context atomically
|
|
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
|
|
|
159
177
|
def scope(
|
|
@@ -178,9 +196,9 @@ def scope(
|
|
|
178
196
|
for m in modules:
|
|
179
197
|
builder.add_scan_package(m)
|
|
180
198
|
|
|
181
|
-
built_container = builder.build()
|
|
199
|
+
built_container = builder.with_eager(not lazy).build()
|
|
182
200
|
|
|
183
|
-
scoped_container =
|
|
201
|
+
scoped_container = ScopedContainer(base=base, strict=strict, built_container=built_container)
|
|
184
202
|
|
|
185
203
|
if not lazy:
|
|
186
204
|
from .proxy import ComponentProxy
|
|
@@ -195,46 +213,9 @@ def scope(
|
|
|
195
213
|
logging.info("Scope container ready.")
|
|
196
214
|
return scoped_container
|
|
197
215
|
|
|
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
|
|
216
|
+
|
|
238
217
|
|
|
239
218
|
def container_fingerprint() -> Optional[tuple]:
|
|
240
|
-
|
|
219
|
+
ctx = _state.get_context()
|
|
220
|
+
return ctx.fingerprint if ctx else None
|
|
221
|
+
|
pico_ioc/builder.py
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# src/pico_ioc/builder.py
|
|
2
2
|
from __future__ import annotations
|
|
3
|
+
|
|
3
4
|
import inspect as _inspect
|
|
4
5
|
import logging
|
|
5
6
|
import os
|
|
6
7
|
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple
|
|
7
8
|
from typing import get_origin, get_args, Annotated
|
|
8
9
|
|
|
9
|
-
# Add missing imports for interceptor types
|
|
10
10
|
from .interceptors import MethodInterceptor, ContainerInterceptor
|
|
11
11
|
from .container import PicoContainer, _is_compatible
|
|
12
12
|
from .policy import apply_policy, _conditional_active
|
|
@@ -14,9 +14,11 @@ from .plugins import PicoPlugin, run_plugin_hook
|
|
|
14
14
|
from .scanner import scan_and_configure
|
|
15
15
|
from .resolver import Resolver, _get_hints
|
|
16
16
|
from . import _state
|
|
17
|
-
|
|
17
|
+
from .config import ConfigRegistry
|
|
18
18
|
|
|
19
19
|
class PicoContainerBuilder:
|
|
20
|
+
"""Configures and builds a PicoContainer. Does not touch global context."""
|
|
21
|
+
|
|
20
22
|
def __init__(self):
|
|
21
23
|
self._scan_plan: List[Tuple[Any, Optional[Callable[[str], bool]], Tuple[PicoPlugin, ...]]] = []
|
|
22
24
|
self._overrides: Dict[Any, Any] = {}
|
|
@@ -27,39 +29,54 @@ class PicoContainerBuilder:
|
|
|
27
29
|
self._roots: Iterable[type] = ()
|
|
28
30
|
self._providers: Dict[Any, Dict] = {}
|
|
29
31
|
self._interceptor_decls: List[Tuple[Any, dict]] = []
|
|
32
|
+
self._eager: bool = True
|
|
33
|
+
self._config_registry: ConfigRegistry | None = None
|
|
34
|
+
|
|
35
|
+
# -------- fluent config --------
|
|
36
|
+
|
|
37
|
+
def with_config(self, registry: ConfigRegistry) -> "PicoContainerBuilder":
|
|
38
|
+
self._config_registry = registry
|
|
39
|
+
return self
|
|
30
40
|
|
|
31
|
-
def with_plugins(self, plugins: Tuple[PicoPlugin, ...]) -> PicoContainerBuilder:
|
|
32
|
-
self._plugins = plugins
|
|
41
|
+
def with_plugins(self, plugins: Tuple[PicoPlugin, ...]) -> "PicoContainerBuilder":
|
|
42
|
+
self._plugins = plugins or ()
|
|
33
43
|
return self
|
|
34
44
|
|
|
35
|
-
def with_profiles(self, profiles: Optional[List[str]]) -> PicoContainerBuilder:
|
|
45
|
+
def with_profiles(self, profiles: Optional[List[str]]) -> "PicoContainerBuilder":
|
|
36
46
|
self._profiles = profiles
|
|
37
47
|
return self
|
|
38
48
|
|
|
39
|
-
def add_scan_package(self, package: Any, exclude: Optional[Callable[[str], bool]] = None) -> PicoContainerBuilder:
|
|
49
|
+
def add_scan_package(self, package: Any, exclude: Optional[Callable[[str], bool]] = None) -> "PicoContainerBuilder":
|
|
40
50
|
self._scan_plan.append((package, exclude, self._plugins))
|
|
41
51
|
return self
|
|
42
52
|
|
|
43
|
-
def with_overrides(self, overrides: Optional[Dict[Any, Any]]) -> PicoContainerBuilder:
|
|
53
|
+
def with_overrides(self, overrides: Optional[Dict[Any, Any]]) -> "PicoContainerBuilder":
|
|
44
54
|
self._overrides = overrides or {}
|
|
45
55
|
return self
|
|
46
56
|
|
|
47
|
-
def with_tag_filters(self, include: Optional[set[str]], exclude: Optional[set[str]]) -> PicoContainerBuilder:
|
|
57
|
+
def with_tag_filters(self, include: Optional[set[str]], exclude: Optional[set[str]]) -> "PicoContainerBuilder":
|
|
48
58
|
self._include_tags = include
|
|
49
59
|
self._exclude_tags = exclude
|
|
50
60
|
return self
|
|
51
61
|
|
|
52
|
-
def with_roots(self, roots: Iterable[type]) -> PicoContainerBuilder:
|
|
53
|
-
self._roots = roots
|
|
62
|
+
def with_roots(self, roots: Iterable[type]) -> "PicoContainerBuilder":
|
|
63
|
+
self._roots = roots or ()
|
|
54
64
|
return self
|
|
55
65
|
|
|
66
|
+
def with_eager(self, eager: bool) -> "PicoContainerBuilder":
|
|
67
|
+
self._eager = bool(eager)
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
# -------- build --------
|
|
71
|
+
|
|
56
72
|
def build(self) -> PicoContainer:
|
|
73
|
+
"""Build and return a fully configured container."""
|
|
57
74
|
requested_profiles = _resolve_profiles(self._profiles)
|
|
58
|
-
|
|
59
|
-
# We now create a single container instance upfront and configure it.
|
|
75
|
+
|
|
60
76
|
container = PicoContainer(providers=self._providers)
|
|
61
77
|
container._active_profiles = tuple(requested_profiles)
|
|
62
|
-
|
|
78
|
+
setattr(container, "_config_registry", self._config_registry)
|
|
79
|
+
|
|
63
80
|
for pkg, exclude, scan_plugins in self._scan_plan:
|
|
64
81
|
with _state.scanning_flag():
|
|
65
82
|
c, f, decls = scan_and_configure(pkg, container, exclude=exclude, plugins=scan_plugins)
|
|
@@ -69,9 +86,9 @@ class PicoContainerBuilder:
|
|
|
69
86
|
_activate_and_build_interceptors(
|
|
70
87
|
container=container,
|
|
71
88
|
interceptor_decls=self._interceptor_decls,
|
|
72
|
-
profiles=requested_profiles
|
|
89
|
+
profiles=requested_profiles,
|
|
73
90
|
)
|
|
74
|
-
|
|
91
|
+
|
|
75
92
|
binder = container.binder()
|
|
76
93
|
|
|
77
94
|
if self._overrides:
|
|
@@ -79,24 +96,30 @@ class PicoContainerBuilder:
|
|
|
79
96
|
|
|
80
97
|
run_plugin_hook(self._plugins, "after_bind", container, binder)
|
|
81
98
|
run_plugin_hook(self._plugins, "before_eager", container, binder)
|
|
99
|
+
|
|
82
100
|
apply_policy(container, profiles=requested_profiles)
|
|
83
101
|
_filter_by_tags(container, self._include_tags, self._exclude_tags)
|
|
102
|
+
|
|
84
103
|
if self._roots:
|
|
85
104
|
_restrict_to_subgraph(container, self._roots, self._overrides)
|
|
86
105
|
|
|
87
106
|
run_plugin_hook(self._plugins, "after_ready", container, binder)
|
|
88
|
-
|
|
107
|
+
|
|
108
|
+
if self._eager:
|
|
109
|
+
container.eager_instantiate_all()
|
|
89
110
|
logging.info("Container configured and ready.")
|
|
90
111
|
return container
|
|
91
112
|
|
|
92
|
-
|
|
93
|
-
#
|
|
94
|
-
|
|
113
|
+
|
|
114
|
+
# ---------------- helpers ----------------
|
|
115
|
+
|
|
116
|
+
def _resolve_profiles(profiles: Optional[List[str]]) -> List[str]:
|
|
95
117
|
if profiles is not None:
|
|
96
118
|
return list(profiles)
|
|
97
119
|
env_val = os.getenv("PICO_PROFILE", "")
|
|
98
120
|
return [p.strip() for p in env_val.split(",") if p.strip()]
|
|
99
121
|
|
|
122
|
+
|
|
100
123
|
def _as_provider(val):
|
|
101
124
|
if isinstance(val, tuple) and len(val) == 2 and callable(val[0]) and isinstance(val[1], bool):
|
|
102
125
|
return val[0], val[1]
|
|
@@ -104,11 +127,13 @@ def _as_provider(val):
|
|
|
104
127
|
return val, False
|
|
105
128
|
return (lambda v=val: v), False
|
|
106
129
|
|
|
130
|
+
|
|
107
131
|
def _apply_overrides(container: PicoContainer, overrides: Dict[Any, Any]) -> None:
|
|
108
132
|
for key, val in overrides.items():
|
|
109
133
|
provider, lazy = _as_provider(val)
|
|
110
134
|
container.bind(key, provider, lazy=lazy)
|
|
111
135
|
|
|
136
|
+
|
|
112
137
|
def _filter_by_tags(container: PicoContainer, include_tags: Optional[set[str]], exclude_tags: Optional[set[str]]) -> None:
|
|
113
138
|
if not include_tags and not exclude_tags:
|
|
114
139
|
return
|
|
@@ -120,12 +145,14 @@ def _filter_by_tags(container: PicoContainer, include_tags: Optional[set[str]],
|
|
|
120
145
|
if exclude_tags and tags.intersection(exclude_tags):
|
|
121
146
|
return False
|
|
122
147
|
return True
|
|
148
|
+
|
|
123
149
|
container._providers = {k: v for k, v in container._providers.items() if _tag_ok(v)}
|
|
124
150
|
|
|
151
|
+
|
|
125
152
|
def _compute_allowed_subgraph(container: PicoContainer, roots: Iterable[type]) -> set:
|
|
126
|
-
allowed: set[Any] = set(roots)
|
|
153
|
+
allowed: set[Any] = set(roots)
|
|
127
154
|
stack = list(roots or ())
|
|
128
|
-
|
|
155
|
+
|
|
129
156
|
def _add_impls_for_base(base_t):
|
|
130
157
|
for prov_key, meta in container._providers.items():
|
|
131
158
|
cls = prov_key if isinstance(prov_key, type) else None
|
|
@@ -136,23 +163,29 @@ def _compute_allowed_subgraph(container: PicoContainer, roots: Iterable[type]) -
|
|
|
136
163
|
|
|
137
164
|
while stack:
|
|
138
165
|
k = stack.pop()
|
|
139
|
-
# if k in allowed: continue # Redundant, add() handles it
|
|
140
166
|
allowed.add(k)
|
|
141
|
-
if isinstance(k, type):
|
|
167
|
+
if isinstance(k, type):
|
|
168
|
+
_add_impls_for_base(k)
|
|
169
|
+
|
|
142
170
|
cls = k if isinstance(k, type) else None
|
|
143
|
-
if cls is None or not container.has(k):
|
|
171
|
+
if cls is None or not container.has(k):
|
|
172
|
+
continue
|
|
173
|
+
|
|
144
174
|
try:
|
|
145
175
|
sig = _inspect.signature(cls.__init__)
|
|
146
176
|
hints = _get_hints(cls.__init__, owner_cls=cls)
|
|
147
177
|
except Exception:
|
|
148
178
|
continue
|
|
179
|
+
|
|
149
180
|
for pname, param in sig.parameters.items():
|
|
150
|
-
if pname == "self":
|
|
181
|
+
if pname == "self":
|
|
182
|
+
continue
|
|
151
183
|
ann = hints.get(pname, param.annotation)
|
|
152
184
|
origin = get_origin(ann) or ann
|
|
153
185
|
if origin in (list, tuple):
|
|
154
186
|
inner = (get_args(ann) or (object,))[0]
|
|
155
|
-
if get_origin(inner) is Annotated:
|
|
187
|
+
if get_origin(inner) is Annotated:
|
|
188
|
+
inner = (get_args(inner) or (object,))[0]
|
|
156
189
|
if isinstance(inner, type):
|
|
157
190
|
if inner not in allowed:
|
|
158
191
|
stack.append(inner)
|
|
@@ -169,38 +202,46 @@ def _restrict_to_subgraph(container: PicoContainer, roots: Iterable[type], overr
|
|
|
169
202
|
keep_keys: set[Any] = allowed | (set(overrides.keys()) if overrides else set())
|
|
170
203
|
container._providers = {k: v for k, v in container._providers.items() if k in keep_keys}
|
|
171
204
|
|
|
205
|
+
|
|
172
206
|
def _activate_and_build_interceptors(
|
|
173
|
-
*, container: PicoContainer, interceptor_decls:
|
|
207
|
+
*, container: PicoContainer, interceptor_decls: List[Tuple[Any, dict]], profiles: List[str],
|
|
174
208
|
) -> None:
|
|
175
209
|
resolver = Resolver(container)
|
|
176
|
-
active:
|
|
177
|
-
activated_method_names:
|
|
178
|
-
activated_container_names:
|
|
179
|
-
skipped_debug:
|
|
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] = []
|
|
180
214
|
|
|
181
215
|
def _interceptor_meta_active(meta: dict) -> bool:
|
|
182
216
|
profs = tuple(meta.get("profiles", ())) or ()
|
|
183
|
-
if profs and (not profiles or not any(p in profs for p in profiles)):
|
|
217
|
+
if profs and (not profiles or not any(p in profs for p in profiles)):
|
|
218
|
+
return False
|
|
184
219
|
req_env = tuple(meta.get("require_env", ())) or ()
|
|
185
|
-
if req_env and not all(os.getenv(k) not in (None, "") for k in req_env):
|
|
220
|
+
if req_env and not all(os.getenv(k) not in (None, "") for k in req_env):
|
|
221
|
+
return False
|
|
186
222
|
pred = meta.get("predicate", None)
|
|
187
223
|
if callable(pred):
|
|
188
224
|
try:
|
|
189
|
-
if not bool(pred()):
|
|
225
|
+
if not bool(pred()):
|
|
226
|
+
return False
|
|
190
227
|
except Exception:
|
|
191
228
|
logging.exception("Interceptor predicate failed; skipping")
|
|
192
229
|
return False
|
|
193
230
|
return True
|
|
194
231
|
|
|
195
232
|
def _looks_like_container_interceptor(inst: Any) -> bool:
|
|
196
|
-
return all(
|
|
233
|
+
return all(
|
|
234
|
+
hasattr(inst, m) for m in ("on_resolve", "on_before_create", "on_after_create", "on_exception")
|
|
235
|
+
)
|
|
197
236
|
|
|
198
237
|
for raw_obj, meta in interceptor_decls:
|
|
199
238
|
owner_cls, obj = (raw_obj[0], raw_obj[1]) if isinstance(raw_obj, tuple) and len(raw_obj) == 2 else (None, raw_obj)
|
|
200
239
|
qn = getattr(obj, "__qualname__", repr(obj))
|
|
240
|
+
|
|
201
241
|
if not _conditional_active(obj, profiles=profiles) or not _interceptor_meta_active(meta):
|
|
202
242
|
skipped_debug.append(f"skip:{qn}")
|
|
203
243
|
continue
|
|
244
|
+
|
|
204
245
|
try:
|
|
205
246
|
if isinstance(obj, type):
|
|
206
247
|
inst = resolver.create_instance(obj)
|
|
@@ -215,6 +256,7 @@ def _activate_and_build_interceptors(
|
|
|
215
256
|
except Exception:
|
|
216
257
|
logging.exception("Failed to construct interceptor %r", obj)
|
|
217
258
|
continue
|
|
259
|
+
|
|
218
260
|
kind = meta.get("kind", "method")
|
|
219
261
|
if kind == "method" and not callable(inst):
|
|
220
262
|
logging.error("Interceptor %s is not valid for kind %s; skipping", qn, kind)
|
|
@@ -222,21 +264,31 @@ def _activate_and_build_interceptors(
|
|
|
222
264
|
if kind == "container" and not _looks_like_container_interceptor(inst):
|
|
223
265
|
logging.error("Container interceptor %s lacks required methods; skipping", qn)
|
|
224
266
|
continue
|
|
267
|
+
|
|
225
268
|
order = int(meta.get("order", 0))
|
|
226
269
|
active.append((order, qn, kind, inst))
|
|
227
|
-
|
|
270
|
+
|
|
228
271
|
active.sort(key=lambda t: (t[0], t[1]))
|
|
229
|
-
|
|
272
|
+
|
|
230
273
|
for _order, _qn, kind, inst in active:
|
|
231
274
|
if kind == "container":
|
|
232
|
-
container.add_container_interceptor(inst)
|
|
275
|
+
container.add_container_interceptor(inst) # type: ignore[arg-type]
|
|
233
276
|
activated_container_names.append(_qn)
|
|
234
277
|
else:
|
|
235
|
-
container.add_method_interceptor(inst)
|
|
278
|
+
container.add_method_interceptor(inst) # type: ignore[arg-type]
|
|
236
279
|
activated_method_names.append(_qn)
|
|
237
280
|
|
|
238
281
|
if activated_method_names or activated_container_names:
|
|
239
|
-
logging.info(
|
|
240
|
-
|
|
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
|
+
)
|
|
241
292
|
if skipped_debug:
|
|
242
293
|
logging.debug("Skipped interceptors: %s", ", ".join(skipped_debug))
|
|
294
|
+
|