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/container.py CHANGED
@@ -1,15 +1,18 @@
1
- # pico_ioc/container.py
1
+ # src/pico_ioc/container.py
2
2
  from __future__ import annotations
3
+
3
4
  import inspect
4
5
  from typing import Any, Dict, get_origin, get_args, Annotated
5
- import typing as _t
6
+ import typing as _t
6
7
 
8
+ from .proxy import IoCProxy
9
+ from .interceptors import MethodInterceptor, ContainerInterceptor
7
10
  from .decorators import QUALIFIERS_KEY
8
- from . import _state # re-entrancy guard
11
+ from . import _state
9
12
 
10
13
 
11
14
  class Binder:
12
- def __init__(self, container: "PicoContainer"):
15
+ def __init__(self, container: PicoContainer):
13
16
  self._c = container
14
17
 
15
18
  def bind(self, key: Any, provider, *, lazy: bool, tags: tuple[str, ...] = ()):
@@ -23,14 +26,38 @@ class Binder:
23
26
 
24
27
 
25
28
  class PicoContainer:
26
- def __init__(self):
27
- self._providers: Dict[Any, Dict[str, Any]] = {}
29
+ def __init__(self, providers: Dict[Any, Dict[str, Any]] | None = None):
30
+ self._providers = providers or {}
28
31
  self._singletons: Dict[Any, Any] = {}
32
+ self._method_interceptors: tuple[MethodInterceptor, ...] = ()
33
+ self._container_interceptors: tuple[ContainerInterceptor, ...] = ()
34
+ self._active_profiles: tuple[str, ...] = ()
35
+ self._seen_interceptor_types: set[type] = set()
36
+
37
+ # --- interceptors ---
38
+
39
+ def add_method_interceptor(self, it: MethodInterceptor) -> None:
40
+ t = type(it)
41
+ if t in self._seen_interceptor_types:
42
+ return
43
+ self._seen_interceptor_types.add(t)
44
+ self._method_interceptors = self._method_interceptors + (it,)
45
+
46
+ def add_container_interceptor(self, it: ContainerInterceptor) -> None:
47
+ t = type(it)
48
+ if t in self._seen_interceptor_types:
49
+ return
50
+ self._seen_interceptor_types.add(t)
51
+ self._container_interceptors = self._container_interceptors + (it,)
52
+
53
+ # --- binding ---
54
+
55
+ def binder(self) -> Binder:
56
+ return Binder(self)
29
57
 
30
58
  def bind(self, key: Any, provider, *, lazy: bool, tags: tuple[str, ...] = ()):
31
59
  self._singletons.pop(key, None)
32
60
  meta = {"factory": provider, "lazy": bool(lazy)}
33
- # qualifiers already present:
34
61
  try:
35
62
  q = getattr(key, QUALIFIERS_KEY, ())
36
63
  except Exception:
@@ -39,11 +66,12 @@ class PicoContainer:
39
66
  meta["tags"] = tuple(tags) if tags else ()
40
67
  self._providers[key] = meta
41
68
 
69
+ # --- resolution ---
70
+
42
71
  def has(self, key: Any) -> bool:
43
72
  return key in self._providers
44
73
 
45
74
  def get(self, key: Any):
46
- # block only when scanning and NOT currently resolving a dependency
47
75
  if _state._scanning.get() and not _state._resolving.get():
48
76
  raise RuntimeError("re-entrant container access during scan")
49
77
 
@@ -54,22 +82,49 @@ class PicoContainer:
54
82
  if key in self._singletons:
55
83
  return self._singletons[key]
56
84
 
57
- # mark resolving around factory execution
85
+ for ci in self._container_interceptors:
86
+ try:
87
+ ci.on_before_create(key)
88
+ except Exception:
89
+ pass
90
+
58
91
  tok = _state._resolving.set(True)
59
92
  try:
60
- instance = prov["factory"]()
93
+ try:
94
+ instance = prov["factory"]()
95
+ except BaseException as exc:
96
+ for ci in self._container_interceptors:
97
+ try:
98
+ ci.on_exception(key, exc)
99
+ except Exception:
100
+ pass
101
+ raise
61
102
  finally:
