pico-ioc 1.5.0__py3-none-any.whl → 2.0.1__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 +82 -57
- pico_ioc/_version.py +1 -1
- pico_ioc/aop.py +281 -0
- pico_ioc/api.py +1137 -198
- pico_ioc/config_runtime.py +289 -0
- pico_ioc/constants.py +10 -0
- pico_ioc/container.py +420 -148
- pico_ioc/event_bus.py +223 -0
- pico_ioc/exceptions.py +72 -0
- pico_ioc/factory.py +48 -0
- pico_ioc/locator.py +53 -0
- pico_ioc/scope.py +155 -39
- pico_ioc-2.0.1.dist-info/METADATA +243 -0
- pico_ioc-2.0.1.dist-info/RECORD +17 -0
- pico_ioc/_state.py +0 -75
- pico_ioc/builder.py +0 -210
- pico_ioc/config.py +0 -332
- pico_ioc/decorators.py +0 -120
- pico_ioc/infra.py +0 -196
- pico_ioc/interceptors.py +0 -76
- pico_ioc/plugins.py +0 -28
- pico_ioc/policy.py +0 -245
- pico_ioc/proxy.py +0 -115
- pico_ioc/public_api.py +0 -76
- pico_ioc/resolver.py +0 -101
- pico_ioc/scanner.py +0 -178
- pico_ioc/utils.py +0 -25
- pico_ioc-1.5.0.dist-info/METADATA +0 -249
- pico_ioc-1.5.0.dist-info/RECORD +0 -23
- {pico_ioc-1.5.0.dist-info → pico_ioc-2.0.1.dist-info}/WHEEL +0 -0
- {pico_ioc-1.5.0.dist-info → pico_ioc-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {pico_ioc-1.5.0.dist-info → pico_ioc-2.0.1.dist-info}/top_level.txt +0 -0
pico_ioc/container.py
CHANGED
|
@@ -1,168 +1,440 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
# src/pico_ioc/container.py
|
|
3
2
|
import inspect
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import contextvars
|
|
4
|
+
from typing import Any, Dict, List, Optional, Tuple, overload, Union
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from .constants import LOGGER, PICO_META
|
|
7
|
+
from .exceptions import CircularDependencyError, ComponentCreationError, ProviderNotFoundError
|
|
8
|
+
from .factory import ComponentFactory
|
|
9
|
+
from .locator import ComponentLocator
|
|
10
|
+
from .scope import ScopedCaches, ScopeManager
|
|
11
|
+
from .aop import UnifiedComponentProxy, ContainerObserver
|
|
6
12
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
from .decorators import QUALIFIERS_KEY
|
|
10
|
-
from . import _state
|
|
13
|
+
KeyT = Union[str, type]
|
|
14
|
+
_resolve_chain: contextvars.ContextVar[Tuple[KeyT, ...]] = contextvars.ContextVar("pico_resolve_chain", default=())
|
|
11
15
|
|
|
12
|
-
class
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
class _TracerFrame:
|
|
17
|
+
__slots__ = ("parent_key", "via")
|
|
18
|
+
def __init__(self, parent_key: KeyT, via: str):
|
|
19
|
+
self.parent_key = parent_key
|
|
20
|
+
self.via = via
|
|
15
21
|
|
|
16
|
-
|
|
17
|
-
|
|
22
|
+
class ResolutionTracer:
|
|
23
|
+
def __init__(self, container: "PicoContainer") -> None:
|
|
24
|
+
self._container = container
|
|
25
|
+
self._stack_var: contextvars.ContextVar[List[_TracerFrame]] = contextvars.ContextVar("pico_tracer_stack", default=[])
|
|
26
|
+
self._edges: Dict[Tuple[KeyT, KeyT], Tuple[str, str]] = {}
|
|
18
27
|
|
|
19
|
-
def
|
|
20
|
-
|
|
28
|
+
def enter(self, parent_key: KeyT, via: str) -> contextvars.Token:
|
|
29
|
+
stack = list(self._stack_var.get())
|
|
30
|
+
stack.append(_TracerFrame(parent_key, via))
|
|
31
|
+
return self._stack_var.set(stack)
|
|
21
32
|
|
|
22
|
-
def
|
|
23
|
-
|
|
33
|
+
def leave(self, token: contextvars.Token) -> None:
|
|
34
|
+
self._stack_var.reset(token)
|
|
24
35
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def add_container_interceptor(self, it: ContainerInterceptor) -> None:
|
|
39
|
-
t = type(it)
|
|
40
|
-
if t in self._seen_interceptor_types:
|
|
36
|
+
def override_via(self, new_via: str) -> Optional[str]:
|
|
37
|
+
stack = self._stack_var.get()
|
|
38
|
+
if not stack:
|
|
39
|
+
return None
|
|
40
|
+
prev = stack[-1].via
|
|
41
|
+
stack[-1].via = new_via
|
|
42
|
+
return prev
|
|
43
|
+
|
|
44
|
+
def restore_via(self, previous: Optional[str]) -> None:
|
|
45
|
+
if previous is None:
|
|
46
|
+
return
|
|
47
|
+
stack = self._stack_var.get()
|
|
48
|
+
if not stack:
|
|
41
49
|
return
|
|
42
|
-
|
|
43
|
-
self._container_interceptors = self._container_interceptors + (it,)
|
|
50
|
+
stack[-1].via = previous
|
|
44
51
|
|
|
45
|
-
def
|
|
46
|
-
|
|
52
|
+
def note_param(self, child_key: KeyT, param_name: str) -> None:
|
|
53
|
+
stack = self._stack_var.get()
|
|
54
|
+
if not stack:
|
|
55
|
+
return
|
|
56
|
+
parent = stack[-1].parent_key
|
|
57
|
+
via = stack[-1].via
|
|
58
|
+
self._edges[(parent, child_key)] = (via, param_name)
|
|
47
59
|
|
|
48
|
-
def
|
|
49
|
-
|
|
60
|
+
def describe_cycle(self, chain: Tuple[KeyT, ...], current: KeyT, locator: Optional[ComponentLocator]) -> str:
|
|
61
|
+
def name_of(k: KeyT) -> str:
|
|
62
|
+
return getattr(k, "__name__", str(k))
|
|
63
|
+
def scope_of(k: KeyT) -> str:
|
|
64
|
+
if not locator:
|
|
65
|
+
return "singleton"
|
|
66
|
+
md = locator._metadata.get(k)
|
|
67
|
+
return md.scope if md else "singleton"
|
|
68
|
+
lines: List[str] = []
|
|
69
|
+
lines.append("Circular dependency detected.")
|
|
70
|
+
lines.append("")
|
|
71
|
+
lines.append("Resolution chain:")
|
|
72
|
+
full = tuple(chain) + (current,)
|
|
73
|
+
for idx, k in enumerate(full, 1):
|
|
74
|
+
mark = " ❌" if idx == len(full) else ""
|
|
75
|
+
lines.append(f" {idx}. {name_of(k)} [scope={scope_of(k)}]{mark}")
|
|
76
|
+
if idx < len(full):
|
|
77
|
+
parent = k
|
|
78
|
+
child = full[idx]
|
|
79
|
+
via, param = self._edges.get((parent, child), ("provider", "?"))
|
|
80
|
+
lines.append(f" └─ via {via} param '{param}' → {name_of(child)}")
|
|
81
|
+
lines.append("")
|
|
82
|
+
lines.append("Hint: break the cycle with a @configure setter or use a factory/provider.")
|
|
83
|
+
return "\n".join(lines)
|
|
50
84
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
class PicoContainer:
|
|
86
|
+
_container_id_var: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar("pico_container_id", default=None)
|
|
87
|
+
_container_registry: Dict[str, "PicoContainer"] = {}
|
|
88
|
+
|
|
89
|
+
class _Ctx:
|
|
90
|
+
def __init__(self, container_id: str, profiles: Tuple[str, ...], created_at: float) -> None:
|
|
91
|
+
self.container_id = container_id
|
|
92
|
+
self.profiles = profiles
|
|
93
|
+
self.created_at = created_at
|
|
94
|
+
self.resolve_count = 0
|
|
95
|
+
self.cache_hit_count = 0
|
|
96
|
+
|
|
97
|
+
def __init__(self, component_factory: ComponentFactory, caches: ScopedCaches, scopes: ScopeManager, observers: Optional[List["ContainerObserver"]] = None, container_id: Optional[str] = None, profiles: Tuple[str, ...] = ()) -> None:
|
|
98
|
+
self._factory = component_factory
|
|
99
|
+
self._caches = caches
|
|
100
|
+
self.scopes = scopes
|
|
101
|
+
self._locator: Optional[ComponentLocator] = None
|
|
102
|
+
self._observers = list(observers or [])
|
|
103
|
+
self.container_id = container_id or self._generate_container_id()
|
|
104
|
+
import time as _t
|
|
105
|
+
self.context = PicoContainer._Ctx(container_id=self.container_id, profiles=profiles, created_at=_t.time())
|
|
106
|
+
PicoContainer._container_registry[self.container_id] = self
|
|
107
|
+
self._tracer = ResolutionTracer(self)
|
|
108
|
+
|
|
109
|
+
@staticmethod
|
|
110
|
+
def _generate_container_id() -> str:
|
|
111
|
+
import time as _t, random as _r
|
|
112
|
+
return f"c{_t.time_ns():x}{_r.randrange(1<<16):04x}"
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def get_current(cls) -> Optional["PicoContainer"]:
|
|
116
|
+
cid = cls._container_id_var.get()
|
|
117
|
+
return cls._container_registry.get(cid) if cid else None
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def get_current_id(cls) -> Optional[str]:
|
|
121
|
+
return cls._container_id_var.get()
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def all_containers(cls) -> Dict[str, "PicoContainer"]:
|
|
125
|
+
return dict(cls._container_registry)
|
|
126
|
+
|
|
127
|
+
def activate(self) -> contextvars.Token:
|
|
128
|
+
return PicoContainer._container_id_var.set(self.container_id)
|
|
129
|
+
|
|
130
|
+
def deactivate(self, token: contextvars.Token) -> None:
|
|
131
|
+
PicoContainer._container_id_var.reset(token)
|
|
132
|
+
|
|
133
|
+
@contextmanager
|
|
134
|
+
def as_current(self):
|
|
135
|
+
token = self.activate()
|
|
82
136
|
try:
|
|
83
|
-
|
|
137
|
+
yield self
|
|
84
138
|
finally:
|
|
85
|
-
|
|
86
|
-
if self._method_interceptors and not isinstance(instance, IoCProxy):
|
|
87
|
-
chain = self._method_interceptors
|
|
88
|
-
cap = getattr(self, "_method_cap", None)
|
|
89
|
-
if isinstance(cap, int) and cap >= 0:
|
|
90
|
-
chain = chain[:cap]
|
|
91
|
-
instance = IoCProxy(instance, chain, container=self, request_key=key)
|
|
92
|
-
self._singletons[key] = instance
|
|
93
|
-
return instance
|
|
139
|
+
self.deactivate(token)
|
|
94
140
|
|
|
95
|
-
def
|
|
96
|
-
|
|
97
|
-
if not prov["lazy"]:
|
|
98
|
-
self.get(key)
|
|
141
|
+
def attach_locator(self, locator: ComponentLocator) -> None:
|
|
142
|
+
self._locator = locator
|
|
99
143
|
|
|
100
|
-
def
|
|
101
|
-
|
|
144
|
+
def _cache_for(self, key: KeyT):
|
|
145
|
+
md = self._locator._metadata.get(key) if self._locator else None
|
|
146
|
+
sc = (md.scope if md else "singleton")
|
|
147
|
+
return self._caches.for_scope(self.scopes, sc)
|
|
102
148
|
|
|
103
|
-
def
|
|
104
|
-
|
|
149
|
+
def has(self, key: KeyT) -> bool:
|
|
150
|
+
cache = self._cache_for(key)
|
|
151
|
+
return cache.get(key) is not None or self._factory.has(key)
|
|
105
152
|
|
|
106
|
-
def
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
153
|
+
def _canonical_key(self, key: KeyT) -> KeyT:
|
|
154
|
+
if self._factory.has(key):
|
|
155
|
+
return key
|
|
156
|
+
if isinstance(key, type) and self._locator:
|
|
157
|
+
cands: List[Tuple[bool, Any]] = []
|
|
158
|
+
for k, md in self._locator._metadata.items():
|
|
159
|
+
typ = md.provided_type or md.concrete_class
|
|
160
|
+
if not isinstance(typ, type):
|
|
161
|
+
continue
|
|
162
|
+
try:
|
|
163
|
+
if typ is not key and issubclass(typ, key):
|
|
164
|
+
cands.append((md.primary, k))
|
|
165
|
+
except Exception:
|
|
166
|
+
continue
|
|
167
|
+
if cands:
|
|
168
|
+
prim = [k for is_p, k in cands if is_p]
|
|
169
|
+
return prim[0] if prim else cands[0][1]
|
|
170
|
+
if isinstance(key, str) and self._locator:
|
|
171
|
+
for k, md in self._locator._metadata.items():
|
|
172
|
+
if md.pico_name == key:
|
|
173
|
+
return k
|
|
174
|
+
return key
|
|
175
|
+
|
|
176
|
+
@overload
|
|
177
|
+
def get(self, key: type) -> Any: ...
|
|
178
|
+
@overload
|
|
179
|
+
def get(self, key: str) -> Any: ...
|
|
180
|
+
def get(self, key: KeyT) -> Any:
|
|
181
|
+
key = self._canonical_key(key)
|
|
182
|
+
cache = self._cache_for(key)
|
|
183
|
+
cached = cache.get(key)
|
|
184
|
+
if cached is not None:
|
|
185
|
+
self.context.cache_hit_count += 1
|
|
186
|
+
for o in self._observers: o.on_cache_hit(key)
|
|
187
|
+
return cached
|
|
188
|
+
import time as _tm
|
|
189
|
+
t0 = _tm.perf_counter()
|
|
190
|
+
chain = list(_resolve_chain.get())
|
|
191
|
+
for k in chain:
|
|
192
|
+
if k == key:
|
|
193
|
+
details = self._tracer.describe_cycle(tuple(chain), key, self._locator)
|
|
194
|
+
raise ComponentCreationError(key, CircularDependencyError(chain, key, details=details))
|
|
195
|
+
token_chain = _resolve_chain.set(tuple(chain + [key]))
|
|
196
|
+
token_container = self.activate()
|
|
197
|
+
token_tracer = self._tracer.enter(key, via="provider")
|
|
198
|
+
try:
|
|
199
|
+
provider = self._factory.get(key)
|
|
200
|
+
try:
|
|
201
|
+
instance = provider()
|
|
202
|
+
except ProviderNotFoundError as e:
|
|
203
|
+
raise
|
|
204
|
+
except Exception as e:
|
|
205
|
+
raise ComponentCreationError(key, e) from e
|
|
206
|
+
instance = self._maybe_wrap_with_aspects(key, instance)
|
|
207
|
+
cache.put(key, instance)
|
|
208
|
+
self.context.resolve_count += 1
|
|
209
|
+
took_ms = (_tm.perf_counter() - t0) * 1000
|
|
210
|
+
for o in self._observers: o.on_resolve(key, took_ms)
|
|
211
|
+
return instance
|
|
212
|
+
finally:
|
|
213
|
+
self._tracer.leave(token_tracer)
|
|
214
|
+
_resolve_chain.reset(token_chain)
|
|
215
|
+
self.deactivate(token_container)
|
|
216
|
+
|
|
217
|
+
async def aget(self, key: KeyT) -> Any:
|
|
218
|
+
key = self._canonical_key(key)
|
|
219
|
+
cache = self._cache_for(key)
|
|
220
|
+
cached = cache.get(key)
|
|
221
|
+
if cached is not None:
|
|
222
|
+
self.context.cache_hit_count += 1
|
|
223
|
+
for o in self._observers: o.on_cache_hit(key)
|
|
224
|
+
return cached
|
|
225
|
+
import time as _tm
|
|
226
|
+
t0 = _tm.perf_counter()
|
|
227
|
+
chain = list(_resolve_chain.get())
|
|
228
|
+
for k in chain:
|
|
229
|
+
if k == key:
|
|
230
|
+
details = self._tracer.describe_cycle(tuple(chain), key, self._locator)
|
|
231
|
+
raise ComponentCreationError(key, CircularDependencyError(chain, key, details=details))
|
|
232
|
+
token_chain = _resolve_chain.set(tuple(chain + [key]))
|
|
233
|
+
token_container = self.activate()
|
|
234
|
+
token_tracer = self._tracer.enter(key, via="provider")
|
|
235
|
+
try:
|
|
236
|
+
provider = self._factory.get(key)
|
|
237
|
+
try:
|
|
238
|
+
instance = provider()
|
|
239
|
+
if inspect.isawaitable(instance):
|
|
240
|
+
instance = await instance
|
|
241
|
+
except ProviderNotFoundError as e:
|
|
242
|
+
raise
|
|
243
|
+
except Exception as e:
|
|
244
|
+
raise ComponentCreationError(key, e) from e
|
|
245
|
+
instance = self._maybe_wrap_with_aspects(key, instance)
|
|
246
|
+
cache.put(key, instance)
|
|
247
|
+
self.context.resolve_count += 1
|
|
248
|
+
took_ms = (_tm.perf_counter() - t0) * 1000
|
|
249
|
+
for o in self._observers: o.on_resolve(key, took_ms)
|
|
250
|
+
return instance
|
|
251
|
+
finally:
|
|
252
|
+
self._tracer.leave(token_tracer)
|
|
253
|
+
_resolve_chain.reset(token_chain)
|
|
254
|
+
self.deactivate(token_container)
|
|
255
|
+
|
|
256
|
+
def _resolve_type_key(self, key: type):
|
|
257
|
+
if not self._locator:
|
|
258
|
+
return None
|
|
259
|
+
cands: List[Tuple[bool, Any]] = []
|
|
260
|
+
for k, md in self._locator._metadata.items():
|
|
261
|
+
typ = md.provided_type or md.concrete_class
|
|
262
|
+
if not isinstance(typ, type):
|
|
113
263
|
continue
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
matches.append(inst)
|
|
119
|
-
return matches
|
|
120
|
-
|
|
121
|
-
def get_providers(self) -> Dict[Any, Dict]:
|
|
122
|
-
return self._providers.copy()
|
|
123
|
-
|
|
124
|
-
def _is_protocol(t) -> bool:
|
|
125
|
-
return getattr(t, "_is_protocol", False) is True
|
|
126
|
-
|
|
127
|
-
def _is_compatible(cls, base) -> bool:
|
|
128
|
-
try:
|
|
129
|
-
if isinstance(base, type) and issubclass(cls, base):
|
|
130
|
-
return True
|
|
131
|
-
except TypeError:
|
|
132
|
-
pass
|
|
133
|
-
if _is_protocol(base):
|
|
134
|
-
names = set(getattr(base, "__annotations__", {}).keys())
|
|
135
|
-
names.update(n for n in getattr(base, "__dict__", {}).keys() if not n.startswith("_"))
|
|
136
|
-
for n in names:
|
|
137
|
-
if n.startswith("__") and n.endswith("__"):
|
|
264
|
+
try:
|
|
265
|
+
if typ is not key and issubclass(typ, key):
|
|
266
|
+
cands.append((md.primary, k))
|
|
267
|
+
except Exception:
|
|
138
268
|
continue
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
269
|
+
if not cands:
|
|
270
|
+
return None
|
|
271
|
+
prim = [k for is_p, k in cands if is_p]
|
|
272
|
+
return prim[0] if prim else cands[0][1]
|
|
273
|
+
|
|
274
|
+
def _maybe_wrap_with_aspects(self, key, instance: Any) -> Any:
|
|
275
|
+
if isinstance(instance, UnifiedComponentProxy):
|
|
276
|
+
return instance
|
|
277
|
+
cls = type(instance)
|
|
278
|
+
for _, fn in inspect.getmembers(cls, predicate=lambda m: inspect.isfunction(m) or inspect.ismethod(m) or inspect.iscoroutinefunction(m)):
|
|
279
|
+
if getattr(fn, "_pico_interceptors_", None):
|
|
280
|
+
return UnifiedComponentProxy(container=self, target=instance)
|
|
281
|
+
return instance
|
|
282
|
+
|
|
283
|
+
def cleanup_all(self) -> None:
|
|
284
|
+
for _, obj in self._caches.all_items():
|
|
285
|
+
for _, m in inspect.getmembers(obj, predicate=inspect.ismethod):
|
|
286
|
+
meta = getattr(m, PICO_META, {})
|
|
287
|
+
if meta.get("cleanup", False):
|
|
288
|
+
from .api import _resolve_args
|
|
289
|
+
kwargs = _resolve_args(m, self)
|
|
290
|
+
m(**kwargs)
|
|
291
|
+
if self._locator:
|
|
292
|
+
seen = set()
|
|
293
|
+
for md in self._locator._metadata.values():
|
|
294
|
+
fc = md.factory_class
|
|
295
|
+
if fc and fc not in seen:
|
|
296
|
+
seen.add(fc)
|
|
297
|
+
inst = self.get(fc) if self._factory.has(fc) else fc()
|
|
298
|
+
for _, m in inspect.getmembers(inst, predicate=inspect.ismethod):
|
|
299
|
+
meta = getattr(m, PICO_META, {})
|
|
300
|
+
if meta.get("cleanup", False):
|
|
301
|
+
from .api import _resolve_args
|
|
302
|
+
kwargs = _resolve_args(m, self)
|
|
303
|
+
m(**kwargs)
|
|
304
|
+
|
|
305
|
+
def activate_scope(self, name: str, scope_id: Any):
|
|
306
|
+
return self.scopes.activate(name, scope_id)
|
|
307
|
+
|
|
308
|
+
def deactivate_scope(self, name: str, token: Optional[contextvars.Token]) -> None:
|
|
309
|
+
self.scopes.deactivate(name, token)
|
|
310
|
+
|
|
311
|
+
def info(self, msg: str) -> None:
|
|
312
|
+
LOGGER.info(f"[{self.container_id[:8]}] {msg}")
|
|
313
|
+
|
|
314
|
+
@contextmanager
|
|
315
|
+
def scope(self, name: str, scope_id: Any):
|
|
316
|
+
tok = self.activate_scope(name, scope_id)
|
|
317
|
+
try:
|
|
318
|
+
yield self
|
|
319
|
+
finally:
|
|
320
|
+
self.deactivate_scope(name, tok)
|
|
321
|
+
|
|
322
|
+
def health_check(self) -> Dict[str, bool]:
|
|
323
|
+
out: Dict[str, bool] = {}
|
|
324
|
+
for k, obj in self._caches.all_items():
|
|
325
|
+
for name, m in inspect.getmembers(obj, predicate=callable):
|
|
326
|
+
if getattr(m, PICO_META, {}).get("health_check", False):
|
|
327
|
+
try:
|
|
328
|
+
out[f"{getattr(k,'__name__',k)}.{name}"] = bool(m())
|
|
329
|
+
except Exception:
|
|
330
|
+
out[f"{getattr(k,'__name__',k)}.{name}"] = False
|
|
331
|
+
return out
|
|
332
|
+
|
|
333
|
+
async def cleanup_all_async(self) -> None:
|
|
334
|
+
for _, obj in self._caches.all_items():
|
|
335
|
+
for _, m in inspect.getmembers(obj, predicate=inspect.ismethod):
|
|
336
|
+
meta = getattr(m, PICO_META, {})
|
|
337
|
+
if meta.get("cleanup", False):
|
|
338
|
+
from .api import _resolve_args
|
|
339
|
+
res = m(**_resolve_args(m, self))
|
|
340
|
+
import inspect as _i
|
|
341
|
+
if _i.isawaitable(res):
|
|
342
|
+
await res
|
|
343
|
+
if self._locator:
|
|
344
|
+
seen = set()
|
|
345
|
+
for md in self._locator._metadata.values():
|
|
346
|
+
fc = md.factory_class
|
|
347
|
+
if fc and fc not in seen:
|
|
348
|
+
seen.add(fc)
|
|
349
|
+
inst = self.get(fc) if self._factory.has(fc) else fc()
|
|
350
|
+
for _, m in inspect.getmembers(inst, predicate=inspect.ismethod):
|
|
351
|
+
meta = getattr(m, PICO_META, {})
|
|
352
|
+
if meta.get("cleanup", False):
|
|
353
|
+
from .api import _resolve_args
|
|
354
|
+
res = m(**_resolve_args(m, self))
|
|
355
|
+
import inspect as _i
|
|
356
|
+
if _i.isawaitable(res):
|
|
357
|
+
await res
|
|
358
|
+
try:
|
|
359
|
+
from .event_bus import EventBus
|
|
360
|
+
for _, obj in self._caches.all_items():
|
|
361
|
+
if isinstance(obj, EventBus):
|
|
362
|
+
await obj.aclose()
|
|
363
|
+
except Exception:
|
|
364
|
+
pass
|
|
365
|
+
|
|
366
|
+
def stats(self) -> Dict[str, Any]:
|
|
367
|
+
import time as _t
|
|
368
|
+
resolves = self.context.resolve_count
|
|
369
|
+
hits = self.context.cache_hit_count
|
|
370
|
+
total = resolves + hits
|
|
371
|
+
return {
|
|
372
|
+
"container_id": self.container_id,
|
|
373
|
+
"profiles": self.context.profiles,
|
|
374
|
+
"uptime_seconds": _t.time() - self.context.created_at,
|
|
375
|
+
"total_resolves": resolves,
|
|
376
|
+
"cache_hits": hits,
|
|
377
|
+
"cache_hit_rate": (hits / total) if total > 0 else 0.0,
|
|
378
|
+
"registered_components": len(self._locator._metadata) if self._locator else 0,
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
def shutdown(self) -> None:
|
|
382
|
+
self.cleanup_all()
|
|
383
|
+
PicoContainer._container_registry.pop(self.container_id, None)
|
|
384
|
+
|
|
385
|
+
def export_graph(
|
|
386
|
+
self,
|
|
387
|
+
path: str,
|
|
388
|
+
*,
|
|
389
|
+
include_scopes: bool = True,
|
|
390
|
+
include_qualifiers: bool = False,
|
|
391
|
+
rankdir: str = "LR",
|
|
392
|
+
title: Optional[str] = None,
|
|
393
|
+
) -> None:
|
|
394
|
+
|
|
395
|
+
if not self._locator:
|
|
396
|
+
raise RuntimeError("No locator attached; cannot export dependency graph.")
|
|
397
|
+
|
|
398
|
+
from .api import _build_resolution_graph
|
|
399
|
+
|
|
400
|
+
md_by_key = self._locator._metadata
|
|
401
|
+
graph = _build_resolution_graph(self)
|
|
402
|
+
|
|
403
|
+
lines: List[str] = []
|
|
404
|
+
lines.append("digraph Pico {")
|
|
405
|
+
lines.append(f' rankdir="{rankdir}";')
|
|
406
|
+
lines.append(" node [shape=box, fontsize=10];")
|
|
407
|
+
if title:
|
|
408
|
+
lines.append(f' labelloc="t";')
|
|
409
|
+
lines.append(f' label="{title}";')
|
|
410
|
+
|
|
411
|
+
def _node_id(k: KeyT) -> str:
|
|
412
|
+
return f'n_{abs(hash(k))}'
|
|
413
|
+
|
|
414
|
+
def _node_label(k: KeyT) -> str:
|
|
415
|
+
name = getattr(k, "__name__", str(k))
|
|
416
|
+
md = md_by_key.get(k)
|
|
417
|
+
parts = [name]
|
|
418
|
+
if md is not None and include_scopes:
|
|
419
|
+
parts.append(f"[scope={md.scope}]")
|
|
420
|
+
if md is not None and include_qualifiers and md.qualifiers:
|
|
421
|
+
q = ",".join(sorted(md.qualifiers))
|
|
422
|
+
parts.append(f"\\n⟨{q}⟩")
|
|
423
|
+
return "\\n".join(parts)
|
|
424
|
+
|
|
425
|
+
for key in md_by_key.keys():
|
|
426
|
+
nid = _node_id(key)
|
|
427
|
+
label = _node_label(key)
|
|
428
|
+
lines.append(f' {nid} [label="{label}"];')
|
|
429
|
+
|
|
430
|
+
for parent, deps in graph.items():
|
|
431
|
+
pid = _node_id(parent)
|
|
432
|
+
for child in deps:
|
|
433
|
+
cid = _node_id(child)
|
|
434
|
+
lines.append(f" {pid} -> {cid};")
|
|
435
|
+
|
|
436
|
+
lines.append("}")
|
|
437
|
+
|
|
438
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
439
|
+
f.write("\n".join(lines))
|
|
168
440
|
|