pico-ioc 1.2.0__py3-none-any.whl → 1.3.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 +17 -4
- pico_ioc/_state.py +30 -0
- pico_ioc/_version.py +1 -1
- pico_ioc/api.py +186 -235
- pico_ioc/builder.py +242 -0
- pico_ioc/container.py +57 -29
- pico_ioc/decorators.py +63 -8
- pico_ioc/interceptors.py +50 -0
- pico_ioc/plugins.py +17 -1
- pico_ioc/policy.py +332 -0
- pico_ioc/proxy.py +41 -1
- pico_ioc/resolver.py +45 -40
- pico_ioc/scanner.py +74 -101
- pico_ioc/utils.py +25 -0
- {pico_ioc-1.2.0.dist-info → pico_ioc-1.3.0.dist-info}/METADATA +59 -16
- pico_ioc-1.3.0.dist-info/RECORD +20 -0
- pico_ioc/typing_utils.py +0 -29
- pico_ioc-1.2.0.dist-info/RECORD +0 -17
- {pico_ioc-1.2.0.dist-info → pico_ioc-1.3.0.dist-info}/WHEEL +0 -0
- {pico_ioc-1.2.0.dist-info → pico_ioc-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {pico_ioc-1.2.0.dist-info → pico_ioc-1.3.0.dist-info}/top_level.txt +0 -0
pico_ioc/builder.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# src/pico_ioc/builder.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import inspect as _inspect
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple
|
|
7
|
+
from typing import get_origin, get_args, Annotated
|
|
8
|
+
|
|
9
|
+
# Add missing imports for interceptor types
|
|
10
|
+
from .interceptors import MethodInterceptor, ContainerInterceptor
|
|
11
|
+
from .container import PicoContainer, _is_compatible
|
|
12
|
+
from .policy import apply_policy, _conditional_active
|
|
13
|
+
from .plugins import PicoPlugin, run_plugin_hook
|
|
14
|
+
from .scanner import scan_and_configure
|
|
15
|
+
from .resolver import Resolver, _get_hints
|
|
16
|
+
from . import _state
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PicoContainerBuilder:
|
|
20
|
+
def __init__(self):
|
|
21
|
+
self._scan_plan: List[Tuple[Any, Optional[Callable[[str], bool]], Tuple[PicoPlugin, ...]]] = []
|
|
22
|
+
self._overrides: Dict[Any, Any] = {}
|
|
23
|
+
self._profiles: Optional[List[str]] = None
|
|
24
|
+
self._plugins: Tuple[PicoPlugin, ...] = ()
|
|
25
|
+
self._include_tags: Optional[set[str]] = None
|
|
26
|
+
self._exclude_tags: Optional[set[str]] = None
|
|
27
|
+
self._roots: Iterable[type] = ()
|
|
28
|
+
self._providers: Dict[Any, Dict] = {}
|
|
29
|
+
self._interceptor_decls: List[Tuple[Any, dict]] = []
|
|
30
|
+
|
|
31
|
+
def with_plugins(self, plugins: Tuple[PicoPlugin, ...]) -> PicoContainerBuilder:
|
|
32
|
+
self._plugins = plugins
|
|
33
|
+
return self
|
|
34
|
+
|
|
35
|
+
def with_profiles(self, profiles: Optional[List[str]]) -> PicoContainerBuilder:
|
|
36
|
+
self._profiles = profiles
|
|
37
|
+
return self
|
|
38
|
+
|
|
39
|
+
def add_scan_package(self, package: Any, exclude: Optional[Callable[[str], bool]] = None) -> PicoContainerBuilder:
|
|
40
|
+
self._scan_plan.append((package, exclude, self._plugins))
|
|
41
|
+
return self
|
|
42
|
+
|
|
43
|
+
def with_overrides(self, overrides: Optional[Dict[Any, Any]]) -> PicoContainerBuilder:
|
|
44
|
+
self._overrides = overrides or {}
|
|
45
|
+
return self
|
|
46
|
+
|
|
47
|
+
def with_tag_filters(self, include: Optional[set[str]], exclude: Optional[set[str]]) -> PicoContainerBuilder:
|
|
48
|
+
self._include_tags = include
|
|
49
|
+
self._exclude_tags = exclude
|
|
50
|
+
return self
|
|
51
|
+
|
|
52
|
+
def with_roots(self, roots: Iterable[type]) -> PicoContainerBuilder:
|
|
53
|
+
self._roots = roots
|
|
54
|
+
return self
|
|
55
|
+
|
|
56
|
+
def build(self) -> PicoContainer:
|
|
57
|
+
requested_profiles = _resolve_profiles(self._profiles)
|
|
58
|
+
|
|
59
|
+
# We now create a single container instance upfront and configure it.
|
|
60
|
+
container = PicoContainer(providers=self._providers)
|
|
61
|
+
container._active_profiles = tuple(requested_profiles)
|
|
62
|
+
|
|
63
|
+
for pkg, exclude, scan_plugins in self._scan_plan:
|
|
64
|
+
with _state.scanning_flag():
|
|
65
|
+
c, f, decls = scan_and_configure(pkg, container, exclude=exclude, plugins=scan_plugins)
|
|
66
|
+
logging.info("Scanned '%s' (components: %d, factories: %d)", getattr(pkg, "__name__", pkg), c, f)
|
|
67
|
+
self._interceptor_decls.extend(decls)
|
|
68
|
+
|
|
69
|
+
_activate_and_build_interceptors(
|
|
70
|
+
container=container,
|
|
71
|
+
interceptor_decls=self._interceptor_decls,
|
|
72
|
+
profiles=requested_profiles
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
binder = container.binder()
|
|
76
|
+
|
|
77
|
+
if self._overrides:
|
|
78
|
+
_apply_overrides(container, self._overrides)
|
|
79
|
+
|
|
80
|
+
run_plugin_hook(self._plugins, "after_bind", container, binder)
|
|
81
|
+
run_plugin_hook(self._plugins, "before_eager", container, binder)
|
|
82
|
+
apply_policy(container, profiles=requested_profiles)
|
|
83
|
+
_filter_by_tags(container, self._include_tags, self._exclude_tags)
|
|
84
|
+
if self._roots:
|
|
85
|
+
_restrict_to_subgraph(container, self._roots, self._overrides)
|
|
86
|
+
|
|
87
|
+
run_plugin_hook(self._plugins, "after_ready", container, binder)
|
|
88
|
+
container.eager_instantiate_all()
|
|
89
|
+
logging.info("Container configured and ready.")
|
|
90
|
+
return container
|
|
91
|
+
|
|
92
|
+
# ... (Helper functions like _resolve_profiles, _apply_overrides etc. remain here) ...
|
|
93
|
+
# --- Start of moved helpers ---
|
|
94
|
+
def _resolve_profiles(profiles: Optional[list[str]]) -> list[str]:
|
|
95
|
+
if profiles is not None:
|
|
96
|
+
return list(profiles)
|
|
97
|
+
env_val = os.getenv("PICO_PROFILE", "")
|
|
98
|
+
return [p.strip() for p in env_val.split(",") if p.strip()]
|
|
99
|
+
|
|
100
|
+
def _as_provider(val):
|
|
101
|
+
if isinstance(val, tuple) and len(val) == 2 and callable(val[0]) and isinstance(val[1], bool):
|
|
102
|
+
return val[0], val[1]
|
|
103
|
+
if callable(val):
|
|
104
|
+
return val, False
|
|
105
|
+
return (lambda v=val: v), False
|
|
106
|
+
|
|
107
|
+
def _apply_overrides(container: PicoContainer, overrides: Dict[Any, Any]) -> None:
|
|
108
|
+
for key, val in overrides.items():
|
|
109
|
+
provider, lazy = _as_provider(val)
|
|
110
|
+
container.bind(key, provider, lazy=lazy)
|
|
111
|
+
|
|
112
|
+
def _filter_by_tags(container: PicoContainer, include_tags: Optional[set[str]], exclude_tags: Optional[set[str]]) -> None:
|
|
113
|
+
if not include_tags and not exclude_tags:
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
def _tag_ok(meta: dict) -> bool:
|
|
117
|
+
tags = set(meta.get("tags", ()))
|
|
118
|
+
if include_tags and not tags.intersection(include_tags):
|
|
119
|
+
return False
|
|
120
|
+
if exclude_tags and tags.intersection(exclude_tags):
|
|
121
|
+
return False
|
|
122
|
+
return True
|
|
123
|
+
container._providers = {k: v for k, v in container._providers.items() if _tag_ok(v)}
|
|
124
|
+
|
|
125
|
+
def _compute_allowed_subgraph(container: PicoContainer, roots: Iterable[type]) -> set:
|
|
126
|
+
allowed: set[Any] = set(roots) # Start with roots
|
|
127
|
+
stack = list(roots or ())
|
|
128
|
+
# ... (rest of the function is the same, just ensure it's here)
|
|
129
|
+
def _add_impls_for_base(base_t):
|
|
130
|
+
for prov_key, meta in container._providers.items():
|
|
131
|
+
cls = prov_key if isinstance(prov_key, type) else None
|
|
132
|
+
if cls is not None and _is_compatible(cls, base_t):
|
|
133
|
+
if prov_key not in allowed:
|
|
134
|
+
allowed.add(prov_key)
|
|
135
|
+
stack.append(prov_key)
|
|
136
|
+
|
|
137
|
+
while stack:
|
|
138
|
+
k = stack.pop()
|
|
139
|
+
# if k in allowed: continue # Redundant, add() handles it
|
|
140
|
+
allowed.add(k)
|
|
141
|
+
if isinstance(k, type): _add_impls_for_base(k)
|
|
142
|
+
cls = k if isinstance(k, type) else None
|
|
143
|
+
if cls is None or not container.has(k): continue
|
|
144
|
+
try:
|
|
145
|
+
sig = _inspect.signature(cls.__init__)
|
|
146
|
+
hints = _get_hints(cls.__init__, owner_cls=cls)
|
|
147
|
+
except Exception:
|
|
148
|
+
continue
|
|
149
|
+
for pname, param in sig.parameters.items():
|
|
150
|
+
if pname == "self": continue
|
|
151
|
+
ann = hints.get(pname, param.annotation)
|
|
152
|
+
origin = get_origin(ann) or ann
|
|
153
|
+
if origin in (list, tuple):
|
|
154
|
+
inner = (get_args(ann) or (object,))[0]
|
|
155
|
+
if get_origin(inner) is Annotated: inner = (get_args(inner) or (object,))[0]
|
|
156
|
+
if isinstance(inner, type):
|
|
157
|
+
if inner not in allowed:
|
|
158
|
+
stack.append(inner)
|
|
159
|
+
continue
|
|
160
|
+
if isinstance(ann, type) and ann not in allowed:
|
|
161
|
+
stack.append(ann)
|
|
162
|
+
elif container.has(pname) and pname not in allowed:
|
|
163
|
+
stack.append(pname)
|
|
164
|
+
return allowed
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _restrict_to_subgraph(container: PicoContainer, roots: Iterable[type], overrides: Optional[Dict[Any, Any]]) -> None:
|
|
168
|
+
allowed = _compute_allowed_subgraph(container, roots)
|
|
169
|
+
keep_keys: set[Any] = allowed | (set(overrides.keys()) if overrides else set())
|
|
170
|
+
container._providers = {k: v for k, v in container._providers.items() if k in keep_keys}
|
|
171
|
+
|
|
172
|
+
def _activate_and_build_interceptors(
|
|
173
|
+
*, container: PicoContainer, interceptor_decls: list[tuple[Any, dict]], profiles: list[str],
|
|
174
|
+
) -> None:
|
|
175
|
+
resolver = Resolver(container)
|
|
176
|
+
active: list[tuple[int, str, str, Any]] = []
|
|
177
|
+
activated_method_names: list[str] = []
|
|
178
|
+
activated_container_names: list[str] = []
|
|
179
|
+
skipped_debug: list[str] = []
|
|
180
|
+
|
|
181
|
+
def _interceptor_meta_active(meta: dict) -> bool:
|
|
182
|
+
profs = tuple(meta.get("profiles", ())) or ()
|
|
183
|
+
if profs and (not profiles or not any(p in profs for p in profiles)): return False
|
|
184
|
+
req_env = tuple(meta.get("require_env", ())) or ()
|
|
185
|
+
if req_env and not all(os.getenv(k) not in (None, "") for k in req_env): return False
|
|
186
|
+
pred = meta.get("predicate", None)
|
|
187
|
+
if callable(pred):
|
|
188
|
+
try:
|
|
189
|
+
if not bool(pred()): return False
|
|
190
|
+
except Exception:
|
|
191
|
+
logging.exception("Interceptor predicate failed; skipping")
|
|
192
|
+
return False
|
|
193
|
+
return True
|
|
194
|
+
|
|
195
|
+
def _looks_like_container_interceptor(inst: Any) -> bool:
|
|
196
|
+
return all(hasattr(inst, m) for m in ("on_resolve", "on_before_create", "on_after_create", "on_exception"))
|
|
197
|
+
|
|
198
|
+
for raw_obj, meta in interceptor_decls:
|
|
199
|
+
owner_cls, obj = (raw_obj[0], raw_obj[1]) if isinstance(raw_obj, tuple) and len(raw_obj) == 2 else (None, raw_obj)
|
|
200
|
+
qn = getattr(obj, "__qualname__", repr(obj))
|
|
201
|
+
if not _conditional_active(obj, profiles=profiles) or not _interceptor_meta_active(meta):
|
|
202
|
+
skipped_debug.append(f"skip:{qn}")
|
|
203
|
+
continue
|
|
204
|
+
try:
|
|
205
|
+
if isinstance(obj, type):
|
|
206
|
+
inst = resolver.create_instance(obj)
|
|
207
|
+
elif owner_cls is not None:
|
|
208
|
+
owner_inst = resolver.create_instance(owner_cls)
|
|
209
|
+
bound = obj.__get__(owner_inst, owner_cls)
|
|
210
|
+
kwargs = resolver.kwargs_for_callable(bound, owner_cls=owner_cls)
|
|
211
|
+
inst = bound(**kwargs)
|
|
212
|
+
else:
|
|
213
|
+
kwargs = resolver.kwargs_for_callable(obj, owner_cls=None)
|
|
214
|
+
inst = obj(**kwargs)
|
|
215
|
+
except Exception:
|
|
216
|
+
logging.exception("Failed to construct interceptor %r", obj)
|
|
217
|
+
continue
|
|
218
|
+
kind = meta.get("kind", "method")
|
|
219
|
+
if kind == "method" and not callable(inst):
|
|
220
|
+
logging.error("Interceptor %s is not valid for kind %s; skipping", qn, kind)
|
|
221
|
+
continue
|
|
222
|
+
if kind == "container" and not _looks_like_container_interceptor(inst):
|
|
223
|
+
logging.error("Container interceptor %s lacks required methods; skipping", qn)
|
|
224
|
+
continue
|
|
225
|
+
order = int(meta.get("order", 0))
|
|
226
|
+
active.append((order, qn, kind, inst))
|
|
227
|
+
|
|
228
|
+
active.sort(key=lambda t: (t[0], t[1]))
|
|
229
|
+
|
|
230
|
+
for _order, _qn, kind, inst in active:
|
|
231
|
+
if kind == "container":
|
|
232
|
+
container.add_container_interceptor(inst)
|
|
233
|
+
activated_container_names.append(_qn)
|
|
234
|
+
else:
|
|
235
|
+
container.add_method_interceptor(inst)
|
|
236
|
+
activated_method_names.append(_qn)
|
|
237
|
+
|
|
238
|
+
if activated_method_names or activated_container_names:
|
|
239
|
+
logging.info("Interceptors activated: method=%d, container=%d", len(activated_method_names), len(activated_container_names))
|
|
240
|
+
logging.debug("Activated method=%s; Activated container=%s", ", ".join(activated_method_names) or "-", ", ".join(activated_container_names) or "-")
|
|
241
|
+
if skipped_debug:
|
|
242
|
+
logging.debug("Skipped interceptors: %s", ", ".join(skipped_debug))
|
pico_ioc/container.py
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
# pico_ioc/container.py
|
|
1
|
+
# src/pico_ioc/container.py (Refactorizado)
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
import inspect
|
|
4
|
-
from typing import Any, Dict, get_origin, get_args, Annotated
|
|
5
|
-
import typing as _t
|
|
6
|
-
|
|
4
|
+
from typing import Any, Dict, get_origin, get_args, Annotated, Sequence, Optional, Callable, Union, Tuple
|
|
5
|
+
import typing as _t
|
|
6
|
+
from .proxy import IoCProxy
|
|
7
|
+
from .interceptors import MethodInterceptor, ContainerInterceptor
|
|
7
8
|
from .decorators import QUALIFIERS_KEY
|
|
8
|
-
from . import _state
|
|
9
|
+
from . import _state
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
class Binder:
|
|
@@ -23,14 +24,35 @@ class Binder:
|
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
class PicoContainer:
|
|
26
|
-
def __init__(self):
|
|
27
|
-
self._providers
|
|
27
|
+
def __init__(self, providers: Dict[Any, Dict[str, Any]]):
|
|
28
|
+
self._providers = providers
|
|
28
29
|
self._singletons: Dict[Any, Any] = {}
|
|
30
|
+
self._method_interceptors: tuple[MethodInterceptor, ...] = ()
|
|
31
|
+
self._container_interceptors: tuple[ContainerInterceptor, ...] = ()
|
|
32
|
+
self._active_profiles: tuple[str, ...] = ()
|
|
33
|
+
self._seen_interceptor_types: set[type] = set()
|
|
34
|
+
|
|
35
|
+
def add_method_interceptor(self, it: MethodInterceptor) -> None:
|
|
36
|
+
t = type(it)
|
|
37
|
+
if t in self._seen_interceptor_types:
|
|
38
|
+
return
|
|
39
|
+
self._seen_interceptor_types.add(t)
|
|
40
|
+
self._method_interceptors = self._method_interceptors + (it,)
|
|
41
|
+
|
|
42
|
+
def add_container_interceptor(self, it: ContainerInterceptor) -> None:
|
|
43
|
+
t = type(it)
|
|
44
|
+
if t in self._seen_interceptor_types:
|
|
45
|
+
return
|
|
46
|
+
self._seen_interceptor_types.add(t)
|
|
47
|
+
self._container_interceptors = self._container_interceptors + (it,)
|
|
48
|
+
|
|
49
|
+
def binder(self) -> Binder:
|
|
50
|
+
"""Returns a binder for this container."""
|
|
51
|
+
return Binder(self)
|
|
29
52
|
|
|
30
53
|
def bind(self, key: Any, provider, *, lazy: bool, tags: tuple[str, ...] = ()):
|
|
31
54
|
self._singletons.pop(key, None)
|
|
32
55
|
meta = {"factory": provider, "lazy": bool(lazy)}
|
|
33
|
-
# qualifiers already present:
|
|
34
56
|
try:
|
|
35
57
|
q = getattr(key, QUALIFIERS_KEY, ())
|
|
36
58
|
except Exception:
|
|
@@ -43,25 +65,41 @@ class PicoContainer:
|
|
|
43
65
|
return key in self._providers
|
|
44
66
|
|
|
45
67
|
def get(self, key: Any):
|
|
46
|
-
# block only when scanning and NOT currently resolving a dependency
|
|
47
68
|
if _state._scanning.get() and not _state._resolving.get():
|
|
48
69
|
raise RuntimeError("re-entrant container access during scan")
|
|
49
|
-
|
|
50
70
|
prov = self._providers.get(key)
|
|
51
71
|
if prov is None:
|
|
52
72
|
raise NameError(f"No provider found for key {key!r}")
|
|
53
|
-
|
|
54
73
|
if key in self._singletons:
|
|
55
74
|
return self._singletons[key]
|
|
56
75
|
|
|
57
|
-
|
|
76
|
+
for ci in self._container_interceptors:
|
|
77
|
+
try: ci.on_before_create(key)
|
|
78
|
+
except Exception: pass
|
|
79
|
+
|
|
58
80
|
tok = _state._resolving.set(True)
|
|
59
81
|
try:
|
|
60
|
-
|
|
82
|
+
try:
|
|
83
|
+
instance = prov["factory"]()
|
|
84
|
+
except BaseException as exc:
|
|
85
|
+
for ci in self._container_interceptors:
|
|
86
|
+
try: ci.on_exception(key, exc)
|
|
87
|
+
except Exception: pass
|
|
88
|
+
raise
|
|
61
89
|
finally:
|
|
62
90
|
_state._resolving.reset(tok)
|
|
63
91
|
|
|
64
|
-
|
|
92
|
+
if self._method_interceptors and not isinstance(instance, IoCProxy):
|
|
93
|
+
instance = IoCProxy(instance, self._method_interceptors)
|
|
94
|
+
|
|
95
|
+
for ci in self._container_interceptors:
|
|
96
|
+
try:
|
|
97
|
+
maybe = ci.on_after_create(key, instance)
|
|
98
|
+
if maybe is not None:
|
|
99
|
+
instance = maybe
|
|
100
|
+
except Exception:
|
|
101
|
+
pass
|
|
102
|
+
|
|
65
103
|
self._singletons[key] = instance
|
|
66
104
|
return instance
|
|
67
105
|
|
|
@@ -82,13 +120,8 @@ class PicoContainer:
|
|
|
82
120
|
cls = provider_key if isinstance(provider_key, type) else None
|
|
83
121
|
if cls is None:
|
|
84
122
|
continue
|
|
85
|
-
|
|
86
|
-
# Avoid self-inclusion loops: if the class itself requires a collection
|
|
87
|
-
# of `base_type` in its __init__, don't treat it as an implementation
|
|
88
|
-
# of `base_type` when building that collection.
|
|
89
123
|
if _requires_collection_of_base(cls, base_type):
|
|
90
124
|
continue
|
|
91
|
-
|
|
92
125
|
if _is_compatible(cls, base_type):
|
|
93
126
|
prov_qs = meta.get("qualifiers", ())
|
|
94
127
|
if all(q in prov_qs for q in qualifiers):
|
|
@@ -96,6 +129,9 @@ class PicoContainer:
|
|
|
96
129
|
matches.append(inst)
|
|
97
130
|
return matches
|
|
98
131
|
|
|
132
|
+
def get_providers(self) -> Dict[Any, Dict]:
|
|
133
|
+
return self._providers.copy()
|
|
134
|
+
|
|
99
135
|
|
|
100
136
|
def _is_protocol(t) -> bool:
|
|
101
137
|
return getattr(t, "_is_protocol", False) is True
|
|
@@ -109,7 +145,6 @@ def _is_compatible(cls, base) -> bool:
|
|
|
109
145
|
pass
|
|
110
146
|
|
|
111
147
|
if _is_protocol(base):
|
|
112
|
-
# simple structural check: ensure methods/attrs declared on the Protocol exist on the class
|
|
113
148
|
names = set(getattr(base, "__annotations__", {}).keys())
|
|
114
149
|
names.update(n for n in getattr(base, "__dict__", {}).keys() if not n.startswith("_"))
|
|
115
150
|
for n in names:
|
|
@@ -121,20 +156,15 @@ def _is_compatible(cls, base) -> bool:
|
|
|
121
156
|
|
|
122
157
|
return False
|
|
123
158
|
|
|
159
|
+
|
|
124
160
|
def _requires_collection_of_base(cls, base) -> bool:
|
|
125
|
-
"""
|
|
126
|
-
Return True if `cls.__init__` has any parameter annotated as a collection
|
|
127
|
-
(list/tuple, including Annotated variants) of `base`. This prevents treating
|
|
128
|
-
`cls` as an implementation of `base` while building that collection,
|
|
129
|
-
avoiding recursion.
|
|
130
|
-
"""
|
|
131
161
|
try:
|
|
132
162
|
sig = inspect.signature(cls.__init__)
|
|
133
163
|
except Exception:
|
|
134
164
|
return False
|
|
135
165
|
|
|
136
166
|
try:
|
|
137
|
-
from .resolver import _get_hints
|
|
167
|
+
from .resolver import _get_hints
|
|
138
168
|
hints = _get_hints(cls.__init__, owner_cls=cls)
|
|
139
169
|
except Exception:
|
|
140
170
|
hints = {}
|
|
@@ -146,7 +176,6 @@ def _requires_collection_of_base(cls, base) -> bool:
|
|
|
146
176
|
origin = get_origin(ann) or ann
|
|
147
177
|
if origin in (list, tuple, _t.List, _t.Tuple):
|
|
148
178
|
inner = (get_args(ann) or (object,))[0]
|
|
149
|
-
# Unwrap Annotated[T, ...] si aparece
|
|
150
179
|
if get_origin(inner) is Annotated:
|
|
151
180
|
args = get_args(inner)
|
|
152
181
|
if args:
|
|
@@ -155,4 +184,3 @@ def _requires_collection_of_base(cls, base) -> bool:
|
|
|
155
184
|
return True
|
|
156
185
|
return False
|
|
157
186
|
|
|
158
|
-
|
pico_ioc/decorators.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# pico_ioc/decorators.py
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
import functools
|
|
4
|
-
from typing import Any, Iterable
|
|
4
|
+
from typing import Any, Iterable, Optional, Callable, Tuple, Literal
|
|
5
5
|
|
|
6
6
|
COMPONENT_FLAG = "_is_component"
|
|
7
7
|
COMPONENT_KEY = "_component_key"
|
|
@@ -17,6 +17,13 @@ QUALIFIERS_KEY = "_pico_qualifiers"
|
|
|
17
17
|
COMPONENT_TAGS = "_pico_tags"
|
|
18
18
|
PROVIDES_TAGS = "_pico_tags"
|
|
19
19
|
|
|
20
|
+
ON_MISSING_META = "_pico_on_missing"
|
|
21
|
+
PRIMARY_FLAG = "_pico_primary"
|
|
22
|
+
CONDITIONAL_META = "_pico_conditional"
|
|
23
|
+
|
|
24
|
+
INTERCEPTOR_META = "__pico_interceptor__"
|
|
25
|
+
|
|
26
|
+
|
|
20
27
|
def factory_component(cls):
|
|
21
28
|
setattr(cls, FACTORY_FLAG, True)
|
|
22
29
|
return cls
|
|
@@ -31,6 +38,7 @@ def component(cls=None, *, name: Any = None, lazy: bool = False, tags: Iterable[
|
|
|
31
38
|
return c
|
|
32
39
|
return dec(cls) if cls else dec
|
|
33
40
|
|
|
41
|
+
|
|
34
42
|
def provides(key: Any, *, lazy: bool = False, tags: Iterable[str] = ()):
|
|
35
43
|
def dec(fn):
|
|
36
44
|
@functools.wraps(fn)
|
|
@@ -49,7 +57,7 @@ def plugin(cls):
|
|
|
49
57
|
|
|
50
58
|
|
|
51
59
|
class Qualifier(str):
|
|
52
|
-
__slots__ = ()
|
|
60
|
+
__slots__ = ()
|
|
53
61
|
|
|
54
62
|
|
|
55
63
|
def qualifier(*qs: Qualifier):
|
|
@@ -66,14 +74,61 @@ def qualifier(*qs: Qualifier):
|
|
|
66
74
|
return dec
|
|
67
75
|
|
|
68
76
|
|
|
77
|
+
def on_missing(selector: object, *, priority: int = 0):
|
|
78
|
+
def dec(obj):
|
|
79
|
+
setattr(obj, ON_MISSING_META, {"selector": selector, "priority": int(priority)})
|
|
80
|
+
return obj
|
|
81
|
+
return dec
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def primary(obj):
|
|
85
|
+
setattr(obj, PRIMARY_FLAG, True)
|
|
86
|
+
return obj
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def conditional(
|
|
90
|
+
*,
|
|
91
|
+
profiles: tuple[str, ...] = (),
|
|
92
|
+
require_env: tuple[str, ...] = (),
|
|
93
|
+
predicate: Optional[Callable[[], bool]] = None,
|
|
94
|
+
):
|
|
95
|
+
def dec(obj):
|
|
96
|
+
setattr(obj, CONDITIONAL_META, {
|
|
97
|
+
"profiles": tuple(profiles),
|
|
98
|
+
"require_env": tuple(require_env),
|
|
99
|
+
"predicate": predicate,
|
|
100
|
+
})
|
|
101
|
+
return obj
|
|
102
|
+
return dec
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def interceptor(
|
|
106
|
+
_obj=None,
|
|
107
|
+
*,
|
|
108
|
+
kind: Literal["method", "container"] = "method",
|
|
109
|
+
order: int = 0,
|
|
110
|
+
profiles: Tuple[str, ...] = (),
|
|
111
|
+
require_env: Tuple[str, ...] = (),
|
|
112
|
+
predicate: Callable[[], bool] | None = None,
|
|
113
|
+
):
|
|
114
|
+
def dec(obj):
|
|
115
|
+
setattr(obj, INTERCEPTOR_META, {
|
|
116
|
+
"kind": kind,
|
|
117
|
+
"order": int(order),
|
|
118
|
+
"profiles": tuple(profiles),
|
|
119
|
+
"require_env": tuple(require_env),
|
|
120
|
+
"predicate": predicate,
|
|
121
|
+
})
|
|
122
|
+
return obj
|
|
123
|
+
return dec if _obj is None else dec(_obj)
|
|
124
|
+
|
|
69
125
|
__all__ = [
|
|
70
|
-
|
|
71
|
-
"component", "factory_component", "provides", "plugin", "qualifier",
|
|
72
|
-
# qualifier type
|
|
73
|
-
"Qualifier",
|
|
74
|
-
# metadata keys (exported for advanced use/testing)
|
|
126
|
+
"component", "factory_component", "provides", "plugin", "qualifier", "Qualifier",
|
|
75
127
|
"COMPONENT_FLAG", "COMPONENT_KEY", "COMPONENT_LAZY",
|
|
76
128
|
"FACTORY_FLAG", "PROVIDES_KEY", "PROVIDES_LAZY",
|
|
77
|
-
"PLUGIN_FLAG", "QUALIFIERS_KEY", "COMPONENT_TAGS", "PROVIDES_TAGS"
|
|
129
|
+
"PLUGIN_FLAG", "QUALIFIERS_KEY", "COMPONENT_TAGS", "PROVIDES_TAGS",
|
|
130
|
+
"on_missing", "primary", "conditional",
|
|
131
|
+
"ON_MISSING_META", "PRIMARY_FLAG", "CONDITIONAL_META",
|
|
132
|
+
"interceptor", "INTERCEPTOR_META",
|
|
78
133
|
]
|
|
79
134
|
|
pico_ioc/interceptors.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# pico_ioc/interceptors.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import Any, Callable, Protocol, Sequence
|
|
4
|
+
import inspect
|
|
5
|
+
|
|
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
|
|
14
|
+
self.args = args
|
|
15
|
+
self.kwargs = kwargs
|
|
16
|
+
self.is_async = inspect.iscoroutinefunction(call)
|
|
17
|
+
|
|
18
|
+
class MethodInterceptor(Protocol):
|
|
19
|
+
def __call__(self, inv: Invocation, proceed: Callable[[], Any]) -> Any: ...
|
|
20
|
+
|
|
21
|
+
async def _chain_async(interceptors: Sequence[MethodInterceptor], inv: Invocation, i: int = 0):
|
|
22
|
+
if i >= len(interceptors):
|
|
23
|
+
return await inv.call(*inv.args, **inv.kwargs)
|
|
24
|
+
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
|
|
29
|
+
|
|
30
|
+
def _chain_sync(interceptors: Sequence[MethodInterceptor], inv: Invocation, i: int = 0):
|
|
31
|
+
if i >= len(interceptors):
|
|
32
|
+
return inv.call(*inv.args, **inv.kwargs)
|
|
33
|
+
cur = interceptors[i]
|
|
34
|
+
return cur(inv, lambda: _chain_sync(interceptors, inv, i + 1))
|
|
35
|
+
|
|
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
|
|
43
|
+
|
|
44
|
+
|
|
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: ...
|
|
50
|
+
|
pico_ioc/plugins.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# pico_ioc/plugins.py
|
|
2
|
-
from typing import Protocol, Any
|
|
2
|
+
from typing import Protocol, Any, Tuple
|
|
3
3
|
from .container import Binder, PicoContainer
|
|
4
|
+
import logging
|
|
4
5
|
|
|
5
6
|
class PicoPlugin(Protocol):
|
|
6
7
|
def before_scan(self, package: Any, binder: Binder) -> None: ...
|
|
@@ -10,3 +11,18 @@ class PicoPlugin(Protocol):
|
|
|
10
11
|
def before_eager(self, container: PicoContainer, binder: Binder) -> None: ...
|
|
11
12
|
def after_ready(self, container: PicoContainer, binder: Binder) -> None: ...
|
|
12
13
|
|
|
14
|
+
def run_plugin_hook(
|
|
15
|
+
plugins: Tuple[PicoPlugin, ...],
|
|
16
|
+
hook_name: str,
|
|
17
|
+
*args,
|
|
18
|
+
**kwargs,
|
|
19
|
+
) -> None:
|
|
20
|
+
"""Run a lifecycle hook across all plugins, logging (but not raising) exceptions."""
|
|
21
|
+
for pl in plugins:
|
|
22
|
+
try:
|
|
23
|
+
fn = getattr(pl, hook_name, None)
|
|
24
|
+
if fn:
|
|
25
|
+
fn(*args, **kwargs)
|
|
26
|
+
except Exception:
|
|
27
|
+
logging.exception("Plugin %s failed", hook_name)
|
|
28
|
+
|