pico-ioc 0.2.1__py3-none-any.whl → 0.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pico_ioc/__init__.py +63 -121
- pico_ioc/_version.py +1 -1
- pico_ioc-0.3.1.dist-info/METADATA +239 -0
- pico_ioc-0.3.1.dist-info/RECORD +6 -0
- pico_ioc-0.2.1.dist-info/METADATA +0 -233
- pico_ioc-0.2.1.dist-info/RECORD +0 -6
- {pico_ioc-0.2.1.dist-info → pico_ioc-0.3.1.dist-info}/WHEEL +0 -0
- {pico_ioc-0.2.1.dist-info → pico_ioc-0.3.1.dist-info}/top_level.txt +0 -0
pico_ioc/__init__.py
CHANGED
|
@@ -1,63 +1,50 @@
|
|
|
1
1
|
import functools, inspect, pkgutil, importlib, logging
|
|
2
|
-
from typing import Callable, Any, Optional
|
|
2
|
+
from typing import Callable, Any, Optional, Dict
|
|
3
3
|
from contextvars import ContextVar
|
|
4
4
|
|
|
5
5
|
try:
|
|
6
|
-
# written at build time by setuptools-scm
|
|
7
6
|
from ._version import __version__
|
|
8
|
-
except Exception:
|
|
7
|
+
except Exception:
|
|
9
8
|
__version__ = "0.0.0"
|
|
10
9
|
|
|
11
10
|
__all__ = ["__version__"]
|
|
12
11
|
|
|
13
|
-
# ------------------------------------------------------------------------------
|
|
14
|
-
# Re-entrancy guards
|
|
15
|
-
# ------------------------------------------------------------------------------
|
|
16
|
-
# True while init/scan is running. Blocks userland container access during scan.
|
|
17
12
|
_scanning: ContextVar[bool] = ContextVar("pico_scanning", default=False)
|
|
18
|
-
|
|
19
|
-
# True while the container is resolving deps for a component (internal use allowed).
|
|
20
13
|
_resolving: ContextVar[bool] = ContextVar("pico_resolving", default=False)
|
|
21
14
|
|
|
22
|
-
|
|
23
|
-
# --- 1. Container and Chameleon Proxy (Framework-Agnostic) ---
|
|
24
|
-
# ==============================================================================
|
|
15
|
+
|
|
25
16
|
class PicoContainer:
|
|
26
17
|
def __init__(self):
|
|
27
|
-
self._providers:
|
|
28
|
-
self._singletons:
|
|
18
|
+
self._providers: Dict[Any, Dict[str, Any]] = {}
|
|
19
|
+
self._singletons: Dict[Any, Any] = {}
|
|
29
20
|
|
|
30
|
-
def bind(self, key: Any, provider: Callable[[], Any]):
|
|
31
|
-
self._providers[key] = provider
|
|
21
|
+
def bind(self, key: Any, provider: Callable[[], Any], *, lazy: bool):
|
|
22
|
+
self._providers[key] = {"factory": provider, "lazy": bool(lazy)}
|
|
32
23
|
|
|
33
24
|
def has(self, key: Any) -> bool:
|
|
34
25
|
return key in self._providers or key in self._singletons
|
|
35
26
|
|
|
36
27
|
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
28
|
if _scanning.get() and not _resolving.get():
|
|
40
29
|
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."
|
|
30
|
+
"pico-ioc: re-entrant container access during scan."
|
|
45
31
|
)
|
|
46
|
-
|
|
47
32
|
if key in self._singletons:
|
|
48
33
|
return self._singletons[key]
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
34
|
+
prov = self._providers.get(key)
|
|
35
|
+
if prov is None:
|
|
36
|
+
raise NameError(f"No provider found for key: {key}")
|
|
37
|
+
instance = prov["factory"]()
|
|
38
|
+
self._singletons[key] = instance
|
|
39
|
+
return instance
|
|
40
|
+
|
|
41
|
+
def _eager_instantiate_all(self):
|
|
42
|
+
for key, meta in list(self._providers.items()):
|
|
43
|
+
if not meta.get("lazy", False) and key not in self._singletons:
|
|
44
|
+
self.get(key)
|
|
45
|
+
|
|
54
46
|
|
|
55
|
-
class
|
|
56
|
-
"""
|
|
57
|
-
A full-fledged lazy proxy that delegates almost all operations
|
|
58
|
-
to the real object, which is created only on first access.
|
|
59
|
-
It is completely framework-agnostic.
|
|
60
|
-
"""
|
|
47
|
+
class ComponentProxy:
|
|
61
48
|
def __init__(self, object_creator: Callable[[], Any]):
|
|
62
49
|
object.__setattr__(self, "_object_creator", object_creator)
|
|
63
50
|
object.__setattr__(self, "__real_object", None)
|
|
@@ -69,30 +56,16 @@ class LazyProxy:
|
|
|
69
56
|
object.__setattr__(self, "__real_object", real_obj)
|
|
70
57
|
return real_obj
|
|
71
58
|
|
|
72
|
-
# --- Core Proxying and Representation ---
|
|
73
59
|
@property
|
|
74
60
|
def __class__(self):
|
|
75
61
|
return self._get_real_object().__class__
|
|
76
62
|
|
|
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 ---
|
|
63
|
+
def __getattr__(self, name): return getattr(self._get_real_object(), name)
|
|
64
|
+
def __setattr__(self, name, value): setattr(self._get_real_object(), name, value)
|
|
65
|
+
def __delattr__(self, name): delattr(self._get_real_object(), name)
|
|
66
|
+
def __str__(self): return str(self._get_real_object())
|
|
67
|
+
def __repr__(self): return repr(self._get_real_object())
|
|
68
|
+
def __dir__(self): return dir(self._get_real_object())
|
|
96
69
|
def __len__(self): return len(self._get_real_object())
|
|
97
70
|
def __getitem__(self, key): return self._get_real_object()[key]
|
|
98
71
|
def __setitem__(self, key, value): self._get_real_object()[key] = value
|
|
@@ -100,8 +73,6 @@ class LazyProxy:
|
|
|
100
73
|
def __iter__(self): return iter(self._get_real_object())
|
|
101
74
|
def __reversed__(self): return reversed(self._get_real_object())
|
|
102
75
|
def __contains__(self, item): return item in self._get_real_object()
|
|
103
|
-
|
|
104
|
-
# --- Emulation of numeric types and operators ---
|
|
105
76
|
def __add__(self, other): return self._get_real_object() + other
|
|
106
77
|
def __sub__(self, other): return self._get_real_object() - other
|
|
107
78
|
def __mul__(self, other): return self._get_real_object() * other
|
|
@@ -116,8 +87,6 @@ class LazyProxy:
|
|
|
116
87
|
def __and__(self, other): return self._get_real_object() & other
|
|
117
88
|
def __xor__(self, other): return self._get_real_object() ^ other
|
|
118
89
|
def __or__(self, other): return self._get_real_object() | other
|
|
119
|
-
|
|
120
|
-
# --- Right-hand side numeric operators ---
|
|
121
90
|
def __radd__(self, other): return other + self._get_real_object()
|
|
122
91
|
def __rsub__(self, other): return other - self._get_real_object()
|
|
123
92
|
def __rmul__(self, other): return other * self._get_real_object()
|
|
@@ -132,14 +101,10 @@ class LazyProxy:
|
|
|
132
101
|
def __rand__(self, other): return other & self._get_real_object()
|
|
133
102
|
def __rxor__(self, other): return other ^ self._get_real_object()
|
|
134
103
|
def __ror__(self, other): return other | self._get_real_object()
|
|
135
|
-
|
|
136
|
-
# --- Unary operators ---
|
|
137
104
|
def __neg__(self): return -self._get_real_object()
|
|
138
105
|
def __pos__(self): return +self._get_real_object()
|
|
139
106
|
def __abs__(self): return abs(self._get_real_object())
|
|
140
107
|
def __invert__(self): return ~self._get_real_object()
|
|
141
|
-
|
|
142
|
-
# --- Comparison operators ---
|
|
143
108
|
def __eq__(self, other): return self._get_real_object() == other
|
|
144
109
|
def __ne__(self, other): return self._get_real_object() != other
|
|
145
110
|
def __lt__(self, other): return self._get_real_object() < other
|
|
@@ -147,31 +112,20 @@ class LazyProxy:
|
|
|
147
112
|
def __gt__(self, other): return self._get_real_object() > other
|
|
148
113
|
def __ge__(self, other): return self._get_real_object() >= other
|
|
149
114
|
def __hash__(self): return hash(self._get_real_object())
|
|
150
|
-
|
|
151
|
-
# --- Truthiness, Callability and Context Management ---
|
|
152
115
|
def __bool__(self): return bool(self._get_real_object())
|
|
153
116
|
def __call__(self, *args, **kwargs): return self._get_real_object()(*args, **kwargs)
|
|
154
117
|
def __enter__(self): return self._get_real_object().__enter__()
|
|
155
118
|
def __exit__(self, exc_type, exc_val, exc_tb): return self._get_real_object().__exit__(exc_type, exc_val, exc_tb)
|
|
156
119
|
|
|
157
|
-
|
|
158
|
-
# --- 2. The Scanner and `init` Facade ---
|
|
159
|
-
# ==============================================================================
|
|
120
|
+
|
|
160
121
|
def _resolve_param(container: PicoContainer, p: inspect.Parameter) -> Any:
|
|
161
122
|
if p.name == 'self' or p.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
162
123
|
raise RuntimeError("Invalid param for resolution")
|
|
163
|
-
|
|
164
|
-
# 1) NAME
|
|
165
124
|
if container.has(p.name):
|
|
166
125
|
return container.get(p.name)
|
|
167
|
-
|
|
168
126
|
ann = p.annotation
|
|
169
|
-
|
|
170
|
-
# 2) TYPE
|
|
171
127
|
if ann is not inspect._empty and container.has(ann):
|
|
172
128
|
return container.get(ann)
|
|
173
|
-
|
|
174
|
-
# 3) TYPE MRO
|
|
175
129
|
if ann is not inspect._empty:
|
|
176
130
|
try:
|
|
177
131
|
for base in getattr(ann, "__mro__", ())[1:]:
|
|
@@ -181,27 +135,19 @@ def _resolve_param(container: PicoContainer, p: inspect.Parameter) -> Any:
|
|
|
181
135
|
return container.get(base)
|
|
182
136
|
except Exception:
|
|
183
137
|
pass
|
|
184
|
-
|
|
185
|
-
# 4) str(NAME)
|
|
186
138
|
if container.has(str(p.name)):
|
|
187
139
|
return container.get(str(p.name))
|
|
188
|
-
|
|
189
140
|
key = p.name if ann is inspect._empty else ann
|
|
190
141
|
return container.get(key)
|
|
191
142
|
|
|
192
143
|
|
|
193
|
-
def _scan_and_configure(
|
|
194
|
-
package_or_name,
|
|
195
|
-
container: PicoContainer,
|
|
196
|
-
exclude: Optional[Callable[[str], bool]] = None
|
|
197
|
-
):
|
|
144
|
+
def _scan_and_configure(package_or_name, container: PicoContainer, exclude: Optional[Callable[[str], bool]] = None):
|
|
198
145
|
package = importlib.import_module(package_or_name) if isinstance(package_or_name, str) else package_or_name
|
|
199
|
-
logging.info(f"
|
|
146
|
+
logging.info(f"Scanning in '{package.__name__}'...")
|
|
200
147
|
component_classes, factory_classes = [], []
|
|
201
|
-
|
|
202
148
|
for _, name, _ in pkgutil.walk_packages(package.__path__, package.__name__ + '.'):
|
|
203
149
|
if exclude and exclude(name):
|
|
204
|
-
logging.info(f"
|
|
150
|
+
logging.info(f"Skipping module {name} (excluded)")
|
|
205
151
|
continue
|
|
206
152
|
try:
|
|
207
153
|
module = importlib.import_module(name)
|
|
@@ -211,23 +157,27 @@ def _scan_and_configure(
|
|
|
211
157
|
elif getattr(obj, '_is_factory_component', False):
|
|
212
158
|
factory_classes.append(obj)
|
|
213
159
|
except Exception as e:
|
|
214
|
-
logging.warning(f"
|
|
215
|
-
|
|
216
|
-
# Register factories
|
|
160
|
+
logging.warning(f"Module {name} not processed: {e}")
|
|
217
161
|
for factory_cls in factory_classes:
|
|
218
162
|
try:
|
|
219
163
|
sig = inspect.signature(factory_cls.__init__)
|
|
220
164
|
instance = factory_cls(container) if 'container' in sig.parameters else factory_cls()
|
|
221
165
|
for _, method in inspect.getmembers(instance, inspect.ismethod):
|
|
222
166
|
if hasattr(method, '_provides_name'):
|
|
223
|
-
|
|
167
|
+
key = getattr(method, '_provides_name')
|
|
168
|
+
is_lazy = bool(getattr(method, '_pico_lazy', False))
|
|
169
|
+
def make_provider(m=method, lazy=is_lazy):
|
|
170
|
+
def _factory():
|
|
171
|
+
if lazy:
|
|
172
|
+
return ComponentProxy(lambda: m())
|
|
173
|
+
return m()
|
|
174
|
+
return _factory
|
|
175
|
+
container.bind(key, make_provider(), lazy=is_lazy)
|
|
224
176
|
except Exception as e:
|
|
225
|
-
logging.error(f"
|
|
226
|
-
|
|
227
|
-
# Register components
|
|
177
|
+
logging.error(f"Error in factory {factory_cls.__name__}: {e}", exc_info=True)
|
|
228
178
|
for component_cls in component_classes:
|
|
229
179
|
key = getattr(component_cls, '_component_key', component_cls)
|
|
230
|
-
|
|
180
|
+
is_lazy = bool(getattr(component_cls, '_component_lazy', False))
|
|
231
181
|
def create_component(cls=component_cls):
|
|
232
182
|
sig = inspect.signature(cls.__init__)
|
|
233
183
|
deps = {}
|
|
@@ -240,21 +190,22 @@ def _scan_and_configure(
|
|
|
240
190
|
finally:
|
|
241
191
|
_resolving.reset(tok)
|
|
242
192
|
return cls(**deps)
|
|
193
|
+
def provider_factory(lazy=is_lazy, maker=create_component):
|
|
194
|
+
def _factory():
|
|
195
|
+
if lazy:
|
|
196
|
+
return ComponentProxy(maker)
|
|
197
|
+
return maker()
|
|
198
|
+
return _factory
|
|
199
|
+
container.bind(key, provider_factory(), lazy=is_lazy)
|
|
243
200
|
|
|
244
|
-
container.bind(key, create_component)
|
|
245
201
|
|
|
246
202
|
_container = None
|
|
247
203
|
|
|
204
|
+
|
|
248
205
|
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
|
-
"""
|
|
254
206
|
global _container
|
|
255
207
|
if _container:
|
|
256
208
|
return _container
|
|
257
|
-
|
|
258
209
|
combined_exclude = exclude
|
|
259
210
|
if auto_exclude_caller:
|
|
260
211
|
try:
|
|
@@ -263,7 +214,6 @@ def init(root_package, *, exclude: Optional[Callable[[str], bool]] = None, auto_
|
|
|
263
214
|
caller_name = getattr(caller_module, "__name__", None)
|
|
264
215
|
except Exception:
|
|
265
216
|
caller_name = None
|
|
266
|
-
|
|
267
217
|
if caller_name:
|
|
268
218
|
if combined_exclude is None:
|
|
269
219
|
def combined_exclude(mod: str, _caller=caller_name):
|
|
@@ -272,47 +222,39 @@ def init(root_package, *, exclude: Optional[Callable[[str], bool]] = None, auto_
|
|
|
272
222
|
prev = combined_exclude
|
|
273
223
|
def combined_exclude(mod: str, _caller=caller_name, _prev=prev):
|
|
274
224
|
return mod == _caller or _prev(mod)
|
|
275
|
-
|
|
276
225
|
_container = PicoContainer()
|
|
277
|
-
logging.info("
|
|
278
|
-
|
|
226
|
+
logging.info("Initializing pico-ioc...")
|
|
279
227
|
tok = _scanning.set(True)
|
|
280
228
|
try:
|
|
281
229
|
_scan_and_configure(root_package, _container, exclude=combined_exclude)
|
|
282
230
|
finally:
|
|
283
231
|
_scanning.reset(tok)
|
|
284
|
-
|
|
285
|
-
logging.info("
|
|
232
|
+
_container._eager_instantiate_all()
|
|
233
|
+
logging.info("Container configured and ready.")
|
|
286
234
|
return _container
|
|
287
235
|
|
|
288
|
-
|
|
289
|
-
# --- 3. The Decorators ---
|
|
290
|
-
# ==============================================================================
|
|
236
|
+
|
|
291
237
|
def factory_component(cls):
|
|
292
238
|
setattr(cls, '_is_factory_component', True)
|
|
293
239
|
return cls
|
|
294
240
|
|
|
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
|
-
"""
|
|
241
|
+
|
|
242
|
+
def provides(key: Any, *, lazy: bool = False):
|
|
300
243
|
def decorator(func):
|
|
301
244
|
@functools.wraps(func)
|
|
302
245
|
def wrapper(*args, **kwargs):
|
|
303
|
-
return
|
|
304
|
-
setattr(wrapper, '_provides_name', key)
|
|
246
|
+
return func(*args, **kwargs)
|
|
247
|
+
setattr(wrapper, '_provides_name', key)
|
|
248
|
+
setattr(wrapper, '_pico_lazy', bool(lazy))
|
|
305
249
|
return wrapper
|
|
306
250
|
return decorator
|
|
307
251
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
Mark a class as a component. Registered by class type by default,
|
|
311
|
-
or by 'name' if provided.
|
|
312
|
-
"""
|
|
252
|
+
|
|
253
|
+
def component(cls=None, *, name: Any = None, lazy: bool = False):
|
|
313
254
|
def decorator(c):
|
|
314
255
|
setattr(c, '_is_component', True)
|
|
315
256
|
setattr(c, '_component_key', name if name is not None else c)
|
|
257
|
+
setattr(c, '_component_lazy', bool(lazy))
|
|
316
258
|
return c
|
|
317
259
|
return decorator(cls) if cls else decorator
|
|
318
260
|
|
pico_ioc/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = '0.
|
|
1
|
+
__version__ = '0.3.1'
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pico-ioc
|
|
3
|
+
Version: 0.3.1
|
|
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
|
+
## ❓ FAQ
|
|
223
|
+
|
|
224
|
+
**Q: Can I make the container lenient at startup?**
|
|
225
|
+
A: By design it’s strict. Prefer `lazy=True` on specific bindings or exclude problem modules from the scan.
|
|
226
|
+
|
|
227
|
+
**Q: Thread safety?**
|
|
228
|
+
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.
|
|
229
|
+
|
|
230
|
+
**Q: Frameworks?**
|
|
231
|
+
A: Framework-agnostic. Works with Flask, FastAPI, CLIs, scripts, etc.
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## 📜 License
|
|
236
|
+
|
|
237
|
+
MIT — see [LICENSE](https://opensource.org/licenses/MIT)
|
|
238
|
+
|
|
239
|
+
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
pico_ioc/__init__.py,sha256=8VKuQM7hgXNrkPNMnpZ8ixg6LK4DpKCU-hvSSidGzEU,11874
|
|
2
|
+
pico_ioc/_version.py,sha256=TZkGuMIRSRmUY3XCIs5owt2o60vXyqYMHWIkhx65uYE,22
|
|
3
|
+
pico_ioc-0.3.1.dist-info/METADATA,sha256=oR-ft50RlnmX5BB_bFOoq5tIuaVflb3GTGXU9rI5oFM,7751
|
|
4
|
+
pico_ioc-0.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
5
|
+
pico_ioc-0.3.1.dist-info/top_level.txt,sha256=_7_RLu616z_dtRw16impXn4Mw8IXe2J4BeX5912m5dQ,9
|
|
6
|
+
pico_ioc-0.3.1.dist-info/RECORD,,
|
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: pico-ioc
|
|
3
|
-
Version: 0.2.1
|
|
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)
|
|
229
|
-
|
|
230
|
-
---
|
|
231
|
-
|
|
232
|
-
¿Quieres que también te prepare **un ejemplo completo en el README** con `fast_model` y `BaseChatModel` para que quede documentado el nuevo orden de resolución? Así quedaría clarísimo para cualquiera que lo use.
|
|
233
|
-
|
pico_ioc-0.2.1.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=PmcQ2PI2oP8irnLtJLJby2YfW6sBvLAmL-VpABzTqwc,22
|
|
3
|
-
pico_ioc-0.2.1.dist-info/METADATA,sha256=xinAC7i03pGdzTtRDoSqm9Q-N2lbA-OLc7WQ0o_u5hY,6599
|
|
4
|
-
pico_ioc-0.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
5
|
-
pico_ioc-0.2.1.dist-info/top_level.txt,sha256=_7_RLu616z_dtRw16impXn4Mw8IXe2J4BeX5912m5dQ,9
|
|
6
|
-
pico_ioc-0.2.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|