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 CHANGED
@@ -1,10 +1,10 @@
1
- import functools
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
+ 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
@@ -0,0 +1,8 @@
1
+ # pico_ioc/_state.py
2
+ from contextvars import ContextVar
3
+
4
+ _scanning: ContextVar[bool] = ContextVar("pico_scanning", default=False)
5
+ _resolving: ContextVar[bool] = ContextVar("pico_resolving", default=False)
6
+
7
+ _container = None
8
+
pico_ioc/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '0.5.1'
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
+
@@ -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.5.1
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
  [![PyPI](https://img.shields.io/pypi/v/pico-ioc.svg)](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,,
@@ -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,,