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/policy.py CHANGED
@@ -1,51 +1,39 @@
1
- # pico_ioc/policy.py
1
+ # src/pico_ioc/policy.py
2
2
  from __future__ import annotations
3
3
 
4
4
  import inspect
5
5
  import os
6
6
  from collections import defaultdict
7
- from typing import Any, Dict, Iterable, List, Tuple, Optional
7
+ from typing import Any, Dict, List, Optional, Tuple
8
+
8
9
  from .utils import create_alias_provider
9
- from .decorators import (
10
- CONDITIONAL_META,
11
- PRIMARY_FLAG,
12
- ON_MISSING_META,
13
- )
10
+ from .decorators import CONDITIONAL_META, PRIMARY_FLAG, ON_MISSING_META
11
+ from . import _state
12
+
14
13
 
15
- # ---------------- helpers ----------------
14
+ # ------------------- helpers -------------------
16
15
 
17
16
  def _target_from_provider(provider):
18
- """
19
- Best-effort: find the real target behind a provider closure.
20
- Priority: bound method > plain function > class. Fallback to the provider itself.
21
- """
17
+ """Try to resolve the 'real' target behind a provider closure (class, function or bound method)."""
22
18
  fn = provider
23
19
  try:
24
20
  cells = getattr(fn, "__closure__", None) or ()
25
- first_func = None
26
- first_cls = None
21
+ first_func, first_cls = None, None
27
22
  for cell in cells:
28
23
  cc = getattr(cell, "cell_contents", None)
29
24
  if inspect.ismethod(cc):
30
- return cc # highest priority: bound method
25
+ return cc
31
26
  if first_func is None and inspect.isfunction(cc):
32
- first_func = cc # keep first function seen
27
+ first_func = cc
33
28
  elif first_cls is None and inspect.isclass(cc):
34
- first_cls = cc # keep first class seen
35
- if first_func is not None:
36
- return first_func
37
- if first_cls is not None:
38
- return first_cls
29
+ first_cls = cc
30
+ return first_func or first_cls or fn
39
31
  except Exception:
40
- pass
41
- return fn
32
+ return fn
42
33
 
43
34
 
44
35
  def _owner_func(obj):
45
- """
46
- If obj is a bound method, return the unbound function owned by the class (if resolvable).
47
- This lets us read decorator flags placed on the original function.
48
- """
36
+ """If obj is a bound method, return the unbound function on its owner class."""
49
37
  try:
50
38
  if inspect.ismethod(obj) and getattr(obj, "__self__", None) is not None:
51
39
  owner = obj.__self__.__class__
@@ -60,96 +48,66 @@ def _owner_func(obj):
60
48
 
61
49
 
62
50
  def _find_attribute_on_target(target: Any, attr_name: str) -> Any:
63
- """
64
- Finds an attribute on a target object, searching the object itself,
65
- its __func__ (for bound methods), or the owning class's function.
66
- """
67
- # 1. Check the object itself
68
- value = getattr(target, attr_name, None)
69
- if value is not None:
70
- return value
71
-
72
- # 2. Check the underlying function for bound methods
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
73
55
  base_func = getattr(target, "__func__", None)
74
- if base_func is not None:
75
- value = getattr(base_func, attr_name, None)
76
- if value is not None:
77
- return value
78
-
79
- # 3. Check the function on the owner class
80
- owner_func = _owner_func(target)
81
- if owner_func is not None:
82
- value = getattr(owner_func, attr_name, None)
83
- if value is not None:
84
- return value
85
-
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
86
65
  return None
87
66
 
88
67
 
89
68
  def _has_flag(obj, flag_name: str) -> bool:
90
- """Reads a boolean decorator flag from the target."""
91
69
  return bool(_find_attribute_on_target(obj, flag_name))
92
70
 
93
71
 
94
72
  def _get_meta(obj, meta_name: str) -> Any:
95
- """Reads metadata (e.g., a dict) from the target."""
96
73
  return _find_attribute_on_target(obj, meta_name)
97
74
 
98
75
 
99
76
  def _on_missing_meta(target):
100
- """
101
- Normalize @on_missing metadata.
102
-
103
- IMPORTANT: The decorator stores {"selector": <T>, "priority": <int>}.
104
- """
77
+ """Normalize @on_missing metadata."""
105
78
  meta = _get_meta(target, ON_MISSING_META)
106
79
  if not meta:
107
80
  return None
