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 +28 -5
- pico_ioc/_state.py +60 -25
- pico_ioc/_version.py +1 -1
- pico_ioc/api.py +38 -56
- pico_ioc/builder.py +68 -100
- pico_ioc/config.py +332 -0
- pico_ioc/container.py +26 -44
- pico_ioc/decorators.py +15 -29
- pico_ioc/infra.py +196 -0
- pico_ioc/interceptors.py +59 -33
- pico_ioc/policy.py +102 -189
- pico_ioc/proxy.py +22 -24
- pico_ioc/resolver.py +12 -40
- pico_ioc/scanner.py +42 -67
- pico_ioc/scope.py +41 -0
- {pico_ioc-1.3.0.dist-info → pico_ioc-1.5.0.dist-info}/METADATA +15 -1
- pico_ioc-1.5.0.dist-info/RECORD +23 -0
- pico_ioc-1.3.0.dist-info/RECORD +0 -20
- {pico_ioc-1.3.0.dist-info → pico_ioc-1.5.0.dist-info}/WHEEL +0 -0
- {pico_ioc-1.3.0.dist-info → pico_ioc-1.5.0.dist-info}/licenses/LICENSE +0 -0
- {pico_ioc-1.3.0.dist-info → pico_ioc-1.5.0.dist-info}/top_level.txt +0 -0
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
|
|
7
|
-
__slots__ = ("
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
self.
|
|
12
|
-
self.
|
|
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.
|
|
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
|
|
39
|
+
def invoke(self, ctx: MethodCtx, call_next: Callable[[MethodCtx], Any]) -> Any: ...
|
|
20
40
|
|
|
21
|
-
|
|
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
|
|
47
|
+
return ctx.method(*ctx.args, **ctx.kwargs)
|
|
24
48
|
cur = interceptors[i]
|
|
25
|
-
|
|
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
|
|
51
|
+
async def _dispatch_method_async(interceptors: Sequence[MethodInterceptor], ctx: MethodCtx, i: int = 0):
|
|
31
52
|
if i >= len(interceptors):
|
|
32
|
-
return
|
|
53
|
+
return await ctx.method(*ctx.args, **ctx.kwargs)
|
|
33
54
|
cur = interceptors[i]
|
|
34
|
-
|
|
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
|
|
37
|
-
if
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
46
|
-
def
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|