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 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, interceptor,
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 Invocation, MethodInterceptor, ContainerInterceptor
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
- "interceptor",
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
- # pico_ioc/_state.py
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
- """Context manager: mark scanning=True within the block."""
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.3.0'
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._container = None
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
- if _state.get_fingerprint() == fp:
93
- return _state._container
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().with_plugins(plugins).with_profiles(profiles).with_overrides(overrides)
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._container = container
155
- _state._root_name = root_name
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 = _ScopedContainer(base=base, strict=strict, built_container=built_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
- 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
-
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
- return _state.get_fingerprint()
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._interceptor_decls: List[Tuple[Any, dict]] = []
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, decls = scan_and_configure(pkg, container, exclude=exclude, plugins=scan_plugins)
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
- self._interceptor_decls.extend(decls)
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
- container.eager_instantiate_all()
85
+ if self._eager:
86
+ container.eager_instantiate_all()
89
87
  logging.info("Container configured and ready.")
90
88
  return container
91
89
 
92
- # ... (Helper functions like _resolve_profiles, _apply_overrides etc. remain here) ...
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) # Start with 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): _add_impls_for_base(k)
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): continue
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": continue
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: inner = (get_args(inner) or (object,))[0]
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 _activate_and_build_interceptors(
173
- *, container: PicoContainer, interceptor_decls: list[tuple[Any, dict]], profiles: list[str],
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)): return False
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 and not all(os.getenv(k) not in (None, "") for k in req_env): return False
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()): return False
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
- def _looks_like_container_interceptor(inst: Any) -> bool:
196
- return all(hasattr(inst, m) for m in ("on_resolve", "on_before_create", "on_after_create", "on_exception"))
197
-
198
- for raw_obj, meta in interceptor_decls:
199
- owner_cls, obj = (raw_obj[0], raw_obj[1]) if isinstance(raw_obj, tuple) and len(raw_obj) == 2 else (None, raw_obj)
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
- if isinstance(obj, type):
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.exception("Failed to construct interceptor %r", obj)
217
- continue
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
- if kind == "container" and not _looks_like_container_interceptor(inst):
223
- logging.error("Container interceptor %s lacks required methods; skipping", qn)
224
- continue
225
- order = int(meta.get("order", 0))
226
- active.append((order, qn, kind, inst))
227
-
228
- active.sort(key=lambda t: (t[0], t[1]))
229
-
230
- for _order, _qn, kind, inst in active:
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
+