62
103
  _state._resolving.reset(tok)
63
104
 
64
- # memoize always (both lazy and non-lazy after first get)
105
+ if self._method_interceptors and not isinstance(instance, IoCProxy):
106
+ instance = IoCProxy(instance, self._method_interceptors)
107
+
108
+ for ci in self._container_interceptors:
109
+ try:
110
+ maybe = ci.on_after_create(key, instance)
111
+ if maybe is not None:
112
+ instance = maybe
113
+ except Exception:
114
+ pass
115
+
65
116
  self._singletons[key] = instance
66
117
  return instance
67
118
 
119
+ # --- lifecycle ---
120
+
68
121
  def eager_instantiate_all(self):
69
122
  for key, prov in list(self._providers.items()):
70
123
  if not prov["lazy"]:
71
124
  self.get(key)
72
125
 
126
+ # --- helpers for multiples ---
127
+
73
128
  def get_all(self, base_type: Any):
74
129
  return tuple(self._resolve_all_for_base(base_type, qualifiers=()))
75
130
 
@@ -82,13 +137,8 @@ class PicoContainer:
82
137
  cls = provider_key if isinstance(provider_key, type) else None
83
138
  if cls is None:
84
139
  continue
85
-
86
- # Avoid self-inclusion loops: if the class itself requires a collection
87
- # of `base_type` in its __init__, don't treat it as an implementation
88
- # of `base_type` when building that collection.
89
140
  if _requires_collection_of_base(cls, base_type):
90
141
  continue
91
-
92
142
  if _is_compatible(cls, base_type):
93
143
  prov_qs = meta.get("qualifiers", ())
94
144
  if all(q in prov_qs for q in qualifiers):
@@ -96,6 +146,11 @@ class PicoContainer:
96
146
  matches.append(inst)
97
147
  return matches
98
148
 
149
+ def get_providers(self) -> Dict[Any, Dict]:
150
+ return self._providers.copy()
151
+
152
+
153
+ # --- compatibility helpers ---
99
154
 
100
155
  def _is_protocol(t) -> bool:
101
156
  return getattr(t, "_is_protocol", False) is True
@@ -109,7 +164,6 @@ def _is_compatible(cls, base) -> bool:
109
164
  pass
110
165
 
111
166
  if _is_protocol(base):
112
- # simple structural check: ensure methods/attrs declared on the Protocol exist on the class
113
167
  names = set(getattr(base, "__annotations__", {}).keys())
114
168
  names.update(n for n in getattr(base, "__dict__", {}).keys() if not n.startswith("_"))
115
169
  for n in names:
@@ -121,20 +175,15 @@ def _is_compatible(cls, base) -> bool:
121
175
 
122
176
  return False
123
177
 
178
+
124
179
  def _requires_collection_of_base(cls, base) -> bool:
125
- """
126
- Return True if `cls.__init__` has any parameter annotated as a collection
127
- (list/tuple, including Annotated variants) of `base`. This prevents treating
128
- `cls` as an implementation of `base` while building that collection,
129
- avoiding recursion.
130
- """
131
180
  try:
132
181
  sig = inspect.signature(cls.__init__)
133
182
  except Exception:
134
183
  return False
135
184
 
136
185
  try:
137
- from .resolver import _get_hints # type: ignore
186
+ from .resolver import _get_hints
138
187
  hints = _get_hints(cls.__init__, owner_cls=cls)
139
188
  except Exception:
140
189
  hints = {}
@@ -146,7 +195,6 @@ def _requires_collection_of_base(cls, base) -> bool:
146
195
  origin = get_origin(ann) or ann
147
196
  if origin in (list, tuple, _t.List, _t.Tuple):
148
197
  inner = (get_args(ann) or (object,))[0]
149
- # Unwrap Annotated[T, ...] si aparece
150
198
  if get_origin(inner) is Annotated:
151
199
  args = get_args(inner)
152
200
  if args:
@@ -155,4 +203,3 @@ def _requires_collection_of_base(cls, base) -> bool:
155
203
  return True
