pico-ioc 0.5.1__py3-none-any.whl → 0.6.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 +8 -389
- pico_ioc/_state.py +8 -0
- pico_ioc/_version.py +1 -1
- pico_ioc/api.py +74 -0
- pico_ioc/container.py +43 -0
- pico_ioc/decorators.py +33 -0
- pico_ioc/plugins.py +12 -0
- pico_ioc/proxy.py +77 -0
- pico_ioc/resolver.py +58 -0
- pico_ioc/scanner.py +105 -0
- pico_ioc/typing_utils.py +24 -0
- {pico_ioc-0.5.1.dist-info → pico_ioc-0.6.0.dist-info}/METADATA +17 -11
- pico_ioc-0.6.0.dist-info/RECORD +16 -0
- pico_ioc-0.5.1.dist-info/RECORD +0 -7
- {pico_ioc-0.5.1.dist-info → pico_ioc-0.6.0.dist-info}/WHEEL +0 -0
- {pico_ioc-0.5.1.dist-info → pico_ioc-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {pico_ioc-0.5.1.dist-info → pico_ioc-0.6.0.dist-info}/top_level.txt +0 -0
pico_ioc/__init__.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
from
|
|
7
|
-
from
|
|
1
|
+
from ._version import __version__
|
|
2
|
+
from .container import PicoContainer, Binder
|
|
3
|
+
from .decorators import component, factory_component, provides
|
|
4
|
+
from .plugins import PicoPlugin
|
|
5
|
+
from .resolver import Resolver
|
|
6
|
+
from .api import init, reset
|
|
7
|
+
from .proxy import ComponentProxy
|
|
8
8
|
|
|
9
9
|
try:
|
|
10
10
|
from ._version import __version__
|
|
@@ -17,6 +17,7 @@ __all__ = [
|
|
|
17
17
|
"Binder",
|
|
18
18
|
"PicoPlugin",
|
|
19
19
|
"init",
|
|
20
|
+
"reset",
|
|
20
21
|
"component",
|
|
21
22
|
"factory_component",
|
|
22
23
|
"provides",
|
|
@@ -24,385 +25,3 @@ __all__ = [
|
|
|
24
25
|
"create_instance",
|
|
25
26
|
]
|
|
26
27
|
|
|
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
pico_ioc/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = '0.
|
|
1
|
+
__version__ = '0.6.0'
|
pico_ioc/api.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Callable, Optional, Tuple
|
|
4
|
+
from .container import PicoContainer, Binder
|
|
5
|
+
from .plugins import PicoPlugin
|
|
6
|
+
from .scanner import scan_and_configure
|
|
7
|
+
from . import _state
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def reset() -> None:
|
|
11
|
+
_state._container = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def init(
|
|
15
|
+
root_package,
|
|
16
|
+
*,
|
|
17
|
+
exclude: Optional[Callable[[str], bool]] = None,
|
|
18
|
+
auto_exclude_caller: bool = True,
|
|
19
|
+
plugins: Tuple[PicoPlugin, ...] = (),
|
|
20
|
+
reuse: bool = True,
|
|
21
|
+
) -> PicoContainer:
|
|
22
|
+
if reuse and _state._container:
|
|
23
|
+
return _state._container
|
|
24
|
+
|
|
25
|
+
combined_exclude = exclude
|
|
26
|
+
if auto_exclude_caller:
|
|
27
|
+
try:
|
|
28
|
+
caller_frame = inspect.stack()[1].frame
|
|
29
|
+
caller_module = inspect.getmodule(caller_frame)
|
|
30
|
+
caller_name = getattr(caller_module, "__name__", None)
|
|
31
|
+
except Exception:
|
|
32
|
+
caller_name = None
|
|
33
|
+
if caller_name:
|
|
34
|
+
if combined_exclude is None:
|
|
35
|
+
def combined_exclude(mod: str, _caller=caller_name):
|
|
36
|
+
return mod == _caller
|
|
37
|
+
else:
|
|
38
|
+
prev = combined_exclude
|
|
39
|
+
def combined_exclude(mod: str, _caller=caller_name, _prev=prev):
|
|
40
|
+
return mod == _caller or _prev(mod)
|
|
41
|
+
|
|
42
|
+
container = PicoContainer()
|
|
43
|
+
binder = Binder(container)
|
|
44
|
+
logging.info("Initializing pico-ioc...")
|
|
45
|
+
|
|
46
|
+
tok = _state._scanning.set(True)
|
|
47
|
+
try:
|
|
48
|
+
scan_and_configure(root_package, container, exclude=combined_exclude, plugins=plugins)
|
|
49
|
+
finally:
|
|
50
|
+
_state._scanning.reset(tok)
|
|
51
|
+
|
|
52
|
+
for pl in plugins:
|
|
53
|
+
try:
|
|
54
|
+
getattr(pl, "after_bind", lambda *a, **k: None)(container, binder)
|
|
55
|
+
except Exception:
|
|
56
|
+
logging.exception("Plugin after_bind failed")
|
|
57
|
+
for pl in plugins:
|
|
58
|
+
try:
|
|
59
|
+
getattr(pl, "before_eager", lambda *a, **k: None)(container, binder)
|
|
60
|
+
except Exception:
|
|
61
|
+
logging.exception("Plugin before_eager failed")
|
|
62
|
+
|
|
63
|
+
container.eager_instantiate_all()
|
|
64
|
+
|
|
65
|
+
for pl in plugins:
|
|
66
|
+
try:
|
|
67
|
+
getattr(pl, "after_ready", lambda *a, **k: None)(container, binder)
|
|
68
|
+
except Exception:
|
|
69
|
+
logging.exception("Plugin after_ready failed")
|
|
70
|
+
|
|
71
|
+
logging.info("Container configured and ready.")
|
|
72
|
+
_state._container = container
|
|
73
|
+
return container
|
|
74
|
+
|
pico_ioc/container.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# pico_ioc/container.py
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Dict
|
|
4
|
+
from ._state import _scanning, _resolving
|
|
5
|
+
|
|
6
|
+
class PicoContainer:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self._providers: Dict[Any, Dict[str, Any]] = {}
|
|
9
|
+
self._singletons: Dict[Any, Any] = {}
|
|
10
|
+
|
|
11
|
+
def bind(self, key: Any, provider: Callable[[], Any], *, lazy: bool):
|
|
12
|
+
self._providers[key] = {"factory": provider, "lazy": bool(lazy)}
|
|
13
|
+
|
|
14
|
+
def has(self, key: Any) -> bool:
|
|
15
|
+
return key in self._providers or key in self._singletons
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get(self, key: Any) -> Any:
|
|
19
|
+
if _scanning.get() and not _resolving.get():
|
|
20
|
+
raise RuntimeError("pico-ioc: re-entrant container access during scan.")
|
|
21
|
+
|
|
22
|
+
if key in self._singletons:
|
|
23
|
+
return self._singletons[key]
|
|
24
|
+
prov = self._providers.get(key)
|
|
25
|
+
if prov is None:
|
|
26
|
+
raise NameError(f"No provider found for key: {key}")
|
|
27
|
+
instance = prov["factory"]()
|
|
28
|
+
self._singletons[key] = instance
|
|
29
|
+
return instance
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def eager_instantiate_all(self):
|
|
33
|
+
for key, meta in list(self._providers.items()):
|
|
34
|
+
if not meta.get("lazy", False) and key not in self._singletons:
|
|
35
|
+
self.get(key)
|
|
36
|
+
|
|
37
|
+
class Binder:
|
|
38
|
+
def __init__(self, container: PicoContainer):
|
|
39
|
+
self._c = container
|
|
40
|
+
def bind(self, key, provider, *, lazy=False): self._c.bind(key, provider, lazy=lazy)
|
|
41
|
+
def has(self, key) -> bool: return self._c.has(key)
|
|
42
|
+
def get(self, key): return self._c.get(key)
|
|
43
|
+
|
pico_ioc/decorators.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# pico_ioc/decorators.py
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
COMPONENT_FLAG = "_is_component"
|
|
7
|
+
COMPONENT_KEY = "_component_key"
|
|
8
|
+
COMPONENT_LAZY = "_component_lazy"
|
|
9
|
+
FACTORY_FLAG = "_is_factory_component"
|
|
10
|
+
PROVIDES_KEY = "_provides_name"
|
|
11
|
+
PROVIDES_LAZY = "_pico_lazy"
|
|
12
|
+
|
|
13
|
+
def factory_component(cls):
|
|
14
|
+
setattr(cls, FACTORY_FLAG, True)
|
|
15
|
+
return cls
|
|
16
|
+
|
|
17
|
+
def provides(key: Any, *, lazy: bool = False):
|
|
18
|
+
def dec(func):
|
|
19
|
+
@functools.wraps(func)
|
|
20
|
+
def w(*a, **k): return func(*a, **k)
|
|
21
|
+
setattr(w, PROVIDES_KEY, key)
|
|
22
|
+
setattr(w, PROVIDES_LAZY, bool(lazy))
|
|
23
|
+
return w
|
|
24
|
+
return dec
|
|
25
|
+
|
|
26
|
+
def component(cls=None, *, name: Any = None, lazy: bool = False):
|
|
27
|
+
def dec(c):
|
|
28
|
+
setattr(c, COMPONENT_FLAG, True)
|
|
29
|
+
setattr(c, COMPONENT_KEY, name if name is not None else c)
|
|
30
|
+
setattr(c, COMPONENT_LAZY, bool(lazy))
|
|
31
|
+
return c
|
|
32
|
+
return dec(cls) if cls else dec
|
|
33
|
+
|
pico_ioc/plugins.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# pico_ioc/plugins.py
|
|
2
|
+
from typing import Protocol, Any
|
|
3
|
+
from .container import Binder, PicoContainer
|
|
4
|
+
|
|
5
|
+
class PicoPlugin(Protocol):
|
|
6
|
+
def before_scan(self, package: Any, binder: Binder) -> None: ...
|
|
7
|
+
def visit_class(self, module: Any, cls: type, binder: Binder) -> None: ...
|
|
8
|
+
def after_scan(self, package: Any, binder: Binder) -> None: ...
|
|
9
|
+
def after_bind(self, container: PicoContainer, binder: Binder) -> None: ...
|
|
10
|
+
def before_eager(self, container: PicoContainer, binder: Binder) -> None: ...
|
|
11
|
+
def after_ready(self, container: PicoContainer, binder: Binder) -> None: ...
|
|
12
|
+
|
pico_ioc/proxy.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# pico_ioc/proxy.py
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable
|
|
4
|
+
|
|
5
|
+
class ComponentProxy:
|
|
6
|
+
def __init__(self, object_creator: Callable[[], Any]):
|
|
7
|
+
object.__setattr__(self, "_object_creator", object_creator)
|
|
8
|
+
object.__setattr__(self, "__real_object", None)
|
|
9
|
+
|
|
10
|
+
def _get_real_object(self) -> Any:
|
|
11
|
+
real_obj = object.__getattribute__(self, "__real_object")
|
|
12
|
+
if real_obj is None:
|
|
13
|
+
real_obj = object.__getattribute__(self, "_object_creator")()
|
|
14
|
+
object.__setattr__(self, "__real_object", real_obj)
|
|
15
|
+
return real_obj
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def __class__(self):
|
|
19
|
+
return self._get_real_object().__class__
|
|
20
|
+
|
|
21
|
+
def __getattr__(self, name): return getattr(self._get_real_object(), name)
|
|
22
|
+
def __setattr__(self, name, value): setattr(self._get_real_object(), name, value)
|
|
23
|
+
def __delattr__(self, name): delattr(self._get_real_object(), name)
|
|
24
|
+
def __str__(self): return str(self._get_real_object())
|
|
25
|
+
def __repr__(self): return repr(self._get_real_object())
|
|
26
|
+
def __dir__(self): return dir(self._get_real_object())
|
|
27
|
+
def __len__(self): return len(self._get_real_object())
|
|
28
|
+
def __getitem__(self, key): return self._get_real_object()[key]
|
|
29
|
+
def __setitem__(self, key, value): self._get_real_object()[key] = value
|
|
30
|
+
def __delitem__(self, key): del self._get_real_object()[key]
|
|
31
|
+
def __iter__(self): return iter(self._get_real_object())
|
|
32
|
+
def __reversed__(self): return reversed(self._get_real_object())
|
|
33
|
+
def __contains__(self, item): return item in self._get_real_object()
|
|
34
|
+
def __add__(self, other): return self._get_real_object() + other
|
|
35
|
+
def __sub__(self, other): return self._get_real_object() - other
|
|
36
|
+
def __mul__(self, other): return self._get_real_object() * other
|
|
37
|
+
def __matmul__(self, other): return self._get_real_object() @ other
|
|
38
|
+
def __truediv__(self, other): return self._get_real_object() / other
|
|
39
|
+
def __floordiv__(self, other): return self._get_real_object() // other
|
|
40
|
+
def __mod__(self, other): return self._get_real_object() % other
|
|
41
|
+
def __divmod__(self, other): return divmod(self._get_real_object(), other)
|
|
42
|
+
def __pow__(self, other, modulo=None): return pow(self._get_real_object(), other, modulo)
|
|
43
|
+
def __lshift__(self, other): return self._get_real_object() << other
|
|
44
|
+
def __rshift__(self, other): return self._get_real_object() >> other
|
|
45
|
+
def __and__(self, other): return self._get_real_object() & other
|
|
46
|
+
def __xor__(self, other): return self._get_real_object() ^ other
|
|
47
|
+
def __or__(self, other): return self._get_real_object() | other
|
|
48
|
+
def __radd__(self, other): return other + self._get_real_object()
|
|
49
|
+
def __rsub__(self, other): return other - self._get_real_object()
|
|
50
|
+
def __rmul__(self, other): return other * self._get_real_object()
|
|
51
|
+
def __rmatmul__(self, other): return other @ self._get_real_object()
|
|
52
|
+
def __rtruediv__(self, other): return other / self._get_real_object()
|
|
53
|
+
def __rfloordiv__(self, other): return other // self._get_real_object()
|
|
54
|
+
def __rmod__(self, other): return other % self._get_real_object()
|
|
55
|
+
def __rdivmod__(self, other): return divmod(other, self._get_real_object())
|
|
56
|
+
def __rpow__(self, other): return pow(other, self._get_real_object())
|
|
57
|
+
def __rlshift__(self, other): return other << self._get_real_object()
|
|
58
|
+
def __rrshift__(self, other): return other >> self._get_real_object()
|
|
59
|
+
def __rand__(self, other): return other & self._get_real_object()
|
|
60
|
+
def __rxor__(self, other): return other ^ self._get_real_object()
|
|
61
|
+
def __ror__(self, other): return other | self._get_real_object()
|
|
62
|
+
def __neg__(self): return -self._get_real_object()
|
|
63
|
+
def __pos__(self): return +self._get_real_object()
|
|
64
|
+
def __abs__(self): return abs(self._get_real_object())
|
|
65
|
+
def __invert__(self): return ~self._get_real_object()
|
|
66
|
+
def __eq__(self, other): return self._get_real_object() == other
|
|
67
|
+
def __ne__(self, other): return self._get_real_object() != other
|
|
68
|
+
def __lt__(self, other): return self._get_real_object() < other
|
|
69
|
+
def __le__(self, other): return self._get_real_object() <= other
|
|
70
|
+
def __gt__(self, other): return self._get_real_object() > other
|
|
71
|
+
def __ge__(self, other): return self._get_real_object() >= other
|
|
72
|
+
def __hash__(self): return hash(self._get_real_object())
|
|
73
|
+
def __bool__(self): return bool(self._get_real_object())
|
|
74
|
+
def __call__(self, *args, **kwargs): return self._get_real_object()(*args, **kwargs)
|
|
75
|
+
def __enter__(self): return self._get_real_object().__enter__()
|
|
76
|
+
def __exit__(self, exc_type, exc_val, exc_tb): return self._get_real_object().__exit__(exc_type, exc_val, exc_tb)
|
|
77
|
+
|
pico_ioc/resolver.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# pico_ioc/resolver.py
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
from .container import PicoContainer
|
|
6
|
+
from .typing_utils import evaluated_hints, resolve_annotation_to_type
|
|
7
|
+
from ._state import _resolving
|
|
8
|
+
|
|
9
|
+
class Resolver:
|
|
10
|
+
def __init__(self, container: PicoContainer):
|
|
11
|
+
self.c = container
|
|
12
|
+
|
|
13
|
+
def _resolve_by_mro(self, ann) -> Optional[Any]:
|
|
14
|
+
try:
|
|
15
|
+
for base in getattr(ann, "__mro__", ())[1:]:
|
|
16
|
+
if base is object: break
|
|
17
|
+
if self.c.has(base): return self.c.get(base)
|
|
18
|
+
except Exception:
|
|
19
|
+
pass
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
def _resolve_param(self, name: str, ann) -> Any:
|
|
23
|
+
if self.c.has(name): return self.c.get(name)
|
|
24
|
+
if ann is not inspect._empty and self.c.has(ann): return self.c.get(ann)
|
|
25
|
+
if ann is not inspect._empty:
|
|
26
|
+
mro_hit = self._resolve_by_mro(ann)
|
|
27
|
+
if mro_hit is not None: return mro_hit
|
|
28
|
+
if self.c.has(str(name)): return self.c.get(str(name))
|
|
29
|
+
key = name if ann is inspect._empty else ann
|
|
30
|
+
return self.c.get(key)
|
|
31
|
+
|
|
32
|
+
def kwargs_for_callable(self, fn, owner_cls=None) -> Dict[str, Any]:
|
|
33
|
+
sig = inspect.signature(fn)
|
|
34
|
+
hints = evaluated_hints(fn, owner_cls=owner_cls)
|
|
35
|
+
deps: Dict[str, Any] = {}
|
|
36
|
+
|
|
37
|
+
tok = _resolving.set(True)
|
|
38
|
+
try:
|
|
39
|
+
for p in sig.parameters.values():
|
|
40
|
+
if p.name == "self" or p.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
41
|
+
continue
|
|
42
|
+
raw_ann = hints.get(p.name, p.annotation)
|
|
43
|
+
ann = resolve_annotation_to_type(raw_ann, fn, owner_cls)
|
|
44
|
+
try:
|
|
45
|
+
deps[p.name] = self._resolve_param(p.name, ann)
|
|
46
|
+
except NameError:
|
|
47
|
+
if p.default is not inspect._empty:
|
|
48
|
+
continue
|
|
49
|
+
missing = ann if ann is not inspect._empty else p.name
|
|
50
|
+
raise NameError(f"No provider found for key: {missing}")
|
|
51
|
+
return deps
|
|
52
|
+
finally:
|
|
53
|
+
_resolving.reset(tok)
|
|
54
|
+
|
|
55
|
+
def create_instance(self, cls: type) -> Any:
|
|
56
|
+
kw = self.kwargs_for_callable(cls.__init__, owner_cls=cls)
|
|
57
|
+
return cls(**kw)
|
|
58
|
+
|
pico_ioc/scanner.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import inspect
|
|
3
|
+
import logging
|
|
4
|
+
import pkgutil
|
|
5
|
+
from typing import Any, Callable, Optional, Tuple, List
|
|
6
|
+
|
|
7
|
+
from .container import PicoContainer, Binder
|
|
8
|
+
from .decorators import (
|
|
9
|
+
COMPONENT_FLAG,
|
|
10
|
+
COMPONENT_KEY,
|
|
11
|
+
COMPONENT_LAZY,
|
|
12
|
+
FACTORY_FLAG,
|
|
13
|
+
PROVIDES_KEY,
|
|
14
|
+
PROVIDES_LAZY,
|
|
15
|
+
)
|
|
16
|
+
from .proxy import ComponentProxy
|
|
17
|
+
from .resolver import Resolver
|
|
18
|
+
from .plugins import PicoPlugin
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def scan_and_configure(
|
|
22
|
+
package_or_name: Any,
|
|
23
|
+
container: PicoContainer,
|
|
24
|
+
*,
|
|
25
|
+
exclude: Optional[Callable[[str], bool]] = None,
|
|
26
|
+
plugins: Tuple[PicoPlugin, ...] = (),
|
|
27
|
+
):
|
|
28
|
+
package = importlib.import_module(package_or_name) if isinstance(package_or_name, str) else package_or_name
|
|
29
|
+
logging.info(f"Scanning in '{package.__name__}'...")
|
|
30
|
+
binder = Binder(container)
|
|
31
|
+
resolver = Resolver(container)
|
|
32
|
+
|
|
33
|
+
for pl in plugins:
|
|
34
|
+
try:
|
|
35
|
+
getattr(pl, "before_scan", lambda *a, **k: None)(package, binder)
|
|
36
|
+
except Exception:
|
|
37
|
+
logging.exception("Plugin before_scan failed")
|
|
38
|
+
|
|
39
|
+
comp_classes: List[type] = []
|
|
40
|
+
factory_classes: List[type] = []
|
|
41
|
+
|
|
42
|
+
for _, name, _ in pkgutil.walk_packages(package.__path__, package.__name__ + "."):
|
|
43
|
+
if exclude and exclude(name):
|
|
44
|
+
logging.info(f"Skipping module {name} (excluded)")
|
|
45
|
+
continue
|
|
46
|
+
try:
|
|
47
|
+
module = importlib.import_module(name)
|
|
48
|
+
for _, obj in inspect.getmembers(module, inspect.isclass):
|
|
49
|
+
for pl in plugins:
|
|
50
|
+
try:
|
|
51
|
+
getattr(pl, "visit_class", lambda *a, **k: None)(module, obj, binder)
|
|
52
|
+
except Exception:
|
|
53
|
+
logging.exception("Plugin visit_class failed")
|
|
54
|
+
if getattr(obj, COMPONENT_FLAG, False):
|
|
55
|
+
comp_classes.append(obj)
|
|
56
|
+
elif getattr(obj, FACTORY_FLAG, False):
|
|
57
|
+
factory_classes.append(obj)
|
|
58
|
+
except Exception as e:
|
|
59
|
+
logging.warning(f"Module {name} not processed: {e}")
|
|
60
|
+
|
|
61
|
+
for pl in plugins:
|
|
62
|
+
try:
|
|
63
|
+
getattr(pl, "after_scan", lambda *a, **k: None)(package, binder)
|
|
64
|
+
except Exception:
|
|
65
|
+
logging.exception("Plugin after_scan failed")
|
|
66
|
+
|
|
67
|
+
# Register @component classes (bind ONLY by declared key)
|
|
68
|
+
for cls in comp_classes:
|
|
69
|
+
key = getattr(cls, COMPONENT_KEY, cls)
|
|
70
|
+
is_lazy = bool(getattr(cls, COMPONENT_LAZY, False))
|
|
71
|
+
|
|
72
|
+
def provider_factory(c=cls, lazy=is_lazy):
|
|
73
|
+
def _factory():
|
|
74
|
+
if lazy:
|
|
75
|
+
return ComponentProxy(lambda: resolver.create_instance(c))
|
|
76
|
+
return resolver.create_instance(c)
|
|
77
|
+
return _factory
|
|
78
|
+
|
|
79
|
+
container.bind(key, provider_factory(), lazy=is_lazy)
|
|
80
|
+
|
|
81
|
+
# Register @factory_component methods marked with @provides
|
|
82
|
+
for fcls in factory_classes:
|
|
83
|
+
try:
|
|
84
|
+
finst = resolver.create_instance(fcls)
|
|
85
|
+
for name, func in inspect.getmembers(fcls, predicate=inspect.isfunction):
|
|
86
|
+
pk = getattr(func, PROVIDES_KEY, None)
|
|
87
|
+
if pk is None:
|
|
88
|
+
continue
|
|
89
|
+
is_lazy = bool(getattr(func, PROVIDES_LAZY, False))
|
|
90
|
+
bound = getattr(finst, name, func.__get__(finst, fcls))
|
|
91
|
+
|
|
92
|
+
def make_provider(m=bound, lazy=is_lazy):
|
|
93
|
+
def _factory():
|
|
94
|
+
kwargs = resolver.kwargs_for_callable(m, owner_cls=fcls)
|
|
95
|
+
|
|
96
|
+
def _call():
|
|
97
|
+
return m(**kwargs)
|
|
98
|
+
|
|
99
|
+
return ComponentProxy(lambda: _call()) if lazy else _call()
|
|
100
|
+
return _factory
|
|
101
|
+
|
|
102
|
+
container.bind(pk, make_provider(), lazy=is_lazy)
|
|
103
|
+
except Exception:
|
|
104
|
+
logging.exception(f"Error in factory {fcls.__name__}")
|
|
105
|
+
|
pico_ioc/typing_utils.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# pico_ioc/typing_utils.py
|
|
2
|
+
|
|
3
|
+
import sys, typing
|
|
4
|
+
|
|
5
|
+
def evaluated_hints(func, owner_cls=None) -> dict:
|
|
6
|
+
try:
|
|
7
|
+
module = sys.modules.get(func.__module__)
|
|
8
|
+
globalns = getattr(module, "__dict__", {})
|
|
9
|
+
localns = vars(owner_cls) if owner_cls is not None else {}
|
|
10
|
+
return typing.get_type_hints(func, globalns=globalns, localns=localns)
|
|
11
|
+
except Exception:
|
|
12
|
+
return {}
|
|
13
|
+
|
|
14
|
+
def resolve_annotation_to_type(ann, func, owner_cls=None):
|
|
15
|
+
if not isinstance(ann, str):
|
|
16
|
+
return ann
|
|
17
|
+
try:
|
|
18
|
+
module = sys.modules.get(func.__module__)
|
|
19
|
+
globalns = getattr(module, "__dict__", {})
|
|
20
|
+
localns = vars(owner_cls) if owner_cls is not None else {}
|
|
21
|
+
return eval(ann, globalns, localns)
|
|
22
|
+
except Exception:
|
|
23
|
+
return ann
|
|
24
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pico-ioc
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: A minimalist, zero-dependency Inversion of Control (IoC) container for Python.
|
|
5
5
|
Author-email: David Perez Cabrera <dperezcabrera@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -45,12 +45,6 @@ Description-Content-Type: text/markdown
|
|
|
45
45
|
License-File: LICENSE
|
|
46
46
|
Dynamic: license-file
|
|
47
47
|
|
|
48
|
-
Got it ✅
|
|
49
|
-
Here’s your **updated README.md in full English**, keeping all original sections but now including the **name-first resolution** feature and new tests section.
|
|
50
|
-
|
|
51
|
-
---
|
|
52
|
-
|
|
53
|
-
````markdown
|
|
54
48
|
# 📦 Pico-IoC: A Minimalist IoC Container for Python
|
|
55
49
|
|
|
56
50
|
[](https://pypi.org/project/pico-ioc/)
|
|
@@ -162,10 +156,10 @@ print(COUNTER["value"]) # 1
|
|
|
162
156
|
|
|
163
157
|
Starting with **v0.5.0**, Pico-IoC enforces **name-first resolution**:
|
|
164
158
|
|
|
165
|
-
1. **Parameter name** (highest priority)
|
|
166
|
-
2. **Exact type annotation**
|
|
167
|
-
3. **MRO fallback** (walk base classes)
|
|
168
|
-
4. **String(name)**
|
|
159
|
+
1. **Parameter name** (highest priority)
|
|
160
|
+
2. **Exact type annotation**
|
|
161
|
+
3. **MRO fallback** (walk base classes)
|
|
162
|
+
4. **String(name)**
|
|
169
163
|
|
|
170
164
|
This means that if a dependency could match both by name and type, **the name match wins**.
|
|
171
165
|
|
|
@@ -191,6 +185,18 @@ class NameVsTypeFactory:
|
|
|
191
185
|
container = init(__name__)
|
|
192
186
|
assert container.get("choose") == "by-name"
|
|
193
187
|
```
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## 📝 Notes on Annotations (PEP 563)
|
|
191
|
+
|
|
192
|
+
Pico-IoC fully supports **postponed evaluation of annotations**
|
|
193
|
+
(`from __future__ import annotations`, a.k.a. **PEP 563**) in Python 3.8–3.10.
|
|
194
|
+
|
|
195
|
+
* Type hints are evaluated with `typing.get_type_hints` and safely resolved.
|
|
196
|
+
* Missing dependencies always raise a **`NameError`**, never a `TypeError`.
|
|
197
|
+
* Behavior is consistent across Python 3.8+ and Python 3.11+ (where PEP 563 is no longer default).
|
|
198
|
+
|
|
199
|
+
This means you can freely use either direct type hints or string-based annotations in your components and factories, without breaking dependency injection.
|
|
194
200
|
|
|
195
201
|
---
|
|
196
202
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
pico_ioc/__init__.py,sha256=1UwdG8Ax7syyvN9uUUPUr_Gzf-TswqFGzoIHx0dG7b0,572
|
|
2
|
+
pico_ioc/_state.py,sha256=hXBOob3SwVYcord8eln4Fe2dzJJTDovh-dAll2DRkvg,225
|
|
3
|
+
pico_ioc/_version.py,sha256=CBY3jsC-9HCm7eZ6CKD-sYLCejqOJ1pYWPQM4LGIXcI,22
|
|
4
|
+
pico_ioc/api.py,sha256=5ZiVqggvgS_ln8AwzpCRZall6OyeRX_JADwW1I1tDdM,2281
|
|
5
|
+
pico_ioc/container.py,sha256=OqR4giSesWOAdbVvy5Z9GpK20h3PWlkYdA0xrqVTbTw,1479
|
|
6
|
+
pico_ioc/decorators.py,sha256=nKbcfzMQc_QKmVRA6Z2IHqlmimkD3Xy-JMzMXZuM6Ps,900
|
|
7
|
+
pico_ioc/plugins.py,sha256=JbI-28VLGJaik7ysXi3L-YGTGxhqwJH4W5QYuWSruDE,589
|
|
8
|
+
pico_ioc/proxy.py,sha256=-e3Z9z7Bc_2wxswwUJI_s8AfvCTps8f8RWUJ9RuEp7E,4606
|
|
9
|
+
pico_ioc/resolver.py,sha256=xCDDBXGFpSay3dp_MKam83921xmaVpCNnHISgyR6JCs,2225
|
|
10
|
+
pico_ioc/scanner.py,sha256=T-E4VMUanU1o1R8c2LUMDuhvIJFonap82UzOOtkNza4,3775
|
|
11
|
+
pico_ioc/typing_utils.py,sha256=lpRusK1o5o36o278wSyuDVsGM3D0LGqFB0n_LOQ83F0,770
|
|
12
|
+
pico_ioc-0.6.0.dist-info/licenses/LICENSE,sha256=N1_nOvHTM6BobYnOTNXiQkroDqCEi6EzfGBv8lWtyZ0,1077
|
|
13
|
+
pico_ioc-0.6.0.dist-info/METADATA,sha256=SdV4U9PjQZNrF1Z06IF4Z_8r4SYFoaTDwYH-Y0W2Nys,10039
|
|
14
|
+
pico_ioc-0.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
15
|
+
pico_ioc-0.6.0.dist-info/top_level.txt,sha256=_7_RLu616z_dtRw16impXn4Mw8IXe2J4BeX5912m5dQ,9
|
|
16
|
+
pico_ioc-0.6.0.dist-info/RECORD,,
|
pico_ioc-0.5.1.dist-info/RECORD
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
pico_ioc/__init__.py,sha256=zWSJSAIgpZ_bXNi_6s88Lmql181ECyIy_Yh7hc5SOd8,16487
|
|
2
|
-
pico_ioc/_version.py,sha256=s8Yq9Om1yBxrMA7xYQ5Y13Paeuxnq99NxhyjuPlnH6A,22
|
|
3
|
-
pico_ioc-0.5.1.dist-info/licenses/LICENSE,sha256=N1_nOvHTM6BobYnOTNXiQkroDqCEi6EzfGBv8lWtyZ0,1077
|
|
4
|
-
pico_ioc-0.5.1.dist-info/METADATA,sha256=dgeFxNxy4tCDXNWgUqirSbjK80CTAlhsNWrumI5z47A,9623
|
|
5
|
-
pico_ioc-0.5.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
-
pico_ioc-0.5.1.dist-info/top_level.txt,sha256=_7_RLu616z_dtRw16impXn4Mw8IXe2J4BeX5912m5dQ,9
|
|
7
|
-
pico_ioc-0.5.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|