pico-ioc 0.5.2__py3-none-any.whl → 1.0.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 +14 -391
- pico_ioc/_state.py +10 -0
- pico_ioc/_version.py +1 -1
- pico_ioc/api.py +128 -0
- pico_ioc/container.py +155 -0
- pico_ioc/decorators.py +76 -0
- pico_ioc/plugins.py +12 -0
- pico_ioc/proxy.py +77 -0
- pico_ioc/public_api.py +76 -0
- pico_ioc/resolver.py +110 -0
- pico_ioc/scanner.py +238 -0
- pico_ioc/typing_utils.py +29 -0
- pico_ioc-1.0.0.dist-info/METADATA +145 -0
- pico_ioc-1.0.0.dist-info/RECORD +17 -0
- pico_ioc-0.5.2.dist-info/METADATA +0 -278
- pico_ioc-0.5.2.dist-info/RECORD +0 -7
- {pico_ioc-0.5.2.dist-info → pico_ioc-1.0.0.dist-info}/WHEEL +0 -0
- {pico_ioc-0.5.2.dist-info → pico_ioc-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {pico_ioc-0.5.2.dist-info → pico_ioc-1.0.0.dist-info}/top_level.txt +0 -0
pico_ioc/__init__.py
CHANGED
|
@@ -1,408 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
import importlib
|
|
3
|
-
import inspect
|
|
4
|
-
import logging
|
|
5
|
-
import pkgutil
|
|
6
|
-
from contextvars import ContextVar
|
|
7
|
-
from typing import Any, Callable, Dict, Optional, Protocol, Tuple, runtime_checkable
|
|
1
|
+
# pico_ioc/__init__.py
|
|
8
2
|
|
|
9
3
|
try:
|
|
10
4
|
from ._version import __version__
|
|
11
5
|
except Exception:
|
|
12
6
|
__version__ = "0.0.0"
|
|
13
7
|
|
|
8
|
+
from .container import PicoContainer, Binder
|
|
9
|
+
from .decorators import component, factory_component, provides, plugin, Qualifier, qualifier
|
|
10
|
+
from .plugins import PicoPlugin
|
|
11
|
+
from .resolver import Resolver
|
|
12
|
+
from .api import init, reset
|
|
13
|
+
from .proxy import ComponentProxy
|
|
14
|
+
|
|
14
15
|
__all__ = [
|
|
15
16
|
"__version__",
|
|
16
17
|
"PicoContainer",
|
|
17
18
|
"Binder",
|
|
18
19
|
"PicoPlugin",
|
|
20
|
+
"ComponentProxy",
|
|
19
21
|
"init",
|
|
22
|
+
"reset",
|
|
20
23
|
"component",
|
|
21
24
|
"factory_component",
|
|
22
25
|
"provides",
|
|
23
|
-
"
|
|
24
|
-
"
|
|
26
|
+
"plugin",
|
|
27
|
+
"Qualifier",
|
|
28
|
+
"qualifier",
|
|
29
|
+
"Resolver",
|
|
25
30
|
]
|
|
26
31
|
|
|
27
|
-
_scanning: ContextVar[bool] = ContextVar("pico_scanning", default=False)
|
|
28
|
-
_resolving: ContextVar[bool] = ContextVar("pico_resolving", default=False)
|
|
29
|
-
|
|
30
|
-
class PicoContainer:
|
|
31
|
-
def __init__(self):
|
|
32
|
-
self._providers: Dict[Any, Dict[str, Any]] = {}
|
|
33
|
-
self._singletons: Dict[Any, Any] = {}
|
|
34
|
-
|
|
35
|
-
def bind(self, key: Any, provider: Callable[[], Any], *, lazy: bool):
|
|
36
|
-
self._providers[key] = {"factory": provider, "lazy": bool(lazy)}
|
|
37
|
-
|
|
38
|
-
def has(self, key: Any) -> bool:
|
|
39
|
-
return key in self._providers or key in self._singletons
|
|
40
|
-
|
|
41
|
-
def get(self, key: Any) -> Any:
|
|
42
|
-
if _scanning.get() and not _resolving.get():
|
|
43
|
-
raise RuntimeError("pico-ioc: re-entrant container access during scan.")
|
|
44
|
-
if key in self._singletons:
|
|
45
|
-
return self._singletons[key]
|
|
46
|
-
prov = self._providers.get(key)
|
|
47
|
-
if prov is None:
|
|
48
|
-
raise NameError(f"No provider found for key: {key}")
|
|
49
|
-
instance = prov["factory"]()
|
|
50
|
-
self._singletons[key] = instance
|
|
51
|
-
return instance
|
|
52
|
-
|
|
53
|
-
def _eager_instantiate_all(self):
|
|
54
|
-
for key, meta in list(self._providers.items()):
|
|
55
|
-
if not meta.get("lazy", False) and key not in self._singletons:
|
|
56
|
-
self.get(key)
|
|
57
|
-
|
|
58
|
-
class Binder:
|
|
59
|
-
def __init__(self, container: PicoContainer):
|
|
60
|
-
self._c = container
|
|
61
|
-
|
|
62
|
-
def bind(self, key: Any, provider: Callable[[], Any], *, lazy: bool = False):
|
|
63
|
-
self._c.bind(key, provider, lazy=lazy)
|
|
64
|
-
|
|
65
|
-
def has(self, key: Any) -> bool:
|
|
66
|
-
return self._c.has(key)
|
|
67
|
-
|
|
68
|
-
def get(self, key: Any) -> Any:
|
|
69
|
-
return self._c.get(key)
|
|
70
|
-
|
|
71
|
-
def factory_component(cls):
|
|
72
|
-
setattr(cls, '_is_factory_component', True)
|
|
73
|
-
return cls
|
|
74
|
-
|
|
75
|
-
def provides(key: Any, *, lazy: bool = False):
|
|
76
|
-
def decorator(func):
|
|
77
|
-
@functools.wraps(func)
|
|
78
|
-
def wrapper(*args, **kwargs):
|
|
79
|
-
return func(*args, **kwargs)
|
|
80
|
-
setattr(wrapper, '_provides_name', key)
|
|
81
|
-
setattr(wrapper, '_pico_lazy', bool(lazy))
|
|
82
|
-
return wrapper
|
|
83
|
-
return decorator
|
|
84
|
-
|
|
85
|
-
def component(cls=None, *, name: Any = None, lazy: bool = False):
|
|
86
|
-
def decorator(c):
|
|
87
|
-
setattr(c, '_is_component', True)
|
|
88
|
-
setattr(c, '_component_key', name if name is not None else c)
|
|
89
|
-
setattr(c, '_component_lazy', bool(lazy))
|
|
90
|
-
return c
|
|
91
|
-
return decorator(cls) if cls else decorator
|
|
92
|
-
|
|
93
|
-
class ComponentProxy:
|
|
94
|
-
def __init__(self, object_creator: Callable[[], Any]):
|
|
95
|
-
object.__setattr__(self, "_object_creator", object_creator)
|
|
96
|
-
object.__setattr__(self, "__real_object", None)
|
|
97
|
-
|
|
98
|
-
def _get_real_object(self) -> Any:
|
|
99
|
-
real_obj = object.__getattribute__(self, "__real_object")
|
|
100
|
-
if real_obj is None:
|
|
101
|
-
real_obj = object.__getattribute__(self, "_object_creator")()
|
|
102
|
-
object.__setattr__(self, "__real_object", real_obj)
|
|
103
|
-
return real_obj
|
|
104
|
-
|
|
105
|
-
@property
|
|
106
|
-
def __class__(self):
|
|
107
|
-
return self._get_real_object().__class__
|
|
108
|
-
|
|
109
|
-
def __getattr__(self, name): return getattr(self._get_real_object(), name)
|
|
110
|
-
def __setattr__(self, name, value): setattr(self._get_real_object(), name, value)
|
|
111
|
-
def __delattr__(self, name): delattr(self._get_real_object(), name)
|
|
112
|
-
def __str__(self): return str(self._get_real_object())
|
|
113
|
-
def __repr__(self): return repr(self._get_real_object())
|
|
114
|
-
def __dir__(self): return dir(self._get_real_object())
|
|
115
|
-
def __len__(self): return len(self._get_real_object())
|
|
116
|
-
def __getitem__(self, key): return self._get_real_object()[key]
|
|
117
|
-
def __setitem__(self, key, value): self._get_real_object()[key] = value
|
|
118
|
-
def __delitem__(self, key): del self._get_real_object()[key]
|
|
119
|
-
def __iter__(self): return iter(self._get_real_object())
|
|
120
|
-
def __reversed__(self): return reversed(self._get_real_object())
|
|
121
|
-
def __contains__(self, item): return item in self._get_real_object()
|
|
122
|
-
def __add__(self, other): return self._get_real_object() + other
|
|
123
|
-
def __sub__(self, other): return self._get_real_object() - other
|
|
124
|
-
def __mul__(self, other): return self._get_real_object() * other
|
|
125
|
-
def __matmul__(self, other): return self._get_real_object() @ other
|
|
126
|
-
def __truediv__(self, other): return self._get_real_object() / other
|
|
127
|
-
def __floordiv__(self, other): return self._get_real_object() // other
|
|
128
|
-
def __mod__(self, other): return self._get_real_object() % other
|
|
129
|
-
def __divmod__(self, other): return divmod(self._get_real_object(), other)
|
|
130
|
-
def __pow__(self, other, modulo=None): return pow(self._get_real_object(), other, modulo)
|
|
131
|
-
def __lshift__(self, other): return self._get_real_object() << other
|
|
132
|
-
def __rshift__(self, other): return self._get_real_object() >> other
|
|
133
|
-
def __and__(self, other): return self._get_real_object() & other
|
|
134
|
-
def __xor__(self, other): return self._get_real_object() ^ other
|
|
135
|
-
def __or__(self, other): return self._get_real_object() | other
|
|
136
|
-
def __radd__(self, other): return other + self._get_real_object()
|
|
137
|
-
def __rsub__(self, other): return other - self._get_real_object()
|
|
138
|
-
def __rmul__(self, other): return other * self._get_real_object()
|
|
139
|
-
def __rmatmul__(self, other): return other @ self._get_real_object()
|
|
140
|
-
def __rtruediv__(self, other): return other / self._get_real_object()
|
|
141
|
-
def __rfloordiv__(self, other): return other // self._get_real_object()
|
|
142
|
-
def __rmod__(self, other): return other % self._get_real_object()
|
|
143
|
-
def __rdivmod__(self, other): return divmod(other, self._get_real_object())
|
|
144
|
-
def __rpow__(self, other): return pow(other, self._get_real_object())
|
|
145
|
-
def __rlshift__(self, other): return other << self._get_real_object()
|
|
146
|
-
def __rrshift__(self, other): return other >> self._get_real_object()
|
|
147
|
-
def __rand__(self, other): return other & self._get_real_object()
|
|
148
|
-
def __rxor__(self, other): return other ^ self._get_real_object()
|
|
149
|
-
def __ror__(self, other): return other | self._get_real_object()
|
|
150
|
-
def __neg__(self): return -self._get_real_object()
|
|
151
|
-
def __pos__(self): return +self._get_real_object()
|
|
152
|
-
def __abs__(self): return abs(self._get_real_object())
|
|
153
|
-
def __invert__(self): return ~self._get_real_object()
|
|
154
|
-
def __eq__(self, other): return self._get_real_object() == other
|
|
155
|
-
def __ne__(self, other): return self._get_real_object() != other
|
|
156
|
-
def __lt__(self, other): return self._get_real_object() < other
|
|
157
|
-
def __le__(self, other): return self._get_real_object() <= other
|
|
158
|
-
def __gt__(self, other): return self._get_real_object() > other
|
|
159
|
-
def __ge__(self, other): return self._get_real_object() >= other
|
|
160
|
-
def __hash__(self): return hash(self._get_real_object())
|
|
161
|
-
def __bool__(self): return bool(self._get_real_object())
|
|
162
|
-
def __call__(self, *args, **kwargs): return self._get_real_object()(*args, **kwargs)
|
|
163
|
-
def __enter__(self): return self._get_real_object().__enter__()
|
|
164
|
-
def __exit__(self, exc_type, exc_val, exc_tb): return self._get_real_object().__exit__(exc_type, exc_val, exc_tb)
|
|
165
|
-
|
|
166
|
-
def resolve_param(container: PicoContainer, p: inspect.Parameter) -> Any:
|
|
167
|
-
if p.name == 'self' or p.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
168
|
-
raise RuntimeError("Invalid param for resolution")
|
|
169
|
-
if container.has(p.name):
|
|
170
|
-
return container.get(p.name)
|
|
171
|
-
ann = p.annotation
|
|
172
|
-
if ann is not inspect._empty and container.has(ann):
|
|
173
|
-
return container.get(ann)
|
|
174
|
-
if ann is not inspect._empty:
|
|
175
|
-
try:
|
|
176
|
-
for base in getattr(ann, "__mro__", ())[1:]:
|
|
177
|
-
if base is object:
|
|
178
|
-
break
|
|
179
|
-
if container.has(base):
|
|
180
|
-
return container.get(base)
|
|
181
|
-
except Exception:
|
|
182
|
-
pass
|
|
183
|
-
if container.has(str(p.name)):
|
|
184
|
-
return container.get(str(p.name))
|
|
185
|
-
key = p.name if ann is inspect._empty else ann
|
|
186
|
-
return container.get(key)
|
|
187
|
-
|
|
188
|
-
def create_instance(cls: type, container: PicoContainer) -> Any:
|
|
189
|
-
"""
|
|
190
|
-
Instantiate `cls` with constructor DI from `container`.
|
|
191
|
-
|
|
192
|
-
Resolution rules:
|
|
193
|
-
- Skip `self`, *args, **kwargs.
|
|
194
|
-
- Try resolve_param(...) for each parameter.
|
|
195
|
-
- If resolve_param raises NameError AND the parameter has a default,
|
|
196
|
-
skip injecting it so Python uses the default value.
|
|
197
|
-
- Otherwise, propagate the error.
|
|
198
|
-
|
|
199
|
-
This preserves the "name > type > MRO > str(name)" precedence in resolve_param,
|
|
200
|
-
while making defaulted, annotated parameters truly optional.
|
|
201
|
-
"""
|
|
202
|
-
sig = inspect.signature(cls.__init__)
|
|
203
|
-
deps: Dict[str, Any] = {}
|
|
204
|
-
|
|
205
|
-
tok = _resolving.set(True)
|
|
206
|
-
try:
|
|
207
|
-
for p in sig.parameters.values():
|
|
208
|
-
if p.name == 'self' or p.kind in (
|
|
209
|
-
inspect.Parameter.VAR_POSITIONAL,
|
|
210
|
-
inspect.Parameter.VAR_KEYWORD,
|
|
211
|
-
):
|
|
212
|
-
continue
|
|
213
|
-
|
|
214
|
-
try:
|
|
215
|
-
deps[p.name] = resolve_param(container, p)
|
|
216
|
-
except NameError:
|
|
217
|
-
if p.default is not inspect._empty:
|
|
218
|
-
continue
|
|
219
|
-
raise
|
|
220
|
-
finally:
|
|
221
|
-
_resolving.reset(tok)
|
|
222
|
-
|
|
223
|
-
return cls(**deps)
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
@runtime_checkable
|
|
227
|
-
class PicoPlugin(Protocol):
|
|
228
|
-
def before_scan(self, package: Any, binder: Binder) -> None: ...
|
|
229
|
-
def visit_class(self, module: Any, cls: type, binder: Binder) -> None: ...
|
|
230
|
-
def after_scan(self, package: Any, binder: Binder) -> None: ...
|
|
231
|
-
def after_bind(self, container: PicoContainer, binder: Binder) -> None: ...
|
|
232
|
-
def before_eager(self, container: PicoContainer, binder: Binder) -> None: ...
|
|
233
|
-
def after_ready(self, container: PicoContainer, binder: Binder) -> None: ...
|
|
234
|
-
|
|
235
|
-
def _scan_and_configure(
|
|
236
|
-
package_or_name,
|
|
237
|
-
container: PicoContainer,
|
|
238
|
-
*,
|
|
239
|
-
exclude: Optional[Callable[[str], bool]] = None,
|
|
240
|
-
plugins: Tuple[PicoPlugin, ...] = (),
|
|
241
|
-
):
|
|
242
|
-
package = importlib.import_module(package_or_name) if isinstance(package_or_name, str) else package_or_name
|
|
243
|
-
logging.info(f"Scanning in '{package.__name__}'...")
|
|
244
|
-
binder = Binder(container)
|
|
245
|
-
|
|
246
|
-
for pl in plugins:
|
|
247
|
-
try:
|
|
248
|
-
if hasattr(pl, "before_scan"):
|
|
249
|
-
pl.before_scan(package, binder)
|
|
250
|
-
except Exception:
|
|
251
|
-
logging.exception("Plugin before_scan failed")
|
|
252
|
-
|
|
253
|
-
component_classes, factory_classes = [], []
|
|
254
|
-
for _, name, _ in pkgutil.walk_packages(package.__path__, package.__name__ + '.'):
|
|
255
|
-
if exclude and exclude(name):
|
|
256
|
-
logging.info(f"Skipping module {name} (excluded)")
|
|
257
|
-
continue
|
|
258
|
-
try:
|
|
259
|
-
module = importlib.import_module(name)
|
|
260
|
-
for _, obj in inspect.getmembers(module, inspect.isclass):
|
|
261
|
-
for pl in plugins:
|
|
262
|
-
try:
|
|
263
|
-
if hasattr(pl, "visit_class"):
|
|
264
|
-
pl.visit_class(module, obj, binder)
|
|
265
|
-
except Exception:
|
|
266
|
-
logging.exception("Plugin visit_class failed")
|
|
267
|
-
if getattr(obj, '_is_component', False):
|
|
268
|
-
component_classes.append(obj)
|
|
269
|
-
elif getattr(obj, '_is_factory_component', False):
|
|
270
|
-
factory_classes.append(obj)
|
|
271
|
-
except Exception as e:
|
|
272
|
-
logging.warning(f"Module {name} not processed: {e}")
|
|
273
|
-
|
|
274
|
-
for pl in plugins:
|
|
275
|
-
try:
|
|
276
|
-
if hasattr(pl, "after_scan"):
|
|
277
|
-
pl.after_scan(package, binder)
|
|
278
|
-
except Exception:
|
|
279
|
-
logging.exception("Plugin after_scan failed")
|
|
280
|
-
|
|
281
|
-
for component_cls in component_classes:
|
|
282
|
-
key = getattr(component_cls, '_component_key', component_cls)
|
|
283
|
-
is_lazy = bool(getattr(component_cls, '_component_lazy', False))
|
|
284
|
-
|
|
285
|
-
def provider_factory(lazy=is_lazy, cls=component_cls):
|
|
286
|
-
def _factory():
|
|
287
|
-
if lazy:
|
|
288
|
-
return ComponentProxy(lambda: create_instance(cls, container))
|
|
289
|
-
return create_instance(cls, container)
|
|
290
|
-
return _factory
|
|
291
|
-
|
|
292
|
-
container.bind(key, provider_factory(), lazy=is_lazy)
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
def _resolve_method_kwargs(meth) -> Dict[str, Any]:
|
|
296
|
-
sig = inspect.signature(meth)
|
|
297
|
-
deps = {}
|
|
298
|
-
tok = _resolving.set(True)
|
|
299
|
-
try:
|
|
300
|
-
for p in sig.parameters.values():
|
|
301
|
-
if p.name == 'self' or p.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
302
|
-
continue
|
|
303
|
-
try:
|
|
304
|
-
deps[p.name] = resolve_param(container, p)
|
|
305
|
-
except NameError:
|
|
306
|
-
if p.default is not inspect._empty:
|
|
307
|
-
continue
|
|
308
|
-
raise
|
|
309
|
-
finally:
|
|
310
|
-
_resolving.reset(tok)
|
|
311
|
-
return deps
|
|
312
|
-
|
|
313
|
-
for factory_cls in factory_classes:
|
|
314
|
-
try:
|
|
315
|
-
factory_instance = create_instance(factory_cls, container)
|
|
316
|
-
|
|
317
|
-
for name, func in inspect.getmembers(factory_cls, predicate=inspect.isfunction):
|
|
318
|
-
provides_key = getattr(func, '_provides_name', None)
|
|
319
|
-
if provides_key is None:
|
|
320
|
-
continue
|
|
321
|
-
|
|
322
|
-
is_lazy = bool(getattr(func, '_pico_lazy', False))
|
|
323
|
-
|
|
324
|
-
bound_method = getattr(factory_instance, name, None)
|
|
325
|
-
if bound_method is None:
|
|
326
|
-
bound_method = func.__get__(factory_instance, factory_cls)
|
|
327
|
-
|
|
328
|
-
fn_meta_src = getattr(bound_method, '__func__', bound_method)
|
|
329
|
-
if getattr(fn_meta_src, '_provides_name', None) is not None:
|
|
330
|
-
provides_key = getattr(fn_meta_src, '_provides_name')
|
|
331
|
-
is_lazy = bool(getattr(fn_meta_src, '_pico_lazy', is_lazy))
|
|
332
|
-
|
|
333
|
-
def make_provider(m=bound_method, lazy=is_lazy):
|
|
334
|
-
def _factory():
|
|
335
|
-
kwargs = _resolve_method_kwargs(m)
|
|
336
|
-
|
|
337
|
-
def _call():
|
|
338
|
-
return m(**kwargs)
|
|
339
|
-
|
|
340
|
-
if lazy:
|
|
341
|
-
return ComponentProxy(lambda: _call())
|
|
342
|
-
return _call()
|
|
343
|
-
return _factory
|
|
344
|
-
|
|
345
|
-
container.bind(provides_key, make_provider(), lazy=is_lazy)
|
|
346
|
-
except Exception:
|
|
347
|
-
logging.exception(f"Error in factory {factory_cls.__name__}")
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
_container: Optional[PicoContainer] = None
|
|
352
|
-
|
|
353
|
-
def init(
|
|
354
|
-
root_package,
|
|
355
|
-
*,
|
|
356
|
-
exclude: Optional[Callable[[str], bool]] = None,
|
|
357
|
-
auto_exclude_caller: bool = True,
|
|
358
|
-
plugins: Tuple[PicoPlugin, ...] = (),
|
|
359
|
-
):
|
|
360
|
-
global _container
|
|
361
|
-
if _container:
|
|
362
|
-
return _container
|
|
363
|
-
combined_exclude = exclude
|
|
364
|
-
if auto_exclude_caller:
|
|
365
|
-
try:
|
|
366
|
-
caller_frame = inspect.stack()[1].frame
|
|
367
|
-
caller_module = inspect.getmodule(caller_frame)
|
|
368
|
-
caller_name = getattr(caller_module, "__name__", None)
|
|
369
|
-
except Exception:
|
|
370
|
-
caller_name = None
|
|
371
|
-
if caller_name:
|
|
372
|
-
if combined_exclude is None:
|
|
373
|
-
def combined_exclude(mod: str, _caller=caller_name):
|
|
374
|
-
return mod == _caller
|
|
375
|
-
else:
|
|
376
|
-
prev = combined_exclude
|
|
377
|
-
def combined_exclude(mod: str, _caller=caller_name, _prev=prev):
|
|
378
|
-
return mod == _caller or _prev(mod)
|
|
379
|
-
_container = PicoContainer()
|
|
380
|
-
binder = Binder(_container)
|
|
381
|
-
logging.info("Initializing pico-ioc...")
|
|
382
|
-
tok = _scanning.set(True)
|
|
383
|
-
try:
|
|
384
|
-
_scan_and_configure(root_package, _container, exclude=combined_exclude, plugins=plugins)
|
|
385
|
-
finally:
|
|
386
|
-
_scanning.reset(tok)
|
|
387
|
-
for pl in plugins:
|
|
388
|
-
try:
|
|
389
|
-
if hasattr(pl, "after_bind"):
|
|
390
|
-
pl.after_bind(_container, binder)
|
|
391
|
-
except Exception:
|
|
392
|
-
logging.exception("Plugin after_bind failed")
|
|
393
|
-
for pl in plugins:
|
|
394
|
-
try:
|
|
395
|
-
if hasattr(pl, "before_eager"):
|
|
396
|
-
pl.before_eager(_container, binder)
|
|
397
|
-
except Exception:
|
|
398
|
-
logging.exception("Plugin before_eager failed")
|
|
399
|
-
_container._eager_instantiate_all()
|
|
400
|
-
for pl in plugins:
|
|
401
|
-
try:
|
|
402
|
-
if hasattr(pl, "after_ready"):
|
|
403
|
-
pl.after_ready(_container, binder)
|
|
404
|
-
except Exception:
|
|
405
|
-
logging.exception("Plugin after_ready failed")
|
|
406
|
-
logging.info("Container configured and ready.")
|
|
407
|
-
return _container
|
|
408
|
-
|
pico_ioc/_state.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# pico_ioc/_state.py
|
|
2
|
+
from contextvars import ContextVar
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
_scanning: ContextVar[bool] = ContextVar("pico_scanning", default=False)
|
|
6
|
+
_resolving: ContextVar[bool] = ContextVar("pico_resolving", default=False)
|
|
7
|
+
|
|
8
|
+
_container = None
|
|
9
|
+
_root_name: Optional[str] = None
|
|
10
|
+
|
pico_ioc/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = '0.
|
|
1
|
+
__version__ = '1.0.0'
|
pico_ioc/api.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# pico_ioc/api.py
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import logging
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from typing import Callable, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
from .container import PicoContainer, Binder
|
|
9
|
+
from .plugins import PicoPlugin
|
|
10
|
+
from .scanner import scan_and_configure
|
|
11
|
+
from . import _state
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def reset() -> None:
|
|
15
|
+
"""Reset the global container."""
|
|
16
|
+
_state._container = None
|
|
17
|
+
_state._root_name = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def init(
|
|
21
|
+
root_package,
|
|
22
|
+
*,
|
|
23
|
+
exclude: Optional[Callable[[str], bool]] = None,
|
|
24
|
+
auto_exclude_caller: bool = True,
|
|
25
|
+
plugins: Tuple[PicoPlugin, ...] = (),
|
|
26
|
+
reuse: bool = True,
|
|
27
|
+
) -> PicoContainer:
|
|
28
|
+
"""
|
|
29
|
+
Initialize and configure a PicoContainer by scanning a root package.
|
|
30
|
+
"""
|
|
31
|
+
root_name = root_package if isinstance(root_package, str) else getattr(root_package, "__name__", None)
|
|
32
|
+
|
|
33
|
+
# Reuse only if the existing container was built for the same root
|
|
34
|
+
if reuse and _state._container and _state._root_name == root_name:
|
|
35
|
+
return _state._container
|
|
36
|
+
|
|
37
|
+
combined_exclude = _build_exclude(exclude, auto_exclude_caller, root_name=root_name)
|
|
38
|
+
|
|
39
|
+
container = PicoContainer()
|
|
40
|
+
binder = Binder(container)
|
|
41
|
+
logging.info("Initializing pico-ioc...")
|
|
42
|
+
|
|
43
|
+
with _scanning_flag():
|
|
44
|
+
scan_and_configure(
|
|
45
|
+
root_package,
|
|
46
|
+
container,
|
|
47
|
+
exclude=combined_exclude,
|
|
48
|
+
plugins=plugins,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
_run_hooks(plugins, "after_bind", container, binder)
|
|
52
|
+
_run_hooks(plugins, "before_eager", container, binder)
|
|
53
|
+
|
|
54
|
+
container.eager_instantiate_all()
|
|
55
|
+
|
|
56
|
+
_run_hooks(plugins, "after_ready", container, binder)
|
|
57
|
+
|
|
58
|
+
logging.info("Container configured and ready.")
|
|
59
|
+
_state._container = container
|
|
60
|
+
_state._root_name = root_name # remember which root this container represents
|
|
61
|
+
return container
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# -------------------- helpers --------------------
|
|
65
|
+
|
|
66
|
+
def _build_exclude(
|
|
67
|
+
exclude: Optional[Callable[[str], bool]],
|
|
68
|
+
auto_exclude_caller: bool,
|
|
69
|
+
*,
|
|
70
|
+
root_name: Optional[str] = None,
|
|
71
|
+
) -> Optional[Callable[[str], bool]]:
|
|
72
|
+
"""
|
|
73
|
+
Compose the exclude predicate. When auto_exclude_caller=True, exclude only
|
|
74
|
+
the exact calling module, but never exclude modules under the root being scanned.
|
|
75
|
+
"""
|
|
76
|
+
if not auto_exclude_caller:
|
|
77
|
+
return exclude
|
|
78
|
+
|
|
79
|
+
caller = _get_caller_module_name()
|
|
80
|
+
if not caller:
|
|
81
|
+
return exclude
|
|
82
|
+
|
|
83
|
+
def _under_root(mod: str) -> bool:
|
|
84
|
+
return bool(root_name) and (mod == root_name or mod.startswith(root_name + "."))
|
|
85
|
+
|
|
86
|
+
if exclude is None:
|
|
87
|
+
return lambda mod, _caller=caller: (mod == _caller) and not _under_root(mod)
|
|
88
|
+
|
|
89
|
+
prev = exclude
|
|
90
|
+
return lambda mod, _caller=caller, _prev=prev: (((mod == _caller) and not _under_root(mod)) or _prev(mod))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _get_caller_module_name() -> Optional[str]:
|
|
94
|
+
"""Return the module name that called `init`."""
|
|
95
|
+
try:
|
|
96
|
+
f = inspect.currentframe()
|
|
97
|
+
# frame -> _get_caller_module_name -> _build_exclude -> init
|
|
98
|
+
if f and f.f_back and f.f_back.f_back and f.f_back.f_back.f_back:
|
|
99
|
+
mod = inspect.getmodule(f.f_back.f_back.f_back)
|
|
100
|
+
return getattr(mod, "__name__", None)
|
|
101
|
+
except Exception:
|
|
102
|
+
pass
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _run_hooks(
|
|
107
|
+
plugins: Tuple[PicoPlugin, ...],
|
|
108
|
+
hook_name: str,
|
|
109
|
+
container: PicoContainer,
|
|
110
|
+
binder: Binder,
|
|
111
|
+
) -> None:
|
|
112
|
+
for pl in plugins:
|
|
113
|
+
try:
|
|
114
|
+
fn = getattr(pl, hook_name, None)
|
|
115
|
+
if fn:
|
|
116
|
+
fn(container, binder)
|
|
117
|
+
except Exception:
|
|
118
|
+
logging.exception("Plugin %s failed", hook_name)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@contextmanager
|
|
122
|
+
def _scanning_flag():
|
|
123
|
+
tok = _state._scanning.set(True)
|
|
124
|
+
try:
|
|
125
|
+
yield
|
|
126
|
+
finally:
|
|
127
|
+
_state._scanning.reset(tok)
|
|
128
|
+
|