pico-ioc 1.4.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/decorators.py CHANGED
@@ -1,11 +1,6 @@
1
- # src/pico_ioc/decorators.py
2
1
  from __future__ import annotations
3
-
4
2
  import functools
5
- from typing import Any, Iterable, Optional, Callable, Tuple, Literal
6
-
7
-
8
- # ---- marker attributes (read by scanner/policy) ----
3
+ from typing import Any, Iterable, Optional, Callable, Tuple
9
4
 
10
5
  COMPONENT_FLAG = "_is_component"
11
6
  COMPONENT_KEY = "_component_key"
@@ -25,19 +20,13 @@ ON_MISSING_META = "_pico_on_missing"
25
20
  PRIMARY_FLAG = "_pico_primary"
26
21
  CONDITIONAL_META = "_pico_conditional"
27
22
 
28
- INTERCEPTOR_META = "__pico_interceptor__"
29
-
30
-
31
- # ---- core decorators ----
23
+ INFRA_META = "__pico_infrastructure__"
32
24
 
33
25
  def factory_component(cls):
34
- """Mark a class as a factory component (its methods can @provides)."""
35
26
  setattr(cls, FACTORY_FLAG, True)
36
27
  return cls
37
28
 
38
-
39
29
  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."""
41
30
  def dec(c):
42
31
  setattr(c, COMPONENT_FLAG, True)
43
32
  setattr(c, COMPONENT_KEY, name if name is not None else c)
@@ -46,9 +35,7 @@ def component(cls=None, *, name: Any = None, lazy: bool = False, tags: Iterable[
46
35
  return c
47
36
  return dec(cls) if cls else dec
48
37
 
49
-
50
38
  def provides(key: Any, *, lazy: bool = False, tags: Iterable[str] = ()):
51
- """Declare a factory method that provides a binding for `key`."""
52
39
  def dec(fn):
53
40
  @functools.wraps(fn)
54
41
  def w(*a, **k):
@@ -59,22 +46,14 @@ def provides(key: Any, *, lazy: bool = False, tags: Iterable[str] = ()):
59
46
  return w
60
47
  return dec
61
48
 
62
-
63
49
  def plugin(cls):
64
- """Mark a class as a Pico plugin (scanner lifecycle)."""
65
50
  setattr(cls, PLUGIN_FLAG, True)
66
51
  return cls
67
52
 
68
-
69
- # ---- qualifiers ----
70
-
71
53
  class Qualifier(str):
72
- """String qualifier type used with Annotated[T, 'q1', ...]."""
73
54
  __slots__ = ()
74
55
 
75
-
76
56
  def qualifier(*qs: Qualifier):
77
- """Attach one or more qualifiers to a component class key."""
78
57
  def dec(cls):
79
58
  current: Iterable[Qualifier] = getattr(cls, QUALIFIERS_KEY, ())
80
59
  seen = set(current)
@@ -87,30 +66,22 @@ def qualifier(*qs: Qualifier):
87
66
  return cls
88
67
  return dec
89
68
 
90
-
91
- # ---- defaults / selection ----
92
-
93
69
  def on_missing(selector: object, *, priority: int = 0):
94
- """Declare this target as a default for `selector` when no binding exists."""
95
70
  def dec(obj):
96
71
  setattr(obj, ON_MISSING_META, {"selector": selector, "priority": int(priority)})
97
72
  return obj
98
73
  return dec
99
74
 
100
-
101
75
  def primary(obj):
102
- """Hint this candidate should be preferred among equals."""
103
76
  setattr(obj, PRIMARY_FLAG, True)
104
77
  return obj
105
78
 
106
-
107
79
  def conditional(
108
80
  *,
109
81
  profiles: Tuple[str, ...] = (),
110
82
  require_env: Tuple[str, ...] = (),
111
83
  predicate: Optional[Callable[[], bool]] = None,
112
84
  ):
113
- """Activate only when profiles/env/predicate conditions pass."""
114
85
  def dec(obj):
115
86
  setattr(obj, CONDITIONAL_META, {
116
87
  "profiles": tuple(profiles),
@@ -120,39 +91,30 @@ def conditional(
120
91
  return obj
121
92
  return dec
122
93
 
123
-
124
- # ---- interceptors ----
125
-
126
- def interceptor(
127
- _obj=None,
128
- *,
129
- kind: Literal["method", "container"] = "method",
130
- order: int = 0,
94
+ def infrastructure(
95
+ _cls=None, *, order: int = 0,
131
96
  profiles: Tuple[str, ...] = (),
132
97
  require_env: Tuple[str, ...] = (),
133
- predicate: Callable[[], bool] | None = None,
98
+ predicate: Optional[Callable[[], bool]] = None,
134
99
  ):
135
- """Declare an interceptor (method or container) with optional activation metadata."""
136
- def dec(obj):
137
- setattr(obj, INTERCEPTOR_META, {
138
- "kind": kind,
100
+ def dec(cls):
101
+ setattr(cls, INFRA_META, {
139
102
  "order": int(order),
140
103
  "profiles": tuple(profiles),
141
104
  "require_env": tuple(require_env),
142
105
  "predicate": predicate,
143
106
  })
144
- return obj
145
- return dec if _obj is None else dec(_obj)
146
-
107
+ return cls
108
+ return dec if _cls is None else dec(_cls)
147
109
 
148
110
  __all__ = [
149
111
  "component", "factory_component", "provides", "plugin",
150
112
  "Qualifier", "qualifier",
151
- "on_missing", "primary", "conditional", "interceptor",
113
+ "on_missing", "primary", "conditional", "infrastructure",
152
114
  "COMPONENT_FLAG", "COMPONENT_KEY", "COMPONENT_LAZY",
153
115
  "FACTORY_FLAG", "PROVIDES_KEY", "PROVIDES_LAZY",
154
116
  "PLUGIN_FLAG", "QUALIFIERS_KEY", "COMPONENT_TAGS", "PROVIDES_TAGS",
155
117
  "ON_MISSING_META", "PRIMARY_FLAG", "CONDITIONAL_META",
156
- "INTERCEPTOR_META",
118
+ "INFRA_META",
157
119
  ]
158
120
 
pico_ioc/infra.py ADDED
@@ -0,0 +1,196 @@
1
+ from __future__ import annotations
2
+ import re
3
+ from typing import Any, Callable, Iterable, Optional, Sequence, Tuple
4
+
5
+ class Select:
6
+ def __init__(self):
7
+ self._tags: set[str] = set()
8
+ self._profiles: set[str] = set()
9
+ self._class_name_regex: Optional[re.Pattern[str]] = None
10
+ self._method_name_regex: Optional[re.Pattern[str]] = None
11
+
12
+ def has_tag(self, *tags: str) -> "Select":
13
+ self._tags.update(t for t in tags if t)
14
+ return self
15
+
16
+ def profile_in(self, *profiles: str) -> "Select":
17
+ self._profiles.update(p for p in profiles if p)
18
+ return self
19
+
20
+ def class_name(self, regex: str) -> "Select":
21
+ self._class_name_regex = re.compile(regex)
22
+ return self
23
+
24
+ def method_name(self, regex: str) -> "Select":
25
+ self._method_name_regex = re.compile(regex)
26
+ return self
27
+
28
+ def is_effectively_empty(self) -> bool:
29
+ return not (self._tags or self._profiles or self._class_name_regex or self._method_name_regex)
30
+
31
+ def match_provider(self, key: Any, meta: dict, *, active_profiles: Sequence[str]) -> bool:
32
+ if self.is_effectively_empty():
33
+ return False
34
+ if self._tags:
35
+ tags = set(meta.get("tags", ()))
36
+ if not tags.intersection(self._tags):
37
+ return False
38
+ if self._profiles:
39
+ if not set(active_profiles).intersection(self._profiles):
40
+ return False
41
+ if self._class_name_regex and isinstance(key, type):
42
+ if not self._class_name_regex.search(getattr(key, "__name__", "")):
43
+ return False
44
+ return True
45
+
46
+ def match_method_name(self, method: str) -> bool:
47
+ if self._method_name_regex is None:
48
+ return True
49
+ return bool(self._method_name_regex.search(method))
50
+
51
+
52
+ class InfraQuery:
53
+ def __init__(self, container, profiles: Tuple[str, ...]):
54
+ self.c = container
55
+ self.profiles = profiles
56
+
57
+ def providers(self, where: Optional[Select] = None, *, limit: Optional[int] = None) -> list[tuple[Any, dict]]:
58
+ sel = where or Select()
59
+ items: list[tuple[Any, dict]] = []
60
+ for k, m in self.c._providers.items():
61
+ if sel.match_provider(k, m, active_profiles=self.profiles):
62
+ items.append((k, m))
63
+ if limit is not None and len(items) >= limit:
64
+ break
65
+ return items
66
+
67
+ def components(self, where: Optional[Select] = None, *, limit: Optional[int] = None) -> list[type]:
68
+ out: list[type] = []
69
+ for k, _m in self.providers(where=where, limit=limit):
70
+ if isinstance(k, type):
71
+ out.append(k)
72
+ return out
73
+
74
+
75
+ class InfraIntercept:
76
+ def __init__(self, container, profiles: Tuple[str, ...]):
77
+ self.c = container
78
+ self.profiles = profiles
79
+ self._per_method_cap: Optional[int] = None
80
+
81
+ def _collect_target_classes(self, where: Select) -> tuple[set[type], set[Any]]:
82
+ classes: set[type] = set()
83
+ keys: set[Any] = set()
84
+ for key, meta in self.c._providers.items():
85
+ if where.match_provider(key, meta, active_profiles=self.profiles):
86
+ keys.add(key)
87
+ if isinstance(key, type):
88
+ classes.add(key)
89
+ return classes, keys
90
+
91
+ def _guard_method_interceptor(self, interceptor, where: Select):
92
+ target_classes, _keys = self._collect_target_classes(where)
93
+ class_names = {cls.__name__ for cls in target_classes}
94
+ class Guarded:
95
+ def invoke(self, ctx, call_next):
96
+ tgt_cls = type(ctx.instance)
97
+ ok_class = any(isinstance(ctx.instance, cls) for cls in target_classes) or (getattr(tgt_cls, "__name__", "") in class_names)
98
+ ok_method = where.match_method_name(ctx.name)
99
+ if ok_class and ok_method:
100
+ return interceptor.invoke(ctx, call_next) if hasattr(interceptor, "invoke") else interceptor(ctx, call_next)
101
+ return call_next(ctx)
102
+ return Guarded()
103
+
104
+ def _guard_container_interceptor(self, interceptor, where: Select):
105
+ target_classes, keys = self._collect_target_classes(where)
106
+ class_names = {cls.__name__ for cls in target_classes}
107
+ def _ok(key: Any) -> bool:
108
+ if key in keys:
109
+ return True
110
+ if isinstance(key, type) and (key in target_classes or getattr(key, "__name__", "") in class_names):
111
+ return True
112
+ return False
113
+ class GuardedCI:
114
+ def around_resolve(self, ctx, call_next):
115
+ if _ok(ctx.key):
116
+ return interceptor.around_resolve(ctx, call_next)
117
+ return call_next(ctx)
118
+ def around_create(self, ctx, call_next):
119
+ if _ok(ctx.key):
120
+ return interceptor.around_create(ctx, call_next)
121
+ return call_next(ctx)
122
+ return GuardedCI()
123
+
124
+ def add(self, *, interceptor, where: Select) -> None:
125
+ sel = where or Select()
126
+ if sel.is_effectively_empty():
127
+ raise ValueError("empty selector for interceptor")
128
+ is_container = all(hasattr(interceptor, m) for m in ("around_resolve", "around_create"))
129
+ if is_container:
130
+ guarded = self._guard_container_interceptor(interceptor, sel)
131
+ self.c.add_container_interceptor(guarded)
132
+ return
133
+ guarded = self._guard_method_interceptor(interceptor, sel)
134
+ self.c.add_method_interceptor(guarded)
135
+
136
+ def limit_per_method(self, max_n: int) -> None:
137
+ self._per_method_cap = int(max_n)
138
+
139
+
140
+ class InfraMutate:
141
+ def __init__(self, container, profiles: Tuple[str, ...]):
142
+ self.c = container
143
+ self.profiles = profiles
144
+
145
+ def add_tags(self, component_or_key: Any, tags: Iterable[str]) -> None:
146
+ key = component_or_key
147
+ if key in self.c._providers:
148
+ meta = self.c._providers[key]
149
+ cur = tuple(meta.get("tags", ()))
150
+ new = tuple(dict.fromkeys(list(cur) + [t for t in tags if t]))
151
+ meta["tags"] = new
152
+ self.c._providers[key] = meta
153
+
154
+ def set_qualifiers(self, provider_key: Any, qualifiers: dict[str, Any]) -> None:
155
+ if provider_key in self.c._providers:
156
+ meta = self.c._providers[provider_key]
157
+ meta["qualifiers"] = tuple(qualifiers or ())
158
+ self.c._providers[provider_key] = meta
159
+
160
+ def replace_provider(self, *, key: Any, with_factory: Callable[[], object]) -> None:
161
+ if key in self.c._providers:
162
+ lazy = bool(self.c._providers[key].get("lazy", False))
163
+ self.c.bind(key, with_factory, lazy=lazy, tags=self.c._providers[key].get("tags", ()))
164
+
165
+ def wrap_provider(self, *, key: Any, around: Callable[[Callable[[], object]], Callable[[], object]]) -> None:
166
+ if key in self.c._providers:
167
+ meta = self.c._providers[key]
168
+ base_factory = meta.get("factory")
169
+ if callable(base_factory):
170
+ wrapped = around(base_factory)
171
+ self.c.bind(key, wrapped, lazy=bool(meta.get("lazy", False)), tags=meta.get("tags", ()))
172
+
173
+ def rename_key(self, *, old: Any, new: Any) -> None:
174
+ if old in self.c._providers and new not in self.c._providers:
175
+ self.c._providers[new] = self.c._providers.pop(old)
176
+
177
+ class Infra:
178
+ def __init__(self, *, container, profiles: Tuple[str, ...]):
179
+ self._c = container
180
+ self._profiles = profiles
181
+ self._query = InfraQuery(container, profiles)
182
+ self._intercept = InfraIntercept(container, profiles)
183
+ self._mutate = InfraMutate(container, profiles)
184
+
185
+ @property
186
+ def query(self) -> InfraQuery:
187
+ return self._query
188
+
189
+ @property
190
+ def intercept(self) -> InfraIntercept:
191
+ return self._intercept
192
+
193
+ @property
194
+ def mutate(self) -> InfraMutate:
195
+ return self._mutate
196
+
pico_ioc/interceptors.py CHANGED
@@ -1,56 +1,76 @@
1
- # src/pico_ioc/interceptors.py
2
1
  from __future__ import annotations
3
-
4
2
  import inspect
5
3
  from typing import Any, Callable, Protocol, Sequence
6
4
 
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
5
+ class MethodCtx:
6
+ __slots__ = ("instance", "cls", "method", "name", "args", "kwargs", "container", "tags", "qualifiers", "request_key", "local")
7
+ def __init__(self, *, instance: object, cls: type, method: Callable[..., Any], name: str, args: tuple, kwargs: dict, container: Any, tags: set[str] | None = None, qualifiers: dict[str, Any] | None = None, request_key: Any = None):
8
+ self.instance = instance
9
+ self.cls = cls
10
+ self.method = method
11
+ self.name = name
16
12
  self.args = args
17
13
  self.kwargs = kwargs
18
- self.is_async = inspect.iscoroutinefunction(call)
19
-
14
+ self.container = container
15
+ self.tags = set(tags or ())
16
+ self.qualifiers = dict(qualifiers or {})
17
+ self.request_key = request_key
18
+ self.local: dict[str, Any] = {}
19
+
20
+ class ResolveCtx:
21
+ __slots__ = ("key", "qualifiers", "requested_by", "profiles", "local")
22
+ def __init__(self, *, key: Any, qualifiers: dict[str, Any] | None, requested_by: Any, profiles: Sequence[str]):
23
+ self.key = key
24
+ self.qualifiers = dict(qualifiers or {})
25
+ self.requested_by = requested_by
26
+ self.profiles = tuple(profiles or ())
27
+ self.local: dict[str, Any] = {}
28
+
29
+ class CreateCtx:
30
+ __slots__ = ("key", "component", "provider", "profiles", "local")
31
+ def __init__(self, *, key: Any, component: type | None, provider: Callable[[], object], profiles: Sequence[str]):
32
+ self.key = key
33
+ self.component = component
34
+ self.provider = provider
35
+ self.profiles = tuple(profiles or ())
36
+ self.local: dict[str, Any] = {}
20
37
 
21
38
  class MethodInterceptor(Protocol):
22
- def __call__(self, inv: Invocation, proceed: Callable[[], Any]) -> Any: ...
39
+ def invoke(self, ctx: MethodCtx, call_next: Callable[[MethodCtx], Any]) -> Any: ...
23
40
 
41
+ class ContainerInterceptor(Protocol):
42
+ def around_resolve(self, ctx: ResolveCtx, call_next: Callable[[ResolveCtx], Any]) -> Any: ...
43
+ def around_create(self, ctx: CreateCtx, call_next: Callable[[CreateCtx], Any]) -> Any: ...
24
44
 
25
- async def _chain_async(interceptors: Sequence[MethodInterceptor], inv: Invocation, i: int = 0):
45
+ def _dispatch_method(interceptors: Sequence[MethodInterceptor], ctx: MethodCtx, i: int = 0):
26
46
  if i >= len(interceptors):
27
- return await inv.call(*inv.args, **inv.kwargs)
47
+ return ctx.method(*ctx.args, **ctx.kwargs)
28
48
  cur = interceptors[i]
49
+ return cur.invoke(ctx, lambda nxt: _dispatch_method(interceptors, nxt, i + 1))
29
50
 
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):
51
+ async def _dispatch_method_async(interceptors: Sequence[MethodInterceptor], ctx: MethodCtx, i: int = 0):
38
52
  if i >= len(interceptors):
39
- return inv.call(*inv.args, **inv.kwargs)
53
+ return await ctx.method(*ctx.args, **ctx.kwargs)
40
54
  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
-
55
+ res = cur.invoke(ctx, lambda nxt: _dispatch_method_async(interceptors, nxt, i + 1))
56
+ return await res if inspect.isawaitable(res) else res
50
57
 
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: ...
58
+ def dispatch_method(interceptors: Sequence[MethodInterceptor], ctx: MethodCtx):
59
+ if inspect.iscoroutinefunction(ctx.method):
60
+ return _dispatch_method_async(interceptors, ctx, 0)
61
+ return _dispatch_method(interceptors, ctx, 0)
62
+
63
+ def run_resolve_chain(interceptors: Sequence[ContainerInterceptor], ctx: ResolveCtx):
64
+ def call(i: int, c: ResolveCtx):
65
+ if i >= len(interceptors):
66
+ return None
67
+ return interceptors[i].around_resolve(c, lambda nxt: call(i + 1, nxt))
68
+ return call(0, ctx)
69
+
70
+ def run_create_chain(interceptors: Sequence[ContainerInterceptor], ctx: CreateCtx):
71
+ def call(i: int, c: CreateCtx):
72
+ if i >= len(interceptors):
73
+ return c.provider()
74
+ return interceptors[i].around_create(c, lambda nxt: call(i + 1, nxt))
75
+ return call(0, ctx)
56
76
 
pico_ioc/proxy.py CHANGED
@@ -1,15 +1,12 @@
1
- # src/pico_ioc/proxy.py
2
1
  from __future__ import annotations
3
2
 
4
3
  import inspect
5
4
  from functools import lru_cache
6
5
  from typing import Any, Callable, Sequence
7
6
 
8
- from .interceptors import Invocation, dispatch, MethodInterceptor
9
-
7
+ from .interceptors import MethodCtx, MethodInterceptor, dispatch_method
10
8
 
11
9
  class ComponentProxy:
12
- """Proxy for lazy components. Creates the real object only when accessed."""
13
10
  def __init__(self, object_creator: Callable[[], Any]):
14
11
  object.__setattr__(self, "_object_creator", object_creator)
15
12
  object.__setattr__(self, "__real_object", None)
@@ -31,8 +28,6 @@ class ComponentProxy:
31
28
  def __str__(self): return str(self._get_real_object())
32
29
  def __repr__(self): return repr(self._get_real_object())
33
30
  def __dir__(self): return dir(self._get_real_object())
34
-
35
- # container-like behavior
36
31
  def __len__(self): return len(self._get_real_object())
37
32
  def __getitem__(self, key): return self._get_real_object()[key]
38
33
  def __setitem__(self, key, value): self._get_real_object()[key] = value
@@ -40,8 +35,6 @@ class ComponentProxy:
40
35
  def __iter__(self): return iter(self._get_real_object())
41
36
  def __reversed__(self): return reversed(self._get_real_object())
42
37
  def __contains__(self, item): return item in self._get_real_object()
43
-
44
- # operators
45
38
  def __add__(self, other): return self._get_real_object() + other
46
39
  def __sub__(self, other): return self._get_real_object() - other
47
40
  def __mul__(self, other): return self._get_real_object() * other
@@ -56,8 +49,6 @@ class ComponentProxy:
56
49
  def __and__(self, other): return self._get_real_object() & other
57
50
  def __xor__(self, other): return self._get_real_object() ^ other
58
51
  def __or__(self, other): return self._get_real_object() | other
59
-
60
- # reflected operators
61
52
  def __radd__(self, other): return other + self._get_real_object()
62
53
  def __rsub__(self, other): return other - self._get_real_object()
63
54
  def __rmul__(self, other): return other * self._get_real_object()
@@ -72,8 +63,6 @@ class ComponentProxy:
72
63
  def __rand__(self, other): return other & self._get_real_object()
73
64
  def __rxor__(self, other): return other ^ self._get_real_object()
74
65
  def __ror__(self, other): return other | self._get_real_object()
75
-
76
- # misc
77
66
  def __neg__(self): return -self._get_real_object()
78
67
  def __pos__(self): return +self._get_real_object()
79
68
  def __abs__(self): return abs(self._get_real_object())
@@ -86,20 +75,18 @@ class ComponentProxy:
86
75
  def __ge__(self, other): return self._get_real_object() >= other
87
76
  def __hash__(self): return hash(self._get_real_object())
88
77
  def __bool__(self): return bool(self._get_real_object())
89
-
90
- # callables & context
91
78
  def __call__(self, *args, **kwargs): return self._get_real_object()(*args, **kwargs)
92
79
  def __enter__(self): return self._get_real_object().__enter__()
93
80
  def __exit__(self, exc_type, exc_val, exc_tb): return self._get_real_object().__exit__(exc_type, exc_val, exc_tb)
94
81
 
95
-
96
82
  class IoCProxy:
97
- """Proxy that wraps an object and applies MethodInterceptors on method calls."""
98
- __slots__ = ("_target", "_interceptors")
83
+ __slots__ = ("_target", "_interceptors", "_container", "_request_key")
99
84
 
100
- def __init__(self, target: object, interceptors: Sequence[MethodInterceptor]):
85
+ def __init__(self, target: object, interceptors: Sequence[MethodInterceptor], container: Any = None, request_key: Any = None):
101
86
  self._target = target
102
87
  self._interceptors = tuple(interceptors)
88
+ self._container = container
89
+ self._request_key = request_key
103
90
 
104
91
  def __getattr__(self, name: str) -> Any:
105
92
  attr = getattr(self._target, name)
@@ -109,18 +96,17 @@ class IoCProxy:
109
96
  bound_fn = attr.__get__(self._target, type(self._target))
110
97
  else:
111
98
  bound_fn = attr
112
-
113
99
  @lru_cache(maxsize=None)
114
100
  def _wrap(fn: Callable[..., Any]):
115
101
  if inspect.iscoroutinefunction(fn):
116
102
  async def aw(*args, **kwargs):
117
- inv = Invocation(self._target, name, fn, args, kwargs)
118
- return await dispatch(self._interceptors, inv)
103
+ ctx = MethodCtx(instance=self._target, cls=type(self._target), method=fn, name=name, args=args, kwargs=kwargs, container=self._container, request_key=self._request_key)
104
+ return await dispatch_method(self._interceptors, ctx)
119
105
  return aw
120
106
  else:
121
107
  def sw(*args, **kwargs):
122
- inv = Invocation(self._target, name, fn, args, kwargs)
123
- res = dispatch(self._interceptors, inv)
108
+ ctx = MethodCtx(instance=self._target, cls=type(self._target), method=fn, name=name, args=args, kwargs=kwargs, container=self._container, request_key=self._request_key)
109
+ res = dispatch_method(self._interceptors, ctx)
124
110
  if inspect.isawaitable(res):
125
111
  raise RuntimeError(f"Async interceptor on sync method: {name}")
126
112
  return res
pico_ioc/resolver.py CHANGED
@@ -1,68 +1,49 @@
1
- # src/pico_ioc/resolver.py
2
1
  from __future__ import annotations
3
2
 
4
3
  import inspect
5
4
  from typing import Any, Annotated, Callable, get_args, get_origin, get_type_hints
6
5
  from contextvars import ContextVar
7
6
 
8
-
9
7
  _path: ContextVar[list[tuple[str, str]]] = ContextVar("pico_resolve_path", default=[])
10
8
 
11
-
12
9
  def _get_hints(obj, owner_cls=None) -> dict:
13
- """Return type hints with include_extras=True, using correct globals/locals."""
14
10
  mod = inspect.getmodule(obj)
15
11
  g = getattr(mod, "__dict__", {})
16
12
  l = vars(owner_cls) if owner_cls is not None else None
17
13
  return get_type_hints(obj, globalns=g, localns=l, include_extras=True)
18
14
 
19
-
20
15
  def _is_collection_hint(tp) -> bool:
21
16
  origin = get_origin(tp) or tp
22
17
  return origin in (list, tuple)
23
18
 
24
-
25
19
  def _base_and_qualifiers_from_hint(tp):
26
- """
27
- Extract (base, qualifiers, container_kind) from a type hint.
28
- Supports list[T], tuple[T], Annotated[T, "qual1", ...].
29
- """
30
20
  origin = get_origin(tp) or tp
31
21
  args = get_args(tp) or ()
32
22
  container_kind = list if origin is list else tuple
33
-
34
23
  if not args:
35
24
  return (object, (), container_kind)
36
-
37
25
  inner = args[0]
38
26
  if get_origin(inner) is Annotated:
39
27
  base, *extras = get_args(inner)
40
28
  quals = tuple(a for a in extras if isinstance(a, str))
41
29
  return (base, quals, container_kind)
42
-
43
30
  return (inner, (), container_kind)
44
31
 
45
-
46
32
  class Resolver:
47
33
  def __init__(self, container, *, prefer_name_first: bool = True):
48
34
  self.c = container
49
35
  self._prefer_name_first = bool(prefer_name_first)
50
36
 
51
- # --- core resolution ---
52
-
53
37
  def _resolve_dependencies_for_callable(self, fn: Callable, owner_cls: Any = None) -> dict:
54
38
  sig = inspect.signature(fn)
55
39
  hints = _get_hints(fn, owner_cls=owner_cls)
56
40
  kwargs = {}
57
-
58
41
  path_owner = getattr(owner_cls, "__name__", getattr(fn, "__qualname__", "callable"))
59
42
  if fn.__name__ == "__init__" and owner_cls:
60
43
  path_owner = f"{path_owner}.__init__"
61
-
62
44
  for name, param in sig.parameters.items():
63
45
  if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD) or name == "self":
64
46
  continue
65
-
66
47
  ann = hints.get(name, param.annotation)
67
48
  st = _path.get()
68
49
  _path.set(st + [(path_owner, name)])
@@ -83,50 +64,38 @@ class Resolver:
83
64
  return kwargs
84
65
 
85
66
  def create_instance(self, cls: type) -> Any:
86
- """Instantiate a class by resolving its __init__ dependencies."""
87
67
  ctor_kwargs = self._resolve_dependencies_for_callable(cls.__init__, owner_cls=cls)
88
68
  return cls(**ctor_kwargs)
89
69
 
90
70
  def kwargs_for_callable(self, fn: Callable, *, owner_cls: Any = None) -> dict:
91
- """Resolve all keyword arguments for any callable."""
92
71
  return self._resolve_dependencies_for_callable(fn, owner_cls=owner_cls)
93
72
 
94
- # --- param resolution ---
95
-
96
73
  def _notify_resolve(self, key, ann, quals=()):
97
- for ci in getattr(self.c, "_container_interceptors", ()):
98
- try:
99
- ci.on_resolve(key, ann, tuple(quals) if quals else ())
100
- except Exception:
101
- pass
74
+ try:
75
+ self.c._notify_resolve(key, ann, quals)
76
+ except Exception:
77
+ pass
102
78
 
103
79
  def _resolve_param(self, name: str, ann: Any):
104
- # collections
105
80
  if _is_collection_hint(ann):
106
81
  base, quals, kind = _base_and_qualifiers_from_hint(ann)
107
82
  self._notify_resolve(base, ann, quals)
108
83
  items = self.c._resolve_all_for_base(base, qualifiers=quals)
109
84
  return list(items) if kind is list else tuple(items)
110
-
111
- # precedence
112
85
  if self._prefer_name_first and self.c.has(name):
113
86
  self._notify_resolve(name, ann, ())
114
87
  return self.c.get(name)
115
-
116
88
  if ann is not inspect._empty and self.c.has(ann):
117
89
  self._notify_resolve(ann, ann, ())
118
90
  return self.c.get(ann)
119
-
120
91
  if ann is not inspect._empty and isinstance(ann, type):
121
92
  for base in ann.__mro__[1:]:
122
93
  if self.c.has(base):
123
94
  self._notify_resolve(base, ann, ())
124
95
  return self.c.get(base)
125
-
126
96
  if self.c.has(name):
127
97
  self._notify_resolve(name, ann, ())
128
98
  return self.c.get(name)
129
-
130
99
  missing = ann if ann is not inspect._empty else name
131
100
  raise NameError(f"No provider found for key {missing!r}")
132
101