156
204
  return False
157
205
 
158
-
pico_ioc/decorators.py CHANGED
@@ -1,7 +1,11 @@
1
- # pico_ioc/decorators.py
1
+ # src/pico_ioc/decorators.py
2
2
  from __future__ import annotations
3
+
3
4
  import functools
4
- from typing import Any, Iterable
5
+ from typing import Any, Iterable, Optional, Callable, Tuple, Literal
6
+
7
+
8
+ # ---- marker attributes (read by scanner/policy) ----
5
9
 
6
10
  COMPONENT_FLAG = "_is_component"
7
11
  COMPONENT_KEY = "_component_key"
@@ -17,12 +21,23 @@ QUALIFIERS_KEY = "_pico_qualifiers"
17
21
  COMPONENT_TAGS = "_pico_tags"
18
22
  PROVIDES_TAGS = "_pico_tags"
19
23
 
24
+ ON_MISSING_META = "_pico_on_missing"
25
+ PRIMARY_FLAG = "_pico_primary"
26
+ CONDITIONAL_META = "_pico_conditional"
27
+
28
+ INTERCEPTOR_META = "__pico_interceptor__"
29
+
30
+
31
+ # ---- core decorators ----
32
+
20
33
  def factory_component(cls):
34
+ """Mark a class as a factory component (its methods can @provides)."""
21
35
  setattr(cls, FACTORY_FLAG, True)
22
36
  return cls
23
37
 
24
38
 
25
39
  def component(cls=None, *, name: Any = None, lazy: bool = False, tags: Iterable[str] = ()):
40
+ """Mark a class as a component. Optional: custom key, lazy instantiation, tags."""
26
41
  def dec(c):
27
42
  setattr(c, COMPONENT_FLAG, True)
28
43
  setattr(c, COMPONENT_KEY, name if name is not None else c)