108
- selector = meta.get("selector")
109
- prio = int(meta.get("priority", 0))
110
- return (selector, prio)
81
+ return (meta.get("selector"), int(meta.get("priority", 0)))
82
+
111
83
 
112
84
  def _conditional_active(target, *, profiles: List[str]) -> bool:
113
- """
114
- Returns True if the target is active given profiles/env/predicate.
115
- Activation logic (conjunctive across what's provided):
116
- - If `profiles` present on target → at least one must match requested profiles.
117
- - If `require_env` present → all listed env vars must be non-empty.
118
- - If `predicate` present → must return True; exceptions → inactive (fail-closed).
119
- - If none provided → active by default.
120
- """
85
+ """Check if target is active given profiles/env/predicate."""
121
86
  meta = _get_meta(target, CONDITIONAL_META)
122
87
  if not meta:
123
88
  return True
124
89
 
125
- profs = tuple(meta.get("profiles", ())) or ()
126
- req_env = tuple(meta.get("require_env", ())) or ()
127
- pred = meta.get("predicate", None)
90
+ profs = tuple(meta.get("profiles", ()))
91
+ req_env = tuple(meta.get("require_env", ()))
92
+ pred = meta.get("predicate")
128
93
 
129
- # 1) profiles (if declared)
130
- if profs:
131
- if not profiles or not any(p in profs for p in profiles):
132
- return False
133
-
134
- # 2) require_env (if declared)
135
- if req_env:
136
- if not all(os.getenv(k) not in (None, "") for k in req_env):
137
- return False
138
-
139
- # 3) predicate (if declared)
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
140
98
  if callable(pred):
141
99
  try:
142
100
  if not bool(pred()):
143
101
  return False
144
102
  except Exception:
145
103
  return False
146
-
147
- # default: active
148
104
  return True
149
105
 
150
- # ---------------- public API ----------------
106
+
107
+ # ------------------- public API -------------------
151
108
 
152
109
  def apply_policy(container, *, profiles: Optional[List[str]] = None) -> None:
110
+ """Run all policy stages on the given container."""
153
111
  profiles = list(profiles or [])
154
112
 
155
113
  _filter_inactive_factory_candidates(container, profiles=profiles)
@@ -159,174 +117,129 @@ def apply_policy(container, *, profiles: Optional[List[str]] = None) -> None:
159
117
 
160
118
 
161
119
  def apply_defaults(container) -> None:
162
- """
163
- Bind default providers declared via @on_missing when no binding exists for the selector.
164
-
165
- Supports:
166
- - Class components decorated with @on_missing(Selector, priority=...)
167
- - Factory @provides(...) where the provided method (or its owner) carries @on_missing
168
- AND the provider got tagged with _pico_alias_for (the base type key).
169
- """
120
+ """Bind defaults declared with @on_missing if no binding exists for selector."""
170
121
  defaults: dict[Any, list[tuple[int, Any]]] = {}
171
122
 
172
- # (1) Class components with @on_missing
173
- for prov_key, meta in list(container._providers.items()): # type: ignore[attr-defined]
123
+ # class components
124
+ for prov_key, meta in list(container._providers.items()): # type: ignore
174
125
  if not isinstance(prov_key, type):
175
126
  continue
176
127
  target = _target_from_provider(meta.get("factory"))
177
128
  om = _on_missing_meta(target)
178
- if not om:
179
- continue
180
- selector, prio = om
181
- defaults.setdefault(selector, []).append((prio, prov_key))
129
+ if om:
130
+ selector, prio = om
131
+ defaults.setdefault(selector, []).append((prio, prov_key))
182
132
 
183
- # (2) Factory @provides(...) with @on_missing on the provided method/owner
184
- for prov_key, meta in list(container._providers.items()): # type: ignore[attr-defined]
133
+ # factory provides
134
+ for prov_key, meta in list(container._providers.items()): # type: ignore
185
135
  prov = meta.get("factory")
186
136
  base = getattr(prov, "_pico_alias_for", None)
187
137
  if base is None:
188
138
  continue
189
139
  target = _target_from_provider(prov)
190
140
  om = _on_missing_meta(target)
191
- if not om:
192
- continue
193
- _selector_from_flag, prio = om
194
- defaults.setdefault(base, []).append((prio, prov_key))
141
+ if om:
142
+ _sel, prio = om
143
+ defaults.setdefault(base, []).append((prio, prov_key))
195
144
 
196
- # Bind highest priority default for each selector if not already bound
197
- for base, candidates in defaults.items():
145
+ # bind highest priority candidate
146
+ for base, cands in defaults.items():
198
147
  if container.has(base):
