pico-ioc 0.3.0__py3-none-any.whl → 0.4.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 +187 -142
- pico_ioc/_version.py +1 -1
- pico_ioc-0.4.0.dist-info/METADATA +321 -0
- pico_ioc-0.4.0.dist-info/RECORD +6 -0
- pico_ioc-0.3.0.dist-info/METADATA +0 -228
- pico_ioc-0.3.0.dist-info/RECORD +0 -6
- {pico_ioc-0.3.0.dist-info → pico_ioc-0.4.0.dist-info}/WHEEL +0 -0
- {pico_ioc-0.3.0.dist-info → pico_ioc-0.4.0.dist-info}/top_level.txt +0 -0
pico_ioc/__init__.py
CHANGED
|
@@ -1,63 +1,96 @@
|
|
|
1
|
-
import functools
|
|
2
|
-
|
|
1
|
+
import functools
|
|
2
|
+
import importlib
|
|
3
|
+
import inspect
|
|
4
|
+
import logging
|
|
5
|
+
import pkgutil
|
|
3
6
|
from contextvars import ContextVar
|
|
7
|
+
from typing import Any, Callable, Dict, Optional, Protocol, Tuple, runtime_checkable
|
|
4
8
|
|
|
5
9
|
try:
|
|
6
|
-
# written at build time by setuptools-scm
|
|
7
10
|
from ._version import __version__
|
|
8
|
-
except Exception:
|
|
11
|
+
except Exception:
|
|
9
12
|
__version__ = "0.0.0"
|
|
10
13
|
|
|
11
|
-
__all__ = [
|
|
14
|
+
__all__ = [
|
|
15
|
+
"__version__",
|
|
16
|
+
"PicoContainer",
|
|
17
|
+
"Binder",
|
|
18
|
+
"PicoPlugin",
|
|
19
|
+
"init",
|
|
20
|
+
"component",
|
|
21
|
+
"factory_component",
|
|
22
|
+
"provides",
|
|
23
|
+
"resolve_param",
|
|
24
|
+
"create_instance",
|
|
25
|
+
]
|
|
12
26
|
|
|
13
|
-
# ------------------------------------------------------------------------------
|
|
14
|
-
# Re-entrancy guards
|
|
15
|
-
# ------------------------------------------------------------------------------
|
|
16
|
-
# True while init/scan is running. Blocks userland container access during scan.
|
|
17
27
|
_scanning: ContextVar[bool] = ContextVar("pico_scanning", default=False)
|
|
18
|
-
|
|
19
|
-
# True while the container is resolving deps for a component (internal use allowed).
|
|
20
28
|
_resolving: ContextVar[bool] = ContextVar("pico_resolving", default=False)
|
|
21
29
|
|
|
22
|
-
# ==============================================================================
|
|
23
|
-
# --- 1. Container and Chameleon Proxy (Framework-Agnostic) ---
|
|
24
|
-
# ==============================================================================
|
|
25
30
|
class PicoContainer:
|
|
26
31
|
def __init__(self):
|
|
27
|
-
self._providers:
|
|
28
|
-
self._singletons:
|
|
32
|
+
self._providers: Dict[Any, Dict[str, Any]] = {}
|
|
33
|
+
self._singletons: Dict[Any, Any] = {}
|
|
29
34
|
|
|
30
|
-
def bind(self, key: Any, provider: Callable[[], Any]):
|
|
31
|
-
self._providers[key] = provider
|
|
35
|
+
def bind(self, key: Any, provider: Callable[[], Any], *, lazy: bool):
|
|
36
|
+
self._providers[key] = {"factory": provider, "lazy": bool(lazy)}
|
|
32
37
|
|
|
33
38
|
def has(self, key: Any) -> bool:
|
|
34
39
|
return key in self._providers or key in self._singletons
|
|
35
40
|
|
|
36
41
|
def get(self, key: Any) -> Any:
|
|
37
|
-
# Forbid user code calling container.get() while the scanner is importing modules.
|
|
38
|
-
# Allow only internal calls performed during dependency resolution.
|
|
39
42
|
if _scanning.get() and not _resolving.get():
|
|
40
|
-
raise RuntimeError(
|
|
41
|
-
"pico-ioc: re-entrant container access during scan. "
|
|
42
|
-
"Avoid calling init()/get() at import time (e.g., in a module body). "
|
|
43
|
-
"Move resolution to runtime (e.g., under if __name__ == '__main__':) "
|
|
44
|
-
"or delay it until pico-ioc init completes."
|
|
45
|
-
)
|
|
46
|
-
|
|
43
|
+
raise RuntimeError("pico-ioc: re-entrant container access during scan.")
|
|
47
44
|
if key in self._singletons:
|
|
48
45
|
return self._singletons[key]
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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:
|
|
61
94
|
def __init__(self, object_creator: Callable[[], Any]):
|
|
62
95
|
object.__setattr__(self, "_object_creator", object_creator)
|
|
63
96
|
object.__setattr__(self, "__real_object", None)
|
|
@@ -69,30 +102,16 @@ class LazyProxy:
|
|
|
69
102
|
object.__setattr__(self, "__real_object", real_obj)
|
|
70
103
|
return real_obj
|
|
71
104
|
|
|
72
|
-
# --- Core Proxying and Representation ---
|
|
73
105
|
@property
|
|
74
106
|
def __class__(self):
|
|
75
107
|
return self._get_real_object().__class__
|
|
76
108
|
|
|
77
|
-
def __getattr__(self, name):
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def __delattr__(self, name):
|
|
84
|
-
delattr(self._get_real_object(), name)
|
|
85
|
-
|
|
86
|
-
def __str__(self):
|
|
87
|
-
return str(self._get_real_object())
|
|
88
|
-
|
|
89
|
-
def __repr__(self):
|
|
90
|
-
return repr(self._get_real_object())
|
|
91
|
-
|
|
92
|
-
def __dir__(self):
|
|
93
|
-
return dir(self._get_real_object())
|
|
94
|
-
|
|
95
|
-
# --- Emulation of container types ---
|
|
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())
|
|
96
115
|
def __len__(self): return len(self._get_real_object())
|
|
97
116
|
def __getitem__(self, key): return self._get_real_object()[key]
|
|
98
117
|
def __setitem__(self, key, value): self._get_real_object()[key] = value
|
|
@@ -100,8 +119,6 @@ class LazyProxy:
|
|
|
100
119
|
def __iter__(self): return iter(self._get_real_object())
|
|
101
120
|
def __reversed__(self): return reversed(self._get_real_object())
|
|
102
121
|
def __contains__(self, item): return item in self._get_real_object()
|
|
103
|
-
|
|
104
|
-
# --- Emulation of numeric types and operators ---
|
|
105
122
|
def __add__(self, other): return self._get_real_object() + other
|
|
106
123
|
def __sub__(self, other): return self._get_real_object() - other
|
|
107
124
|
def __mul__(self, other): return self._get_real_object() * other
|
|
@@ -116,8 +133,6 @@ class LazyProxy:
|
|
|
116
133
|
def __and__(self, other): return self._get_real_object() & other
|
|
117
134
|
def __xor__(self, other): return self._get_real_object() ^ other
|
|
118
135
|
def __or__(self, other): return self._get_real_object() | other
|
|
119
|
-
|
|
120
|
-
# --- Right-hand side numeric operators ---
|
|
121
136
|
def __radd__(self, other): return other + self._get_real_object()
|
|
122
137
|
def __rsub__(self, other): return other - self._get_real_object()
|
|
123
138
|
def __rmul__(self, other): return other * self._get_real_object()
|
|
@@ -132,14 +147,10 @@ class LazyProxy:
|
|
|
132
147
|
def __rand__(self, other): return other & self._get_real_object()
|
|
133
148
|
def __rxor__(self, other): return other ^ self._get_real_object()
|
|
134
149
|
def __ror__(self, other): return other | self._get_real_object()
|
|
135
|
-
|
|
136
|
-
# --- Unary operators ---
|
|
137
150
|
def __neg__(self): return -self._get_real_object()
|
|
138
151
|
def __pos__(self): return +self._get_real_object()
|
|
139
152
|
def __abs__(self): return abs(self._get_real_object())
|
|
140
153
|
def __invert__(self): return ~self._get_real_object()
|
|
141
|
-
|
|
142
|
-
# --- Comparison operators ---
|
|
143
154
|
def __eq__(self, other): return self._get_real_object() == other
|
|
144
155
|
def __ne__(self, other): return self._get_real_object() != other
|
|
145
156
|
def __lt__(self, other): return self._get_real_object() < other
|
|
@@ -147,31 +158,19 @@ class LazyProxy:
|
|
|
147
158
|
def __gt__(self, other): return self._get_real_object() > other
|
|
148
159
|
def __ge__(self, other): return self._get_real_object() >= other
|
|
149
160
|
def __hash__(self): return hash(self._get_real_object())
|
|
150
|
-
|
|
151
|
-
# --- Truthiness, Callability and Context Management ---
|
|
152
161
|
def __bool__(self): return bool(self._get_real_object())
|
|
153
162
|
def __call__(self, *args, **kwargs): return self._get_real_object()(*args, **kwargs)
|
|
154
163
|
def __enter__(self): return self._get_real_object().__enter__()
|
|
155
164
|
def __exit__(self, exc_type, exc_val, exc_tb): return self._get_real_object().__exit__(exc_type, exc_val, exc_tb)
|
|
156
165
|
|
|
157
|
-
|
|
158
|
-
# --- 2. The Scanner and `init` Facade ---
|
|
159
|
-
# ==============================================================================
|
|
160
|
-
def _resolve_param(container: PicoContainer, p: inspect.Parameter) -> Any:
|
|
166
|
+
def resolve_param(container: PicoContainer, p: inspect.Parameter) -> Any:
|
|
161
167
|
if p.name == 'self' or p.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
162
168
|
raise RuntimeError("Invalid param for resolution")
|
|
163
|
-
|
|
164
|
-
# 1) NAME
|
|
165
169
|
if container.has(p.name):
|
|
166
170
|
return container.get(p.name)
|
|
167
|
-
|
|
168
171
|
ann = p.annotation
|
|
169
|
-
|
|
170
|
-
# 2) TYPE
|
|
171
172
|
if ann is not inspect._empty and container.has(ann):
|
|
172
173
|
return container.get(ann)
|
|
173
|
-
|
|
174
|
-
# 3) TYPE MRO
|
|
175
174
|
if ann is not inspect._empty:
|
|
176
175
|
try:
|
|
177
176
|
for base in getattr(ann, "__mro__", ())[1:]:
|
|
@@ -181,80 +180,115 @@ def _resolve_param(container: PicoContainer, p: inspect.Parameter) -> Any:
|
|
|
181
180
|
return container.get(base)
|
|
182
181
|
except Exception:
|
|
183
182
|
pass
|
|
184
|
-
|
|
185
|
-
# 4) str(NAME)
|
|
186
183
|
if container.has(str(p.name)):
|
|
187
184
|
return container.get(str(p.name))
|
|
188
|
-
|
|
189
185
|
key = p.name if ann is inspect._empty else ann
|
|
190
186
|
return container.get(key)
|
|
191
187
|
|
|
188
|
+
def create_instance(cls: type, container: PicoContainer) -> Any:
|
|
189
|
+
sig = inspect.signature(cls.__init__)
|
|
190
|
+
deps = {}
|
|
191
|
+
tok = _resolving.set(True)
|
|
192
|
+
try:
|
|
193
|
+
for p in sig.parameters.values():
|
|
194
|
+
if p.name == 'self' or p.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
195
|
+
continue
|
|
196
|
+
deps[p.name] = resolve_param(container, p)
|
|
197
|
+
finally:
|
|
198
|
+
_resolving.reset(tok)
|
|
199
|
+
return cls(**deps)
|
|
200
|
+
|
|
201
|
+
@runtime_checkable
|
|
202
|
+
class PicoPlugin(Protocol):
|
|
203
|
+
def before_scan(self, package: Any, binder: Binder) -> None: ...
|
|
204
|
+
def visit_class(self, module: Any, cls: type, binder: Binder) -> None: ...
|
|
205
|
+
def after_scan(self, package: Any, binder: Binder) -> None: ...
|
|
206
|
+
def after_bind(self, container: PicoContainer, binder: Binder) -> None: ...
|
|
207
|
+
def before_eager(self, container: PicoContainer, binder: Binder) -> None: ...
|
|
208
|
+
def after_ready(self, container: PicoContainer, binder: Binder) -> None: ...
|
|
192
209
|
|
|
193
210
|
def _scan_and_configure(
|
|
194
211
|
package_or_name,
|
|
195
212
|
container: PicoContainer,
|
|
196
|
-
|
|
213
|
+
*,
|
|
214
|
+
exclude: Optional[Callable[[str], bool]] = None,
|
|
215
|
+
plugins: Tuple[PicoPlugin, ...] = (),
|
|
197
216
|
):
|
|
198
217
|
package = importlib.import_module(package_or_name) if isinstance(package_or_name, str) else package_or_name
|
|
199
|
-
logging.info(f"
|
|
218
|
+
logging.info(f"Scanning in '{package.__name__}'...")
|
|
219
|
+
binder = Binder(container)
|
|
220
|
+
for pl in plugins:
|
|
221
|
+
try:
|
|
222
|
+
if hasattr(pl, "before_scan"):
|
|
223
|
+
pl.before_scan(package, binder)
|
|
224
|
+
except Exception:
|
|
225
|
+
logging.exception("Plugin before_scan failed")
|
|
200
226
|
component_classes, factory_classes = [], []
|
|
201
|
-
|
|
202
227
|
for _, name, _ in pkgutil.walk_packages(package.__path__, package.__name__ + '.'):
|
|
203
228
|
if exclude and exclude(name):
|
|
204
|
-
logging.info(f"
|
|
229
|
+
logging.info(f"Skipping module {name} (excluded)")
|
|
205
230
|
continue
|
|
206
231
|
try:
|
|
207
232
|
module = importlib.import_module(name)
|
|
208
233
|
for _, obj in inspect.getmembers(module, inspect.isclass):
|
|
234
|
+
for pl in plugins:
|
|
235
|
+
try:
|
|
236
|
+
if hasattr(pl, "visit_class"):
|
|
237
|
+
pl.visit_class(module, obj, binder)
|
|
238
|
+
except Exception:
|
|
239
|
+
logging.exception("Plugin visit_class failed")
|
|
209
240
|
if getattr(obj, '_is_component', False):
|
|
210
241
|
component_classes.append(obj)
|
|
211
242
|
elif getattr(obj, '_is_factory_component', False):
|
|
212
243
|
factory_classes.append(obj)
|
|
213
244
|
except Exception as e:
|
|
214
|
-
logging.warning(f"
|
|
215
|
-
|
|
216
|
-
|
|
245
|
+
logging.warning(f"Module {name} not processed: {e}")
|
|
246
|
+
for pl in plugins:
|
|
247
|
+
try:
|
|
248
|
+
if hasattr(pl, "after_scan"):
|
|
249
|
+
pl.after_scan(package, binder)
|
|
250
|
+
except Exception:
|
|
251
|
+
logging.exception("Plugin after_scan failed")
|
|
217
252
|
for factory_cls in factory_classes:
|
|
218
253
|
try:
|
|
219
254
|
sig = inspect.signature(factory_cls.__init__)
|
|
220
255
|
instance = factory_cls(container) if 'container' in sig.parameters else factory_cls()
|
|
221
256
|
for _, method in inspect.getmembers(instance, inspect.ismethod):
|
|
222
257
|
if hasattr(method, '_provides_name'):
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
258
|
+
key = getattr(method, '_provides_name')
|
|
259
|
+
is_lazy = bool(getattr(method, '_pico_lazy', False))
|
|
260
|
+
def make_provider(m=method, lazy=is_lazy):
|
|
261
|
+
def _factory():
|
|
262
|
+
if lazy:
|
|
263
|
+
return ComponentProxy(lambda: m())
|
|
264
|
+
return m()
|
|
265
|
+
return _factory
|
|
266
|
+
container.bind(key, make_provider(), lazy=is_lazy)
|
|
267
|
+
except Exception:
|
|
268
|
+
logging.exception(f"Error in factory {factory_cls.__name__}")
|
|
228
269
|
for component_cls in component_classes:
|
|
229
270
|
key = getattr(component_cls, '_component_key', component_cls)
|
|
230
|
-
|
|
231
|
-
def
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
def init(root_package, *, exclude: Optional[Callable[[str], bool]] = None, auto_exclude_caller: bool = True):
|
|
249
|
-
"""
|
|
250
|
-
Initialize the global container and scan the given root package/module.
|
|
251
|
-
While scanning, re-entrant userland access to container.get() is blocked
|
|
252
|
-
to avoid import-time side effects.
|
|
253
|
-
"""
|
|
271
|
+
is_lazy = bool(getattr(component_cls, '_component_lazy', False))
|
|
272
|
+
def provider_factory(lazy=is_lazy, cls=component_cls):
|
|
273
|
+
def _factory():
|
|
274
|
+
if lazy:
|
|
275
|
+
return ComponentProxy(lambda: create_instance(cls, container))
|
|
276
|
+
return create_instance(cls, container)
|
|
277
|
+
return _factory
|
|
278
|
+
container.bind(key, provider_factory(), lazy=is_lazy)
|
|
279
|
+
|
|
280
|
+
_container: Optional[PicoContainer] = None
|
|
281
|
+
|
|
282
|
+
def init(
|
|
283
|
+
root_package,
|
|
284
|
+
*,
|
|
285
|
+
exclude: Optional[Callable[[str], bool]] = None,
|
|
286
|
+
auto_exclude_caller: bool = True,
|
|
287
|
+
plugins: Tuple[PicoPlugin, ...] = (),
|
|
288
|
+
):
|
|
254
289
|
global _container
|
|
255
290
|
if _container:
|
|
256
291
|
return _container
|
|
257
|
-
|
|
258
292
|
combined_exclude = exclude
|
|
259
293
|
if auto_exclude_caller:
|
|
260
294
|
try:
|
|
@@ -263,7 +297,6 @@ def init(root_package, *, exclude: Optional[Callable[[str], bool]] = None, auto_
|
|
|
263
297
|
caller_name = getattr(caller_module, "__name__", None)
|
|
264
298
|
except Exception:
|
|
265
299
|
caller_name = None
|
|
266
|
-
|
|
267
300
|
if caller_name:
|
|
268
301
|
if combined_exclude is None:
|
|
269
302
|
def combined_exclude(mod: str, _caller=caller_name):
|
|
@@ -272,47 +305,59 @@ def init(root_package, *, exclude: Optional[Callable[[str], bool]] = None, auto_
|
|
|
272
305
|
prev = combined_exclude
|
|
273
306
|
def combined_exclude(mod: str, _caller=caller_name, _prev=prev):
|
|
274
307
|
return mod == _caller or _prev(mod)
|
|
275
|
-
|
|
276
308
|
_container = PicoContainer()
|
|
277
|
-
|
|
278
|
-
|
|
309
|
+
binder = Binder(_container)
|
|
310
|
+
logging.info("Initializing pico-ioc...")
|
|
279
311
|
tok = _scanning.set(True)
|
|
280
312
|
try:
|
|
281
|
-
_scan_and_configure(root_package, _container, exclude=combined_exclude)
|
|
313
|
+
_scan_and_configure(root_package, _container, exclude=combined_exclude, plugins=plugins)
|
|
282
314
|
finally:
|
|
283
315
|
_scanning.reset(tok)
|
|
284
|
-
|
|
285
|
-
|
|
316
|
+
for pl in plugins:
|
|
317
|
+
try:
|
|
318
|
+
if hasattr(pl, "after_bind"):
|
|
319
|
+
pl.after_bind(_container, binder)
|
|
320
|
+
except Exception:
|
|
321
|
+
logging.exception("Plugin after_bind failed")
|
|
322
|
+
for pl in plugins:
|
|
323
|
+
try:
|
|
324
|
+
if hasattr(pl, "before_eager"):
|
|
325
|
+
pl.before_eager(_container, binder)
|
|
326
|
+
except Exception:
|
|
327
|
+
logging.exception("Plugin before_eager failed")
|
|
328
|
+
_container._eager_instantiate_all()
|
|
329
|
+
for pl in plugins:
|
|
330
|
+
try:
|
|
331
|
+
if hasattr(pl, "after_ready"):
|
|
332
|
+
pl.after_ready(_container, binder)
|
|
333
|
+
except Exception:
|
|
334
|
+
logging.exception("Plugin after_ready failed")
|
|
335
|
+
logging.info("Container configured and ready.")
|
|
286
336
|
return _container
|
|
287
337
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
# ==============================================================================
|
|
338
|
+
|
|
339
|
+
|
|
291
340
|
def factory_component(cls):
|
|
292
341
|
setattr(cls, '_is_factory_component', True)
|
|
293
342
|
return cls
|
|
294
343
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
Declare that a factory method provides a component under 'key'.
|
|
298
|
-
By default, returns a LazyProxy that instantiates upon first real use.
|
|
299
|
-
"""
|
|
344
|
+
|
|
345
|
+
def provides(key: Any, *, lazy: bool = False):
|
|
300
346
|
def decorator(func):
|
|
301
347
|
@functools.wraps(func)
|
|
302
348
|
def wrapper(*args, **kwargs):
|
|
303
|
-
return
|
|
304
|
-
setattr(wrapper, '_provides_name', key)
|
|
349
|
+
return func(*args, **kwargs)
|
|
350
|
+
setattr(wrapper, '_provides_name', key)
|
|
351
|
+
setattr(wrapper, '_pico_lazy', bool(lazy))
|
|
305
352
|
return wrapper
|
|
306
353
|
return decorator
|
|
307
354
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
Mark a class as a component. Registered by class type by default,
|
|
311
|
-
or by 'name' if provided.
|
|
312
|
-
"""
|
|
355
|
+
|
|
356
|
+
def component(cls=None, *, name: Any = None, lazy: bool = False):
|
|
313
357
|
def decorator(c):
|
|
314
358
|
setattr(c, '_is_component', True)
|
|
315
359
|
setattr(c, '_component_key', name if name is not None else c)
|
|
360
|
+
setattr(c, '_component_lazy', bool(lazy))
|
|
316
361
|
return c
|
|
317
362
|
return decorator(cls) if cls else decorator
|
|
318
363
|
|
pico_ioc/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = '0.
|
|
1
|
+
__version__ = '0.4.0'
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pico-ioc
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: A minimalist, zero-dependency Inversion of Control (IoC) container for Python.
|
|
5
|
+
Author-email: David Perez Cabrera <dperezcabrera@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/dperezcabrera/pico-ioc
|
|
7
|
+
Project-URL: Repository, https://github.com/dperezcabrera/pico-ioc
|
|
8
|
+
Project-URL: Issue Tracker, https://github.com/dperezcabrera/pico-ioc/issues
|
|
9
|
+
Keywords: ioc,di,dependency injection,inversion of control,decorator
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# 📦 Pico-IoC: A Minimalist IoC Container for Python
|
|
25
|
+
|
|
26
|
+
[](https://pypi.org/project/pico-ioc/)
|
|
27
|
+
[](https://opensource.org/licenses/MIT)
|
|
28
|
+

|
|
29
|
+
|
|
30
|
+
**Pico-IoC** is a tiny, zero-dependency, decorator-based Inversion of Control container for Python.
|
|
31
|
+
Build loosely-coupled, testable apps without manual wiring. Inspired by the Spring ecosystem.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## ✨ Key Features
|
|
36
|
+
|
|
37
|
+
* **Zero dependencies** — pure Python.
|
|
38
|
+
* **Decorator API** — `@component`, `@factory_component`, `@provides`.
|
|
39
|
+
* **Auto discovery** — scans a package and registers components.
|
|
40
|
+
* **Eager by default, fail-fast** — non-lazy bindings are instantiated immediately after `init()`. Missing deps fail startup.
|
|
41
|
+
* **Opt-in lazy** — set `lazy=True` to defer creation (wrapped in `ComponentProxy`).
|
|
42
|
+
* **Factories** — encapsulate complex creation logic.
|
|
43
|
+
* **Smart resolution** — by **parameter name**, then **type annotation**, then **MRO fallback**, then **string(name)**.
|
|
44
|
+
* **Re-entrancy guard** — prevents `get()` during scanning.
|
|
45
|
+
* **Auto-exclude caller** — `init()` skips the calling module to avoid double scanning.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 📦 Installation
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install pico-ioc
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 🚀 Quick Start
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from pico_ioc import component, init
|
|
61
|
+
|
|
62
|
+
@component
|
|
63
|
+
class AppConfig:
|
|
64
|
+
def get_db_url(self):
|
|
65
|
+
return "postgresql://user:pass@host/db"
|
|
66
|
+
|
|
67
|
+
@component
|
|
68
|
+
class DatabaseService:
|
|
69
|
+
def __init__(self, config: AppConfig):
|
|
70
|
+
self._cs = config.get_db_url()
|
|
71
|
+
def get_data(self):
|
|
72
|
+
return f"Data from {self._cs}"
|
|
73
|
+
|
|
74
|
+
container = init(__name__) # blueprint runs here (eager + fail-fast)
|
|
75
|
+
db = container.get(DatabaseService)
|
|
76
|
+
print(db.get_data())
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 🧩 Custom Component Keys
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from pico_ioc import component, init
|
|
85
|
+
|
|
86
|
+
@component(name="config") # custom key
|
|
87
|
+
class AppConfig:
|
|
88
|
+
db_url = "postgresql://user:pass@localhost/db"
|
|
89
|
+
|
|
90
|
+
@component
|
|
91
|
+
class Repository:
|
|
92
|
+
def __init__(self, config: "config"): # resolve by NAME
|
|
93
|
+
self.url = config.db_url
|
|
94
|
+
|
|
95
|
+
container = init(__name__)
|
|
96
|
+
print(container.get("config").db_url)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 🏭 Factories and `@provides`
|
|
102
|
+
|
|
103
|
+
* Default is **eager** (`lazy=False`). Eager bindings are constructed at the end of `init()`.
|
|
104
|
+
* Use `lazy=True` for on-first-use creation via `ComponentProxy`.
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from pico_ioc import factory_component, provides, init
|
|
108
|
+
|
|
109
|
+
COUNTER = {"value": 0}
|
|
110
|
+
|
|
111
|
+
@factory_component
|
|
112
|
+
class ServicesFactory:
|
|
113
|
+
@provides(key="heavy_service", lazy=True)
|
|
114
|
+
def heavy(self):
|
|
115
|
+
COUNTER["value"] += 1
|
|
116
|
+
return {"payload": "hello"}
|
|
117
|
+
|
|
118
|
+
container = init(__name__)
|
|
119
|
+
svc = container.get("heavy_service") # not created yet
|
|
120
|
+
print(COUNTER["value"]) # 0
|
|
121
|
+
print(svc["payload"]) # triggers creation
|
|
122
|
+
print(COUNTER["value"]) # 1
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## 🧠 Dependency Resolution Order
|
|
128
|
+
|
|
129
|
+
1. parameter **name**
|
|
130
|
+
2. exact **type annotation**
|
|
131
|
+
3. **MRO fallback** (walk base classes)
|
|
132
|
+
4. `str(name)`
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## ⚡ Eager vs. Lazy (Blueprint Behavior)
|
|
137
|
+
|
|
138
|
+
At the end of `init()`, Pico-IoC performs a **blueprint**:
|
|
139
|
+
|
|
140
|
+
- **Eager** (`lazy=False`, default): instantiated immediately; failures stop startup.
|
|
141
|
+
- **Lazy** (`lazy=True`): returns a `ComponentProxy`; instantiated on first real use.
|
|
142
|
+
|
|
143
|
+
**Lifecycle:**
|
|
144
|
+
|
|
145
|
+
┌───────────────────────┐
|
|
146
|
+
│ init() │
|
|
147
|
+
└───────────────────────┘
|
|
148
|
+
│
|
|
149
|
+
▼
|
|
150
|
+
┌───────────────────────┐
|
|
151
|
+
│ Scan & bind deps │
|
|
152
|
+
└───────────────────────┘
|
|
153
|
+
│
|
|
154
|
+
▼
|
|
155
|
+
┌─────────────────────────────┐
|
|
156
|
+
│ Blueprint instantiates all │
|
|
157
|
+
│ non-lazy (eager) beans │
|
|
158
|
+
└─────────────────────────────┘
|
|
159
|
+
│
|
|
160
|
+
┌───────────────────────┐
|
|
161
|
+
│ Container ready │
|
|
162
|
+
└───────────────────────┘
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
**Best practice:** keep eager+fail-fast for production parity with Spring; use lazy only for heavy/optional deps or to support negative tests.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## 🔄 Migration Guide (v0.2.1 → v0.3.0)
|
|
170
|
+
|
|
171
|
+
* **Defaults changed:** `@component` and `@provides` now default to `lazy=False` (eager).
|
|
172
|
+
* **Proxy renamed:** `LazyProxy` → `ComponentProxy` (only relevant if referenced directly).
|
|
173
|
+
* **Tests/fixtures:** components intentionally missing deps should be marked `@component(lazy=True)` (to avoid failing `init()`), or excluded from the scan.
|
|
174
|
+
|
|
175
|
+
Example fix for an intentional failure case:
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
@component(lazy=True)
|
|
179
|
+
class MissingDep:
|
|
180
|
+
def __init__(self, missing):
|
|
181
|
+
self.missing = missing
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## 🛠 API Reference
|
|
187
|
+
|
|
188
|
+
### `init(root, *, exclude=None, auto_exclude_caller=True) -> PicoContainer`
|
|
189
|
+
|
|
190
|
+
Scan and bind components in `root` (str module name or module).
|
|
191
|
+
Skips the calling module if `auto_exclude_caller=True`.
|
|
192
|
+
Runs blueprint (instantiate all `lazy=False` bindings).
|
|
193
|
+
|
|
194
|
+
### `@component(cls=None, *, name=None, lazy=False)`
|
|
195
|
+
|
|
196
|
+
Register a class as a component.
|
|
197
|
+
Use `name` for a custom key.
|
|
198
|
+
Set `lazy=True` to defer creation.
|
|
199
|
+
|
|
200
|
+
### `@factory_component`
|
|
201
|
+
|
|
202
|
+
Mark a class as a component factory (its methods can `@provides` bindings).
|
|
203
|
+
|
|
204
|
+
### `@provides(key, *, lazy=False)`
|
|
205
|
+
|
|
206
|
+
Declare that a factory method provides a component under `key`.
|
|
207
|
+
Set `lazy=True` for deferred creation (`ComponentProxy`).
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## 🧪 Testing
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
pip install tox
|
|
215
|
+
tox -e py311
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Tip: for “missing dependency” tests, mark those components as `lazy=True` so `init()` remains fail-fast for real components while your test still asserts failure on resolution.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## 🔌 Extensibility: Plugins, Binder, and Lifecycle Hooks
|
|
223
|
+
|
|
224
|
+
From `v0.4.0` onward, Pico-IoC can be cleanly extended without patching the core.
|
|
225
|
+
This enables optional integration layers like `pico-ioc-rest` for Flask, FastAPI, etc., while keeping the core dependency-free.
|
|
226
|
+
|
|
227
|
+
### Plugin Protocol
|
|
228
|
+
|
|
229
|
+
A plugin is any object that implements some or all of the following methods:
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
from pico_ioc import PicoPlugin, Binder
|
|
233
|
+
|
|
234
|
+
class MyPlugin:
|
|
235
|
+
def before_scan(self, package, binder: Binder): ...
|
|
236
|
+
def visit_class(self, module, cls, binder: Binder): ...
|
|
237
|
+
def after_scan(self, package, binder: Binder): ...
|
|
238
|
+
def after_bind(self, container, binder: Binder): ...
|
|
239
|
+
def before_eager(self, container, binder: Binder): ...
|
|
240
|
+
def after_ready(self, container, binder: Binder): ...
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
All hooks are optional. If present, they are called in this order during `init()`:
|
|
244
|
+
|
|
245
|
+
1. **before\_scan** — called before package scanning starts.
|
|
246
|
+
2. **visit\_class** — called for every class discovered during scanning.
|
|
247
|
+
3. **after\_scan** — called after scanning all modules.
|
|
248
|
+
4. **after\_bind** — called after the core has bound all components/factories.
|
|
249
|
+
5. **before\_eager** — called right before eager (non-lazy) instantiation.
|
|
250
|
+
6. **after\_ready** — called after all eager instantiation is complete.
|
|
251
|
+
|
|
252
|
+
### Binder API
|
|
253
|
+
|
|
254
|
+
Plugins receive a [`Binder`](#binder-api) instance in each hook, allowing them to:
|
|
255
|
+
|
|
256
|
+
* **bind**: register new providers in the container.
|
|
257
|
+
* **has**: check if a binding exists.
|
|
258
|
+
* **get**: resolve a binding immediately.
|
|
259
|
+
|
|
260
|
+
Example plugin that binds a “marker” component when a certain class is discovered:
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
class MarkerPlugin:
|
|
264
|
+
def visit_class(self, module, cls, binder):
|
|
265
|
+
if cls.__name__ == "SpecialService" and not binder.has("marker"):
|
|
266
|
+
binder.bind("marker", lambda: {"ok": True}, lazy=False)
|
|
267
|
+
|
|
268
|
+
container = init("my_app", plugins=(MarkerPlugin(),))
|
|
269
|
+
assert container.get("marker") == {"ok": True}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Creating Extensions
|
|
273
|
+
|
|
274
|
+
With the plugin API, you can build separate packages like `pico-ioc-rest`:
|
|
275
|
+
|
|
276
|
+
```python
|
|
277
|
+
from pico_ioc import PicoPlugin, Binder, create_instance, resolve_param
|
|
278
|
+
from flask import Flask
|
|
279
|
+
|
|
280
|
+
class FlaskRestPlugin:
|
|
281
|
+
def __init__(self):
|
|
282
|
+
self.controllers = []
|
|
283
|
+
|
|
284
|
+
def visit_class(self, module, cls, binder: Binder):
|
|
285
|
+
if getattr(cls, "_is_controller", False):
|
|
286
|
+
self.controllers.append(cls)
|
|
287
|
+
|
|
288
|
+
def after_bind(self, container, binder: Binder):
|
|
289
|
+
app: Flask = container.get(Flask)
|
|
290
|
+
for ctl_cls in self.controllers:
|
|
291
|
+
ctl = create_instance(ctl_cls, container)
|
|
292
|
+
# register routes here using `resolve_param` for handler DI
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Public Helpers for Extensions
|
|
296
|
+
|
|
297
|
+
Plugins can reuse Pico-IoC’s DI logic without duplicating it:
|
|
298
|
+
|
|
299
|
+
* **`create_instance(cls, container)`** — instantiate a class with DI, respecting Pico-IoC’s resolution order.
|
|
300
|
+
* **`resolve_param(container, parameter)`** — resolve a single function/class parameter via Pico-IoC rules.
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## ❓ FAQ
|
|
305
|
+
|
|
306
|
+
**Q: Can I make the container lenient at startup?**
|
|
307
|
+
A: By design it’s strict. Prefer `lazy=True` on specific bindings or exclude problem modules from the scan.
|
|
308
|
+
|
|
309
|
+
**Q: Thread safety?**
|
|
310
|
+
A: Container uses `ContextVar` to guard re-entrancy during scanning. Singletons are created once per container; typical usage is in single-threaded app startup, then read-mostly.
|
|
311
|
+
|
|
312
|
+
**Q: Frameworks?**
|
|
313
|
+
A: Framework-agnostic. Works with Flask, FastAPI, CLIs, scripts, etc.
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## 📜 License
|
|
318
|
+
|
|
319
|
+
MIT — see [LICENSE](https://opensource.org/licenses/MIT)
|
|
320
|
+
|
|
321
|
+
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
pico_ioc/__init__.py,sha256=IqQOM75teE_gplSaemvxrfZjjj3w8-PBoRWRzFtZsz4,15219
|
|
2
|
+
pico_ioc/_version.py,sha256=2eiWQI55fd-roDdkt4Hvl9WzrTJ4xQo33VzFud6D03U,22
|
|
3
|
+
pico_ioc-0.4.0.dist-info/METADATA,sha256=vpgj38emsZxoJoGcW8-CWBt5K59U8cUF2GZrCzGy2ss,10733
|
|
4
|
+
pico_ioc-0.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
5
|
+
pico_ioc-0.4.0.dist-info/top_level.txt,sha256=_7_RLu616z_dtRw16impXn4Mw8IXe2J4BeX5912m5dQ,9
|
|
6
|
+
pico_ioc-0.4.0.dist-info/RECORD,,
|
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: pico-ioc
|
|
3
|
-
Version: 0.3.0
|
|
4
|
-
Summary: A minimalist, zero-dependency Inversion of Control (IoC) container for Python.
|
|
5
|
-
Author-email: David Perez Cabrera <dperezcabrera@gmail.com>
|
|
6
|
-
Project-URL: Homepage, https://github.com/dperezcabrera/pico-ioc
|
|
7
|
-
Project-URL: Repository, https://github.com/dperezcabrera/pico-ioc
|
|
8
|
-
Project-URL: Issue Tracker, https://github.com/dperezcabrera/pico-ioc/issues
|
|
9
|
-
Keywords: ioc,di,dependency injection,inversion of control,decorator
|
|
10
|
-
Classifier: Development Status :: 4 - Beta
|
|
11
|
-
Classifier: Programming Language :: Python :: 3
|
|
12
|
-
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
20
|
-
Classifier: Operating System :: OS Independent
|
|
21
|
-
Requires-Python: >=3.8
|
|
22
|
-
Description-Content-Type: text/markdown
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
# 📦 Pico-IoC: A Minimalist IoC Container for Python
|
|
26
|
-
|
|
27
|
-
[](https://pypi.org/project/pico-ioc/)
|
|
28
|
-
[](https://opensource.org/licenses/MIT)
|
|
29
|
-

|
|
30
|
-
|
|
31
|
-
**Pico-IoC** is a tiny, zero-dependency, decorator-based Inversion of Control (IoC) container for Python.
|
|
32
|
-
It helps you manage dependencies in a clean, intuitive, and *Pythonic* way.
|
|
33
|
-
|
|
34
|
-
The core idea is to let you build loosely coupled, easily testable applications without manually wiring components.
|
|
35
|
-
*Inspired by the IoC philosophy popularized by the Spring Framework.*
|
|
36
|
-
|
|
37
|
-
---
|
|
38
|
-
|
|
39
|
-
## ✨ Key Features
|
|
40
|
-
|
|
41
|
-
* **Zero Dependencies:** Pure Python, no external libraries.
|
|
42
|
-
* **Decorator-Based API:** Simple decorators like `@component` and `@provides`.
|
|
43
|
-
* **Automatic Discovery:** Scans your package to auto-register components.
|
|
44
|
-
* **Lazy Instantiation:** Objects are created on first use.
|
|
45
|
-
* **Flexible Factories:** Encapsulate complex creation logic.
|
|
46
|
-
* **Framework-Agnostic:** Works with Flask, FastAPI, CLIs, scripts, etc.
|
|
47
|
-
* **Smart Dependency Resolution:** Resolves by **parameter name**, then **type annotation**, then **MRO fallback**.
|
|
48
|
-
* **Auto-Exclude Caller:** `init()` automatically skips the calling module to avoid double-initialization during scans.
|
|
49
|
-
|
|
50
|
-
---
|
|
51
|
-
|
|
52
|
-
## 📦 Installation
|
|
53
|
-
|
|
54
|
-
```bash
|
|
55
|
-
pip install pico-ioc
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
---
|
|
59
|
-
|
|
60
|
-
## 🚀 Quick Start
|
|
61
|
-
|
|
62
|
-
```python
|
|
63
|
-
from pico_ioc import component, init
|
|
64
|
-
|
|
65
|
-
@component
|
|
66
|
-
class AppConfig:
|
|
67
|
-
def get_db_url(self):
|
|
68
|
-
return "postgresql://user:pass@host/db"
|
|
69
|
-
|
|
70
|
-
@component
|
|
71
|
-
class DatabaseService:
|
|
72
|
-
def __init__(self, config: AppConfig):
|
|
73
|
-
self._cs = config.get_db_url()
|
|
74
|
-
|
|
75
|
-
def get_data(self):
|
|
76
|
-
return f"Data from {self._cs}"
|
|
77
|
-
|
|
78
|
-
# Initialize the container scanning the current module
|
|
79
|
-
container = init(__name__)
|
|
80
|
-
|
|
81
|
-
db = container.get(DatabaseService)
|
|
82
|
-
print(db.get_data()) # Data from postgresql://user:pass@host/db
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
---
|
|
86
|
-
|
|
87
|
-
## 🧩 Custom Component Keys
|
|
88
|
-
|
|
89
|
-
You can register a component with a **custom key** (string, class, enum…).
|
|
90
|
-
`key=` is the preferred syntax. For backwards compatibility, `name=` still works.
|
|
91
|
-
|
|
92
|
-
```python
|
|
93
|
-
from pico_ioc import component, init
|
|
94
|
-
|
|
95
|
-
@component(name="config") # still supported for legacy code
|
|
96
|
-
class AppConfig:
|
|
97
|
-
def __init__(self):
|
|
98
|
-
self.db_url = "postgresql://user:pass@localhost/db"
|
|
99
|
-
|
|
100
|
-
@component
|
|
101
|
-
class Repository:
|
|
102
|
-
def __init__(self, config: "config"): # resolve by name
|
|
103
|
-
self._url = config.db_url
|
|
104
|
-
|
|
105
|
-
container = init(__name__)
|
|
106
|
-
repo = container.get(Repository)
|
|
107
|
-
print(repo._url) # postgresql://user:pass@localhost/db
|
|
108
|
-
print(container.get("config").db_url)
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
---
|
|
112
|
-
|
|
113
|
-
## 🏭 Factory Components and `@provides`
|
|
114
|
-
|
|
115
|
-
Factories can provide components under a specific **key**.
|
|
116
|
-
Default is lazy creation (via `LazyProxy`).
|
|
117
|
-
|
|
118
|
-
```python
|
|
119
|
-
from pico_ioc import factory_component, provides, init
|
|
120
|
-
|
|
121
|
-
CREATION_COUNTER = {"value": 0}
|
|
122
|
-
|
|
123
|
-
@factory_component
|
|
124
|
-
class ServicesFactory:
|
|
125
|
-
@provides(key="heavy_service") # preferred
|
|
126
|
-
def make_heavy(self):
|
|
127
|
-
CREATION_COUNTER["value"] += 1
|
|
128
|
-
return {"payload": "Hello from heavy service"}
|
|
129
|
-
|
|
130
|
-
container = init(__name__)
|
|
131
|
-
svc = container.get("heavy_service")
|
|
132
|
-
print(CREATION_COUNTER["value"]) # 0 (not created yet)
|
|
133
|
-
|
|
134
|
-
print(svc["payload"]) # triggers creation
|
|
135
|
-
print(CREATION_COUNTER["value"]) # 1
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
---
|
|
139
|
-
|
|
140
|
-
## 📦 Project-Style Scanning
|
|
141
|
-
|
|
142
|
-
```
|
|
143
|
-
project_root/
|
|
144
|
-
└── myapp/
|
|
145
|
-
├── __init__.py
|
|
146
|
-
├── services.py
|
|
147
|
-
└── main.py
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
**myapp/services.py**
|
|
151
|
-
|
|
152
|
-
```python
|
|
153
|
-
from pico_ioc import component
|
|
154
|
-
|
|
155
|
-
@component
|
|
156
|
-
class Config:
|
|
157
|
-
def __init__(self):
|
|
158
|
-
self.base_url = "https://api.example.com"
|
|
159
|
-
|
|
160
|
-
@component
|
|
161
|
-
class ApiClient:
|
|
162
|
-
def __init__(self, config: Config):
|
|
163
|
-
self.base_url = config.base_url
|
|
164
|
-
|
|
165
|
-
def get(self, path: str):
|
|
166
|
-
return f"GET {self.base_url}/{path}"
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
**myapp/main.py**
|
|
170
|
-
|
|
171
|
-
```python
|
|
172
|
-
import pico_ioc
|
|
173
|
-
from myapp.services import ApiClient
|
|
174
|
-
|
|
175
|
-
container = pico_ioc.init("myapp")
|
|
176
|
-
client = container.get(ApiClient)
|
|
177
|
-
print(client.get("status")) # GET https://api.example.com/status
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
---
|
|
181
|
-
|
|
182
|
-
## 🧠 Dependency Resolution Order
|
|
183
|
-
|
|
184
|
-
When Pico-IoC instantiates a component, it tries to resolve each parameter in this order:
|
|
185
|
-
|
|
186
|
-
1. **Exact parameter name** (string key in container)
|
|
187
|
-
2. **Exact type annotation** (class key in container)
|
|
188
|
-
3. **MRO fallback** (walk base classes until match)
|
|
189
|
-
4. **String version** of the parameter name
|
|
190
|
-
|
|
191
|
-
---
|
|
192
|
-
|
|
193
|
-
## 🛠 API Reference
|
|
194
|
-
|
|
195
|
-
### `init(root_package_or_module, *, exclude=None, auto_exclude_caller=True) -> PicoContainer`
|
|
196
|
-
|
|
197
|
-
Scan the given root **package** (str) or **module**.
|
|
198
|
-
By default, excludes the calling module.
|
|
199
|
-
|
|
200
|
-
### `@component(cls=None, *, name=None)`
|
|
201
|
-
|
|
202
|
-
Register a class as a component.
|
|
203
|
-
If `name` is given, registers under that string; otherwise under the class type.
|
|
204
|
-
|
|
205
|
-
### `@factory_component`
|
|
206
|
-
|
|
207
|
-
Register a class as a factory of components.
|
|
208
|
-
|
|
209
|
-
### `@provides(key=None, *, name=None, lazy=True)`
|
|
210
|
-
|
|
211
|
-
Declare that a factory method provides a component under `key`.
|
|
212
|
-
`name` is accepted for backwards compatibility.
|
|
213
|
-
If `lazy=True`, returns a `LazyProxy` that instantiates on first real use.
|
|
214
|
-
|
|
215
|
-
---
|
|
216
|
-
|
|
217
|
-
## 🧪 Testing
|
|
218
|
-
|
|
219
|
-
```bash
|
|
220
|
-
pip install tox
|
|
221
|
-
tox -e py311
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
---
|
|
225
|
-
|
|
226
|
-
## 📜 License
|
|
227
|
-
|
|
228
|
-
MIT — see [LICENSE](https://opensource.org/licenses/MIT)
|
pico_ioc-0.3.0.dist-info/RECORD
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
pico_ioc/__init__.py,sha256=wCgi07l_0ZqhgZRfoRgWld_Q3_0qkqazUPWsO0XbPQI,13474
|
|
2
|
-
pico_ioc/_version.py,sha256=3wVEs2QD_7OcTlD97cZdCeizd2hUbJJ0GeIO8wQIjrk,22
|
|
3
|
-
pico_ioc-0.3.0.dist-info/METADATA,sha256=nzmDf0ZLTn0OkaoYDytJDnPQstVgVf2ExGZv0nMs15g,6374
|
|
4
|
-
pico_ioc-0.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
5
|
-
pico_ioc-0.3.0.dist-info/top_level.txt,sha256=_7_RLu616z_dtRw16impXn4Mw8IXe2J4BeX5912m5dQ,9
|
|
6
|
-
pico_ioc-0.3.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|