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/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,50 +1,76 @@
1
- # pico_ioc/interceptors.py
2
1
  from __future__ import annotations
3
- from typing import Any, Callable, Protocol, Sequence
4
2
  import inspect
3
+ from typing import Any, Callable, Protocol, Sequence
5
4
 
6
- class Invocation:
7
- __slots__ = ("target", "method_name", "call", "args", "kwargs", "is_async")
8
-
9
- def __init__(self, target: object, method_name: str, call: Callable[..., Any],
10
- args: tuple, kwargs: dict):
11
- self.target = target
12
- self.method_name = method_name
13
- 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
14
12
  self.args = args
15
13
  self.kwargs = kwargs
16
- self.is_async = inspect.iscoroutinefunction(call)
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] = {}
17
37
 
18
38
  class MethodInterceptor(Protocol):
19
- def __call__(self, inv: Invocation, proceed: Callable[[], Any]) -> Any: ...
39
+ def invoke(self, ctx: MethodCtx, call_next: Callable[[MethodCtx], Any]) -> Any: ...
20
40
 
21
- async def _chain_async(interceptors: Sequence[MethodInterceptor], inv: Invocation, i: int = 0):
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: ...
44
+
45
+ def _dispatch_method(interceptors: Sequence[MethodInterceptor], ctx: MethodCtx, i: int = 0):
22
46
  if i >= len(interceptors):
23
- return await inv.call(*inv.args, **inv.kwargs)
47
+ return ctx.method(*ctx.args, **ctx.kwargs)
24
48
  cur = interceptors[i]
25
- async def next_step():
26
- return await _chain_async(interceptors, inv, i + 1)
27
- res = cur(inv, next_step)
28
- return await res if inspect.isawaitable(res) else res
49
+ return cur.invoke(ctx, lambda nxt: _dispatch_method(interceptors, nxt, i + 1))
29
50
 
30
- 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):
31
52
  if i >= len(interceptors):
32
- return inv.call(*inv.args, **inv.kwargs)
53
+ return await ctx.method(*ctx.args, **ctx.kwargs)
33
54
  cur = interceptors[i]
34
- return cur(inv, lambda: _chain_sync(interceptors, inv, i + 1))
55
+ res = cur.invoke(ctx, lambda nxt: _dispatch_method_async(interceptors, nxt, i + 1))
56
+ return await res if inspect.isawaitable(res) else res
35
57
 
36
- def dispatch(interceptors: Sequence[MethodInterceptor], inv: Invocation):
37
- if inv.is_async:
38
- # return a coroutine that the caller will await
39
- return _chain_async(interceptors, inv, 0)
40
- # return the final value directly for sync methods
41
- res = _chain_sync(interceptors, inv, 0)
42
- return res
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)
43
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)
44
69
 
45
- class ContainerInterceptor(Protocol):
46
- def on_resolve(self, key: Any, annotation: Any, qualifiers: tuple[str, ...] | tuple()) -> None: ...
47
- def on_before_create(self, key: Any) -> None: ...
48
- def on_after_create(self, key: Any, instance: Any) -> Any: ...
49
- def on_exception(self, key: Any, exc: BaseException) -> None: ...
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)
50
76