199
148
  continue
200
- candidates.sort(key=lambda t: t[0], reverse=True)
201
- chosen_key = candidates[0][1]
149
+ cands.sort(key=lambda t: t[0], reverse=True)
150
+ chosen_key = cands[0][1]
202
151
 
203
- def _make_delegate(_chosen_key=chosen_key):
204
- def _factory():
205
- return container.get(_chosen_key)
206
- return _factory
152
+ def _delegate(_k=chosen_key):
153
+ def _f():
154
+ return container.get(_k)
155
+ return _f
207
156
 
208
- container.bind(base, _make_delegate(), lazy=True)
157
+ container.bind(base, _delegate(), lazy=True)
209
158
 
210
- # ---------------- stages ----------------
159
+
160
+ # ------------------- stages -------------------
211
161
 
212
162
  def _filter_inactive_factory_candidates(container, *, profiles: List[str]) -> None:
213
- """
214
- Remove factory-provided candidates whose target is inactive under the given profiles/env/predicate.
215
- This trims the candidate set early so later selection/aliasing runs on active options only.
216
- """
163
+ """Remove factories inactive under profiles/env/predicate."""
217
164
  to_delete = []
218
- for prov_key, meta in list(container._providers.items()): # type: ignore[attr-defined]
165
+ for prov_key, meta in list(container._providers.items()): # type: ignore
219
166
  prov = meta.get("factory")
220
167
  base = getattr(prov, "_pico_alias_for", None)
221
168
  if base is None:
222
169
  continue
223
170
  target = _target_from_provider(prov)
224
- active = _conditional_active(target, profiles=profiles)
225
- if not active:
171
+ if not _conditional_active(target, profiles=profiles):
226
172
  to_delete.append(prov_key)
227
173
  for k in to_delete:
228
- container._providers.pop(k, None) # type: ignore[attr-defined]
174
+ container._providers.pop(k, None) # type: ignore
229
175
 
230
176
 
231
177
  def _collapse_identical_keys_preferring_primary(container) -> None:
232
- """
233
- For factory-provided products of the same base type, collapse to a single alias:
234
- - If any candidate is marked @primary -> pick the first primary.
235
- - Else leave multiple to be decided by defaults or later alias logic.
236
- """
237
- alias_groups: dict[Any, list[tuple[Any, dict]]] = defaultdict(list)
238
- for k, m in list(container._providers.items()): # type: ignore[attr-defined]
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
239
181
  prov = m.get("factory")
240
- base_key = getattr(prov, "_pico_alias_for", None)
241
- if base_key is not None:
242
- alias_groups[base_key].append((k, m))
182
+ base = getattr(prov, "_pico_alias_for", None)
183
+ if base is not None:
184
+ groups[base].append((k, m))
243
185
 
244
- for base, entries in alias_groups.items():
186
+ for base, entries in groups.items():
245
187
  if not entries:
246
188
  continue
247
-
248
189
  if len(entries) == 1:
249
- keep_key, _ = entries[0]
250
- if (not container.has(base)) or (base != keep_key):
251
- factory = create_alias_provider(container, keep_key)
190
+ keep, _ = entries[0]
191
+ if (not container.has(base)) or (base != keep):
192
+ factory = create_alias_provider(container, keep)
252
193
  container.bind(base, factory, lazy=True)
253
194
  continue
254
195
 
255
- primaries: list[tuple[Any, dict]] = []
256
- for (kk, mm) in entries:
257
- tgt = _target_from_provider(mm.get("factory"))
258
- if _has_flag(tgt, PRIMARY_FLAG):
259
- primaries.append((kk, mm))
260
-
261
- if primaries:
262
- keep_key, _ = primaries[0]
263
- if (not container.has(base)) or (base != keep_key):
264
- factory = create_alias_provider(container, keep_key)
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)
265
201
  container.bind(base, factory, lazy=True)
266
- for (kk, _mm) in entries:
267
- if kk != keep_key and kk != base:
268
- container._providers.pop(kk, None) # type: ignore[attr-defined]
269
- else:
270
- # multiple, no @primary -> leave for defaults
271
- pass
202
+ for kk, _mm in entries:
203
+ if kk != keep and kk != base:
204
+ container._providers.pop(kk, None) # type: ignore
272
205
 
273
206
 
274
207
  def _create_active_component_base_aliases(container, *, profiles: List[str]) -> None:
275
- """
276
- For class components (not factory-bound), create base->impl aliases among ACTIVE implementations.
277
-
278
- Preference order:
279
- 1) Regular active implementations (non-@on_missing), prefer @primary; else first.
280
- 2) If none, fall back to @on_missing implementations, prefer @primary; else first.
281
- """
282
- base_to_impls: Dict[Any, List[Tuple[Any, Dict[str, Any]]]] = defaultdict(list)
283
-
284
- # Collect active implementations
285
- impls: List[Tuple[type, Dict[str, Any]]] = []
286
- for key, meta in list(container._providers.items()): # type: ignore[attr-defined]
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
287
211
  if not isinstance(key, type):
288
212
  continue
289
213
  tgt = _target_from_provider(meta.get("factory"))
290
214
  if _conditional_active(tgt, profiles=profiles):
291
215
  impls.append((key, meta))
292
216
 
293
- # Map each impl to all bases in its MRO (excluding itself and object)
217
+ base_to_impls: Dict[Any, List[Tuple[Any, dict]]] = defaultdict(list)
294
218
  for impl_key, impl_meta in impls:
295
219
  for base in getattr(impl_key, "__mro__", ())[1:]:
296
220
  if base is object:
297
221
  break
298
- base_to_impls.setdefault(base, []).append((impl_key, impl_meta))
222
+ base_to_impls[base].append((impl_key, impl_meta))
299
223
 
300
- # Choose per-base implementation with correct priority
301
224
  for base, impl_list in base_to_impls.items():
302
225
  if container.has(base) or not impl_list:
303
226
  continue
304
227
 
305
- regular: List[Tuple[Any, Dict[str, Any]]] = []
306
- fallbacks: List[Tuple[Any, Dict[str, Any]]] = []
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))
307
232
 
308
- for (impl_key, impl_meta) in impl_list:
309
- tgt = _target_from_provider(impl_meta.get("factory"))
310
- if _on_missing_meta(tgt):
311
- fallbacks.append((impl_key, impl_meta))
312
- else:
313
- regular.append((impl_key, impl_meta))
314
-
315
- def pick(cands: List[Tuple[Any, Dict[str, Any]]]) -> Optional[Any]:
233
+ def pick(cands: List[Tuple[Any, dict]]) -> Optional[Any]:
316
234
  if not cands:
317
235
  return None
318
- primaries = []
319
- for (ik, im) in cands:
320
- tgt = _target_from_provider(im.get("factory"))
321
- if _has_flag(tgt, PRIMARY_FLAG):
322
- primaries.append((ik, im))
323
- chosen_key, _ = primaries[0] if primaries else cands[0]
324
- return chosen_key
325
-
326
- chosen_key = pick(regular) or pick(fallbacks)
327
- if chosen_key is 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:
328
241
  continue
329
242
 
330
- factory = create_alias_provider(container, chosen_key)
243
+ factory = create_alias_provider(container, chosen)
331
244
  container.bind(base, factory, lazy=True)
332
245
 
pico_ioc/proxy.py CHANGED
@@ -1,23 +1,25 @@
1
- # pico_ioc/proxy.py
2
-
1
+ # src/pico_ioc/proxy.py
3
2
  from __future__ import annotations
3
+
4
+ import inspect
4
5
  from functools import lru_cache
5
6
  from typing import Any, Callable, Sequence
6
- import inspect
7
7
 
8
8
  from .interceptors import Invocation, dispatch, MethodInterceptor
9
9
 
10
+
10
11
  class ComponentProxy:
12
+ """Proxy for lazy components. Creates the real object only when accessed."""
11
13
  def __init__(self, object_creator: Callable[[], Any]):
12
14
  object.__setattr__(self, "_object_creator", object_creator)
13
15
  object.__setattr__(self, "__real_object", None)
14
16
 
15
17
  def _get_real_object(self) -> Any:
16
- real_obj = object.__getattribute__(self, "__real_object")
17
- if real_obj is None:
18
- real_obj = object.__getattribute__(self, "_object_creator")()
19
- object.__setattr__(self, "__real_object", real_obj)
20
- return real_obj
18
+ real = object.__getattribute__(self, "__real_object")
19
+ if real is None:
20
+ real = object.__getattribute__(self, "_object_creator")()
21
+ object.__setattr__(self, "__real_object", real)
22
+ return real
21
23
 
22
24
  @property
23
25
  def __class__(self):
@@ -29,6 +31,8 @@ class ComponentProxy:
29
31
  def __str__(self): return str(self._get_real_object())
