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/__init__.py +21 -11
- pico_ioc/_version.py +1 -1
- pico_ioc/api.py +3 -2
- pico_ioc/builder.py +31 -115
- pico_ioc/container.py +18 -55
- pico_ioc/decorators.py +11 -49
- pico_ioc/infra.py +196 -0
- pico_ioc/interceptors.py +59 -39
- pico_ioc/proxy.py +9 -23
- pico_ioc/resolver.py +4 -35
- pico_ioc/scanner.py +30 -55
- pico_ioc/scope.py +2 -7
- {pico_ioc-1.4.0.dist-info → pico_ioc-1.5.0.dist-info}/METADATA +10 -2
- pico_ioc-1.5.0.dist-info/RECORD +23 -0
- pico_ioc-1.4.0.dist-info/RECORD +0 -22
- {pico_ioc-1.4.0.dist-info → pico_ioc-1.5.0.dist-info}/WHEEL +0 -0
- {pico_ioc-1.4.0.dist-info → pico_ioc-1.5.0.dist-info}/licenses/LICENSE +0 -0
- {pico_ioc-1.4.0.dist-info → pico_ioc-1.5.0.dist-info}/top_level.txt +0 -0
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
|
|
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
|
-
|
|
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
|
-
|
|
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]
|
|
98
|
+
predicate: Optional[Callable[[], bool]] = None,
|
|
134
99
|
):
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
145
|
-
return dec if
|
|
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", "
|
|
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
|
-
"
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
self.
|
|
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.
|
|
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
|
|
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
|
-
|
|
45
|
+
def _dispatch_method(interceptors: Sequence[MethodInterceptor], ctx: MethodCtx, i: int = 0):
|
|
26
46
|
if i >= len(interceptors):
|
|
27
|
-
return
|
|
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
|
-
|
|
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
|
|
53
|
+
return await ctx.method(*ctx.args, **ctx.kwargs)
|
|
40
54
|
cur = interceptors[i]
|
|
41
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
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
|
-
"""
|
|
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
|
-
|
|
118
|
-
return await
|
|
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
|
-
|
|
123
|
-
res =
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|