@@ -31,7 +46,9 @@ def component(cls=None, *, name: Any = None, lazy: bool = False, tags: Iterable[
31
46
  return c
32
47
  return dec(cls) if cls else dec
33
48
 
49
+
34
50
  def provides(key: Any, *, lazy: bool = False, tags: Iterable[str] = ()):
51
+ """Declare a factory method that provides a binding for `key`."""
35
52
  def dec(fn):
36
53
  @functools.wraps(fn)
37
54
  def w(*a, **k):
@@ -44,15 +61,20 @@ def provides(key: Any, *, lazy: bool = False, tags: Iterable[str] = ()):
44
61
 
45
62
 
46
63
  def plugin(cls):
64
+ """Mark a class as a Pico plugin (scanner lifecycle)."""
47
65
  setattr(cls, PLUGIN_FLAG, True)
48
66
  return cls
49
67
 
50
68
 
69
+ # ---- qualifiers ----
70
+
51
71
  class Qualifier(str):
52
- __slots__ = () # tiny memory win; immutable like str
72
+ """String qualifier type used with Annotated[T, 'q1', ...]."""
73
+ __slots__ = ()
53
74
 
54
75
 
55
76
  def qualifier(*qs: Qualifier):
77
+ """Attach one or more qualifiers to a component class key."""
56
78
  def dec(cls):
57
79
  current: Iterable[Qualifier] = getattr(cls, QUALIFIERS_KEY, ())
58
80
  seen = set(current)
@@ -66,14 +88,71 @@ def qualifier(*qs: Qualifier):
66
88
  return dec
67
89
 
68
90
 
91
+ # ---- defaults / selection ----
92
+
93
+ def on_missing(selector: object, *, priority: int = 0):
94
+ """Declare this target as a default for `selector` when no binding exists."""
95
+ def dec(obj):
96
+ setattr(obj, ON_MISSING_META, {"selector": selector, "priority": int(priority)})
97
+ return obj
98
+ return dec
99
+
100
+
101
+ def primary(obj):
102
+ """Hint this candidate should be preferred among equals."""
103
+ setattr(obj, PRIMARY_FLAG, True)
104
+ return obj
105
+
106
+
107
+ def conditional(
108
+ *,
109
+ profiles: Tuple[str, ...] = (),
110
+ require_env: Tuple[str, ...] = (),
111
+ predicate: Optional[Callable[[], bool]] = None,
112
+ ):
113
+ """Activate only when profiles/env/predicate conditions pass."""
114
+ def dec(obj):
115
+ setattr(obj, CONDITIONAL_META, {
116
+ "profiles": tuple(profiles),
117
+ "require_env": tuple(require_env),
118
+ "predicate": predicate,
119
+ })
120
+ return obj
121
+ return dec
122
+
123
+
124
+ # ---- interceptors ----
125
+
126
+ def interceptor(
127
+ _obj=None,
128
+ *,
129
+ kind: Literal["method", "container"] = "method",
130
+ order: int = 0,
131
+ profiles: Tuple[str, ...] = (),
132
+ require_env: Tuple[str, ...] = (),
133
+ predicate: Callable[[], bool] | None = None,
134
+ ):
135
+ """Declare an interceptor (method or container) with optional activation metadata."""
136
+ def dec(obj):
137
+ setattr(obj, INTERCEPTOR_META, {
138
+ "kind": kind,
139
+ "order": int(order),
140
+ "profiles": tuple(profiles),
141
+ "require_env": tuple(require_env),
142
+ "predicate": predicate,
143
+ })
144
+ return obj
145
+ return dec if _obj is None else dec(_obj)
146
+
147
+
69
148
  __all__ = [
70
- # decorators
71
- "component", "factory_component", "provides", "plugin", "qualifier",
72
- # qualifier type
73
- "Qualifier",
74
- # metadata keys (exported for advanced use/testing)
149
+ "component", "factory_component", "provides", "plugin",
150
+ "Qualifier", "qualifier",
151
+ "on_missing", "primary", "conditional", "interceptor",
75
152
  "COMPONENT_FLAG", "COMPONENT_KEY", "COMPONENT_LAZY",
76
153
  "FACTORY_FLAG", "PROVIDES_KEY", "PROVIDES_LAZY",
77
- "PLUGIN_FLAG", "QUALIFIERS_KEY", "COMPONENT_TAGS", "PROVIDES_TAGS"
154
+ "PLUGIN_FLAG", "QUALIFIERS_KEY", "COMPONENT_TAGS", "PROVIDES_TAGS",
155
+ "ON_MISSING_META", "PRIMARY_FLAG", "CONDITIONAL_META",
156
+ "INTERCEPTOR_META",
78
157
  ]
79
158
 
@@ -0,0 +1,56 @@
1
+ # src/pico_ioc/interceptors.py
2
+ from __future__ import annotations
3
+
4
+ import inspect
5
+ from typing import Any, Callable, Protocol, Sequence
6
+
7
+
8
+ class Invocation:
9
+ __slots__ = ("target", "method_name", "call", "args", "kwargs", "is_async")
10
+
11
+ def __init__(self, target: object, method_name: str, call: Callable[..., Any],
12
+ args: tuple, kwargs: dict):
13
+ self.target = target
14
+ self.method_name = method_name
15
+ self.call = call
16
+ self.args = args
17
+ self.kwargs = kwargs
18
+ self.is_async = inspect.iscoroutinefunction(call)
19
+
20
+
21
+ class MethodInterceptor(Protocol):
22
+ def __call__(self, inv: Invocation, proceed: Callable[[], Any]) -> Any: ...
23
+
24
+
25
+ async def _chain_async(interceptors: Sequence[MethodInterceptor], inv: Invocation, i: int = 0):
26
+ if i >= len(interceptors):
27
+ return await inv.call(*inv.args, **inv.kwargs)
28
+ cur = interceptors[i]
29
+
30
+ async def next_step():
31
+ return await _chain_async(interceptors, inv, i + 1)
32
+
33
+ res = cur(inv, next_step)
34
+ return await res if inspect.isawaitable(res) else res
35
+
36
+
37
+ def _chain_sync(interceptors: Sequence[MethodInterceptor], inv: Invocation, i: int = 0):
38
+ if i >= len(interceptors):
39
+ return inv.call(*inv.args, **inv.kwargs)
40
+ cur = interceptors[i]
41
+ return cur(inv, lambda: _chain_sync(interceptors, inv, i + 1))
42
+
43
+
44
+ def dispatch(interceptors: Sequence[MethodInterceptor], inv: Invocation):
45
+ """Dispatch invocation through a chain of interceptors."""
46
+ if inv.is_async:
47
+ return _chain_async(interceptors, inv, 0) # coroutine
48
+ return _chain_sync(interceptors, inv, 0) # value
49
+
50
+
51
+ class ContainerInterceptor(Protocol):
52
+ def on_resolve(self, key: Any, annotation: Any, qualifiers: tuple[str, ...] | tuple()) -> None: ...
53
+ def on_before_create(self, key: Any) -> None: ...
54
+ def on_after_create(self, key: Any, instance: Any) -> Any: ...
55
+ def on_exception(self, key: Any, exc: BaseException) -> None: ...
56
+
pico_ioc/plugins.py CHANGED
@@ -1,6 +1,7 @@
1
1
  # pico_ioc/plugins.py
2
- from typing import Protocol, Any
2
+ from typing import Protocol, Any, Tuple
3
3
  from .container import Binder, PicoContainer
4
+ import logging
4
5
 
5
6
  class PicoPlugin(Protocol):
6
7
  def before_scan(self, package: Any, binder: Binder) -> None: ...
@@ -10,3 +11,18 @@ class PicoPlugin(Protocol):
10
11
  def before_eager(self, container: PicoContainer, binder: Binder) -> None: ...
11
12
  def after_ready(self, container: PicoContainer, binder: Binder) -> None: ...
12
13
 
14
+ def run_plugin_hook(
15
+ plugins: Tuple[PicoPlugin, ...],
16
+ hook_name: str,
17
+ *args,
18
+ **kwargs,
19
+ ) -> None:
20
+ """Run a lifecycle hook across all plugins, logging (but not raising) exceptions."""
21
+ for pl in plugins:
22
+ try:
23
+ fn = getattr(pl, hook_name, None)
24
+ if fn:
25
+ fn(*args, **kwargs)
26
+ except Exception:
27
+ logging.exception("Plugin %s failed", hook_name)
28
+
pico_ioc/policy.py ADDED
@@ -0,0 +1,245 @@
1
+ # src/pico_ioc/policy.py
2
+ from __future__ import annotations
3
+
4
+ import inspect
5
+ import os
6
+ from collections import defaultdict
7
+ from typing import Any, Dict, List, Optional, Tuple
8
+
9
+ from .utils import create_alias_provider
10
+ from .decorators import CONDITIONAL_META, PRIMARY_FLAG, ON_MISSING_META
11
+ from . import _state
12
+
13
+
14
+ # ------------------- helpers -------------------
15
+
16
+ def _target_from_provider(provider):
17
+ """Try to resolve the 'real' target behind a provider closure (class, function or bound method)."""
18
+ fn = provider
19
+ try:
20
+ cells = getattr(fn, "__closure__", None) or ()
21
+ first_func, first_cls = None, None
22
+ for cell in cells:
23
+ cc = getattr(cell, "cell_contents", None)
24
+ if inspect.ismethod(cc):
25
+ return cc
26
+ if first_func is None and inspect.isfunction(cc):
27
+ first_func = cc
28
+ elif first_cls is None and inspect.isclass(cc):
29
+ first_cls = cc
30
+ return first_func or first_cls or fn
31
+ except Exception:
32
+ return fn
33
+
34
+
35
+ def _owner_func(obj):
36
+ """If obj is a bound method, return the unbound function on its owner class."""
37
+ try:
38
+ if inspect.ismethod(obj) and getattr(obj, "__self__", None) is not None:
39
+ owner = obj.__self__.__class__
40
+ name = getattr(obj, "__name__", None)
41
+ if name and hasattr(owner, name):
42
+ cand = getattr(owner, name)
43
+ if inspect.isfunction(cand):
44
+ return cand
45
+ except Exception:
46
+ pass
47
+ return None
48
+
49
+
50
+ def _find_attribute_on_target(target: Any, attr_name: str) -> Any:
51
+ """Look for metadata on object, its underlying function, or owner class method."""
52
+ val = getattr(target, attr_name, None)
53
+ if val is not None:
54
+ return val
55
+ base_func = getattr(target, "__func__", None)
56
+ if base_func:
57
+ val = getattr(base_func, attr_name, None)
58
+ if val is not None:
59
+ return val
60
+ of = _owner_func(target)
61
+ if of:
62
+ val = getattr(of, attr_name, None)
63
+ if val is not None:
64
+ return val
65
+ return None
66
+
67
+
68
+ def _has_flag(obj, flag_name: str) -> bool:
69
+ return bool(_find_attribute_on_target(obj, flag_name))
70
+
71
+
72
+ def _get_meta(obj, meta_name: str) -> Any:
73
+ return _find_attribute_on_target(obj, meta_name)
74
+
75
+
76
+ def _on_missing_meta(target):
77
+ """Normalize @on_missing metadata."""
78
+ meta = _get_meta(target, ON_MISSING_META)
79
+ if not meta:
80
+ return None
81
+ return (meta.get("selector"), int(meta.get("priority", 0)))
82
+
83
+
84
+ def _conditional_active(target, *, profiles: List[str]) -> bool:
85
+ """Check if target is active given profiles/env/predicate."""
86
+ meta = _get_meta(target, CONDITIONAL_META)
87
+ if not meta:
88
+ return True
89
+
90
+ profs = tuple(meta.get("profiles", ()))
91
+ req_env = tuple(meta.get("require_env", ()))
92
+ pred = meta.get("predicate")
93
+
94
+ if profs and (not profiles or not any(p in profs for p in profiles)):
95
+ return False
96
+ if req_env and not all(os.getenv(k) not in (None, "") for k in req_env):
97
+ return False
98
+ if callable(pred):
99
+ try:
100
+ if not bool(pred()):
101
+ return False
102
+ except Exception:
103
+ return False
104
+ return True
105
+
106
+
107
+ # ------------------- public API -------------------
108
+
109
+ def apply_policy(container, *, profiles: Optional[List[str]] = None) -> None:
110
+ """Run all policy stages on the given container."""
111
+ profiles = list(profiles or [])
112
+
113
+ _filter_inactive_factory_candidates(container, profiles=profiles)
114
+ _collapse_identical_keys_preferring_primary(container)
115
+ _create_active_component_base_aliases(container, profiles=profiles)
116
+ apply_defaults(container)
117
+
118
+
119
+ def apply_defaults(container) -> None:
120
+ """Bind defaults declared with @on_missing if no binding exists for selector."""
121
+ defaults: dict[Any, list[tuple[int, Any]]] = {}
122
+
123
+ # class components
124
+ for prov_key, meta in list(container._providers.items()): # type: ignore
125
+ if not isinstance(prov_key, type):
126
+ continue
127
+ target = _target_from_provider(meta.get("factory"))
128
+ om = _on_missing_meta(target)
129
+ if om:
130
+ selector, prio = om
131
+ defaults.setdefault(selector, []).append((prio, prov_key))
132
+
133
+ # factory provides
134
+ for prov_key, meta in list(container._providers.items()): # type: ignore
135
+ prov = meta.get("factory")
136
+ base = getattr(prov, "_pico_alias_for", None)
137
+ if base is None:
138
+ continue
139
+ target = _target_from_provider(prov)
140
+ om = _on_missing_meta(target)
141
+ if om:
142
+ _sel, prio = om
143
+ defaults.setdefault(base, []).append((prio, prov_key))
144
+
145
+ # bind highest priority candidate
146
+ for base, cands in defaults.items():
147
+ if container.has(base):
148
+ continue
149
+ cands.sort(key=lambda t: t[0], reverse=True)
150
+ chosen_key = cands[0][1]
151
+
152
+ def _delegate(_k=chosen_key):
153
+ def _f():
154
+ return container.get(_k)
155
+ return _f
156
+
157
+ container.bind(base, _delegate(), lazy=True)
158
+
159
+
160
+ # ------------------- stages -------------------
161
+
162
+ def _filter_inactive_factory_candidates(container, *, profiles: List[str]) -> None:
163
+ """Remove factories inactive under profiles/env/predicate."""
164
+ to_delete = []
165
+ for prov_key, meta in list(container._providers.items()): # type: ignore
166
+ prov = meta.get("factory")
167
+ base = getattr(prov, "_pico_alias_for", None)
168
+ if base is None:
169
+ continue
170
+ target = _target_from_provider(prov)
171
+ if not _conditional_active(target, profiles=profiles):
172
+ to_delete.append(prov_key)
173
+ for k in to_delete:
174
+ container._providers.pop(k, None) # type: ignore
175
+
176
+
177
+ def _collapse_identical_keys_preferring_primary(container) -> None:
178
+ """For multiple factory candidates of same base, keep one (prefer @primary)."""
179
+ groups: dict[Any, list[tuple[Any, dict]]] = defaultdict(list)
180
+ for k, m in list(container._providers.items()): # type: ignore
181
+ prov = m.get("factory")
182
+ base = getattr(prov, "_pico_alias_for", None)
183
+ if base is not None:
184
+ groups[base].append((k, m))
185
+
186
+ for base, entries in groups.items():
187
+ if not entries:
188
+ continue
189
+ if len(entries) == 1:
190
+ keep, _ = entries[0]
191
+ if (not container.has(base)) or (base != keep):
192
+ factory = create_alias_provider(container, keep)
193
+ container.bind(base, factory, lazy=True)
194
+ continue
195
+
196
+ prims = [(kk, mm) for (kk, mm) in entries if _has_flag(_target_from_provider(mm["factory"]), PRIMARY_FLAG)]
197
+ if prims:
198
+ keep, _ = prims[0]
199
+ if (not container.has(base)) or (base != keep):
200
+ factory = create_alias_provider(container, keep)
201
+ container.bind(base, factory, lazy=True)
202
+ for kk, _mm in entries:
203
+ if kk != keep and kk != base:
204
+ container._providers.pop(kk, None) # type: ignore
205
+
206
+
207
+ def _create_active_component_base_aliases(container, *, profiles: List[str]) -> None:
208
+ """For active class components, create base->impl aliases (prefer @primary)."""
209
+ impls: List[Tuple[type, dict]] = []
210
+ for key, meta in list(container._providers.items()): # type: ignore
211
+ if not isinstance(key, type):
212
+ continue
213
+ tgt = _target_from_provider(meta.get("factory"))
214
+ if _conditional_active(tgt, profiles=profiles):
215
+ impls.append((key, meta))
216
+
217
+ base_to_impls: Dict[Any, List[Tuple[Any, dict]]] = defaultdict(list)
218
+ for impl_key, impl_meta in impls:
219
+ for base in getattr(impl_key, "__mro__", ())[1:]:
220
+ if base is object:
221
+ break
222
+ base_to_impls[base].append((impl_key, impl_meta))
223
+
224
+ for base, impl_list in base_to_impls.items():
225
+ if container.has(base) or not impl_list:
226
+ continue
227
+
228
+ regular, fallbacks = [], []
229
+ for ik, im in impl_list:
230
+ tgt = _target_from_provider(im["factory"])
231
+ (fallbacks if _on_missing_meta(tgt) else regular).append((ik, im))
232
+
233
+ def pick(cands: List[Tuple[Any, dict]]) -> Optional[Any]:
234
+ if not cands:
235
+ return None
236
+ prims = [(ik, im) for ik, im in cands if _has_flag(_target_from_provider(im["factory"]), PRIMARY_FLAG)]
237
+ return prims[0][0] if prims else cands[0][0]
238
+
239
+ chosen = pick(regular) or pick(fallbacks)
240
+ if not chosen:
241
+ continue
242
+
243
+ factory = create_alias_provider(container, chosen)
244
+ container.bind(base, factory, lazy=True)
245
+