30
32
  def __repr__(self): return repr(self._get_real_object())
31
33
  def __dir__(self): return dir(self._get_real_object())
34
+
35
+ # container-like behavior
32
36
  def __len__(self): return len(self._get_real_object())
33
37
  def __getitem__(self, key): return self._get_real_object()[key]
34
38
  def __setitem__(self, key, value): self._get_real_object()[key] = value
@@ -36,6 +40,8 @@ class ComponentProxy:
36
40
  def __iter__(self): return iter(self._get_real_object())
37
41
  def __reversed__(self): return reversed(self._get_real_object())
38
42
  def __contains__(self, item): return item in self._get_real_object()
43
+
44
+ # operators
39
45
  def __add__(self, other): return self._get_real_object() + other
40
46
  def __sub__(self, other): return self._get_real_object() - other
41
47
  def __mul__(self, other): return self._get_real_object() * other
@@ -50,6 +56,8 @@ class ComponentProxy:
50
56
  def __and__(self, other): return self._get_real_object() & other
51
57
  def __xor__(self, other): return self._get_real_object() ^ other
52
58
  def __or__(self, other): return self._get_real_object() | other
59
+
60
+ # reflected operators
53
61
  def __radd__(self, other): return other + self._get_real_object()
54
62
  def __rsub__(self, other): return other - self._get_real_object()
55
63
  def __rmul__(self, other): return other * self._get_real_object()
@@ -64,6 +72,8 @@ class ComponentProxy:
64
72
  def __rand__(self, other): return other & self._get_real_object()
65
73
  def __rxor__(self, other): return other ^ self._get_real_object()
66
74
  def __ror__(self, other): return other | self._get_real_object()
75
+
76
+ # misc
67
77
  def __neg__(self): return -self._get_real_object()
68
78
  def __pos__(self): return +self._get_real_object()
69
79
  def __abs__(self): return abs(self._get_real_object())
@@ -76,11 +86,15 @@ class ComponentProxy:
76
86
  def __ge__(self, other): return self._get_real_object() >= other
77
87
  def __hash__(self): return hash(self._get_real_object())
78
88
  def __bool__(self): return bool(self._get_real_object())
89
+
90
+ # callables & context
79
91
  def __call__(self, *args, **kwargs): return self._get_real_object()(*args, **kwargs)
80
92
  def __enter__(self): return self._get_real_object().__enter__()
81
93
  def __exit__(self, exc_type, exc_val, exc_tb): return self._get_real_object().__exit__(exc_type, exc_val, exc_tb)
82
-
94
+
95
+
83
96
  class IoCProxy:
97
+ """Proxy that wraps an object and applies MethodInterceptors on method calls."""
84
98
  __slots__ = ("_target", "_interceptors")
85
99
 
86
100
  def __init__(self, target: object, interceptors: Sequence[MethodInterceptor]):
@@ -92,26 +106,24 @@ class IoCProxy:
92
106
  if not callable(attr):
93
107
  return attr
94
108
  if hasattr(attr, "__get__"):
95
- fn = attr.__get__(self._target, type(self._target))
109
+ bound_fn = attr.__get__(self._target, type(self._target))
96
110
  else:
97
- fn = attr
111
+ bound_fn = attr
98
112
 
99
113
  @lru_cache(maxsize=None)
100
- def _wrap(bound_fn: Callable[..., Any]):
101
- if inspect.iscoroutinefunction(bound_fn):
114
+ def _wrap(fn: Callable[..., Any]):
115
+ if inspect.iscoroutinefunction(fn):
102
116
  async def aw(*args, **kwargs):
103
- inv = Invocation(self._target, name, bound_fn, args, kwargs)
104
- # dispatch returns a coroutine for async methods
117
+ inv = Invocation(self._target, name, fn, args, kwargs)
105
118
  return await dispatch(self._interceptors, inv)
106
119
  return aw
107
120
  else:
108
121
  def sw(*args, **kwargs):
109
- inv = Invocation(self._target, name, bound_fn, args, kwargs)
110
- # dispatch returns a *value* for sync methods
122
+ inv = Invocation(self._target, name, fn, args, kwargs)
111
123
  res = dispatch(self._interceptors, inv)
112
124
  if inspect.isawaitable(res):
113
125
  raise RuntimeError(f"Async interceptor on sync method: {name}")
114
126
  return res
115
127
  return sw
116
- return _wrap(fn)
128
+ return _wrap(bound_fn)
117
129