pico-ioc 0.4.0__py3-none-any.whl → 0.5.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 +91 -46
- pico_ioc/_version.py +1 -1
- pico_ioc-0.5.1.dist-info/METADATA +284 -0
- pico_ioc-0.5.1.dist-info/RECORD +7 -0
- pico_ioc-0.5.1.dist-info/licenses/LICENSE +21 -0
- pico_ioc-0.4.0.dist-info/METADATA +0 -321
- pico_ioc-0.4.0.dist-info/RECORD +0 -6
- {pico_ioc-0.4.0.dist-info → pico_ioc-0.5.1.dist-info}/WHEEL +0 -0
- {pico_ioc-0.4.0.dist-info → pico_ioc-0.5.1.dist-info}/top_level.txt +0 -0
pico_ioc/__init__.py
CHANGED
|
@@ -186,18 +186,43 @@ def resolve_param(container: PicoContainer, p: inspect.Parameter) -> Any:
|
|
|
186
186
|
return container.get(key)
|
|
187
187
|
|
|
188
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
|
+
"""
|
|
189
202
|
sig = inspect.signature(cls.__init__)
|
|
190
|
-
deps = {}
|
|
203
|
+
deps: Dict[str, Any] = {}
|
|
204
|
+
|
|
191
205
|
tok = _resolving.set(True)
|
|
192
206
|
try:
|
|
193
207
|
for p in sig.parameters.values():
|
|
194
|
-
if p.name == 'self' or p.kind in (
|
|
208
|
+
if p.name == 'self' or p.kind in (
|
|
209
|
+
inspect.Parameter.VAR_POSITIONAL,
|
|
210
|
+
inspect.Parameter.VAR_KEYWORD,
|
|
211
|
+
):
|
|
195
212
|
continue
|
|
196
|
-
|
|
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
|
|
197
220
|
finally:
|
|
198
221
|
_resolving.reset(tok)
|
|
222
|
+
|
|
199
223
|
return cls(**deps)
|
|
200
224
|
|
|
225
|
+
|
|
201
226
|
@runtime_checkable
|
|
202
227
|
class PicoPlugin(Protocol):
|
|
203
228
|
def before_scan(self, package: Any, binder: Binder) -> None: ...
|
|
@@ -217,12 +242,14 @@ def _scan_and_configure(
|
|
|
217
242
|
package = importlib.import_module(package_or_name) if isinstance(package_or_name, str) else package_or_name
|
|
218
243
|
logging.info(f"Scanning in '{package.__name__}'...")
|
|
219
244
|
binder = Binder(container)
|
|
245
|
+
|
|
220
246
|
for pl in plugins:
|
|
221
247
|
try:
|
|
222
248
|
if hasattr(pl, "before_scan"):
|
|
223
249
|
pl.before_scan(package, binder)
|
|
224
250
|
except Exception:
|
|
225
251
|
logging.exception("Plugin before_scan failed")
|
|
252
|
+
|
|
226
253
|
component_classes, factory_classes = [], []
|
|
227
254
|
for _, name, _ in pkgutil.walk_packages(package.__path__, package.__name__ + '.'):
|
|
228
255
|
if exclude and exclude(name):
|
|
@@ -243,40 +270,84 @@ def _scan_and_configure(
|
|
|
243
270
|
factory_classes.append(obj)
|
|
244
271
|
except Exception as e:
|
|
245
272
|
logging.warning(f"Module {name} not processed: {e}")
|
|
273
|
+
|
|
246
274
|
for pl in plugins:
|
|
247
275
|
try:
|
|
248
276
|
if hasattr(pl, "after_scan"):
|
|
249
277
|
pl.after_scan(package, binder)
|
|
250
278
|
except Exception:
|
|
251
279
|
logging.exception("Plugin after_scan failed")
|
|
252
|
-
|
|
253
|
-
try:
|
|
254
|
-
sig = inspect.signature(factory_cls.__init__)
|
|
255
|
-
instance = factory_cls(container) if 'container' in sig.parameters else factory_cls()
|
|
256
|
-
for _, method in inspect.getmembers(instance, inspect.ismethod):
|
|
257
|
-
if hasattr(method, '_provides_name'):
|
|
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__}")
|
|
280
|
+
|
|
269
281
|
for component_cls in component_classes:
|
|
270
282
|
key = getattr(component_cls, '_component_key', component_cls)
|
|
271
283
|
is_lazy = bool(getattr(component_cls, '_component_lazy', False))
|
|
284
|
+
|
|
272
285
|
def provider_factory(lazy=is_lazy, cls=component_cls):
|
|
273
286
|
def _factory():
|
|
274
287
|
if lazy:
|
|
275
288
|
return ComponentProxy(lambda: create_instance(cls, container))
|
|
276
289
|
return create_instance(cls, container)
|
|
277
290
|
return _factory
|
|
291
|
+
|
|
278
292
|
container.bind(key, provider_factory(), lazy=is_lazy)
|
|
279
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
|
+
|
|
280
351
|
_container: Optional[PicoContainer] = None
|
|
281
352
|
|
|
282
353
|
def init(
|
|
@@ -335,29 +406,3 @@ def init(
|
|
|
335
406
|
logging.info("Container configured and ready.")
|
|
336
407
|
return _container
|
|
337
408
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
def factory_component(cls):
|
|
341
|
-
setattr(cls, '_is_factory_component', True)
|
|
342
|
-
return cls
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
def provides(key: Any, *, lazy: bool = False):
|
|
346
|
-
def decorator(func):
|
|
347
|
-
@functools.wraps(func)
|
|
348
|
-
def wrapper(*args, **kwargs):
|
|
349
|
-
return func(*args, **kwargs)
|
|
350
|
-
setattr(wrapper, '_provides_name', key)
|
|
351
|
-
setattr(wrapper, '_pico_lazy', bool(lazy))
|
|
352
|
-
return wrapper
|
|
353
|
-
return decorator
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
def component(cls=None, *, name: Any = None, lazy: bool = False):
|
|
357
|
-
def decorator(c):
|
|
358
|
-
setattr(c, '_is_component', True)
|
|
359
|
-
setattr(c, '_component_key', name if name is not None else c)
|
|
360
|
-
setattr(c, '_component_lazy', bool(lazy))
|
|
361
|
-
return c
|
|
362
|
-
return decorator(cls) if cls else decorator
|
|
363
|
-
|
pico_ioc/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = '0.
|
|
1
|
+
__version__ = '0.5.1'
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pico-ioc
|
|
3
|
+
Version: 0.5.1
|
|
4
|
+
Summary: A minimalist, zero-dependency Inversion of Control (IoC) container for Python.
|
|
5
|
+
Author-email: David Perez Cabrera <dperezcabrera@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 David Pérez Cabrera
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/dperezcabrera/pico-ioc
|
|
29
|
+
Project-URL: Repository, https://github.com/dperezcabrera/pico-ioc
|
|
30
|
+
Project-URL: Issue Tracker, https://github.com/dperezcabrera/pico-ioc/issues
|
|
31
|
+
Keywords: ioc,di,dependency injection,inversion of control,decorator
|
|
32
|
+
Classifier: Development Status :: 4 - Beta
|
|
33
|
+
Classifier: Programming Language :: Python :: 3
|
|
34
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
41
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
42
|
+
Classifier: Operating System :: OS Independent
|
|
43
|
+
Requires-Python: >=3.8
|
|
44
|
+
Description-Content-Type: text/markdown
|
|
45
|
+
License-File: LICENSE
|
|
46
|
+
Dynamic: license-file
|
|
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
|
+
# 📦 Pico-IoC: A Minimalist IoC Container for Python
|
|
55
|
+
|
|
56
|
+
[](https://pypi.org/project/pico-ioc/)
|
|
57
|
+
[](https://opensource.org/licenses/MIT)
|
|
58
|
+

|
|
59
|
+
[](https://codecov.io/gh/dperezcabrera/pico-ioc)
|
|
60
|
+
[](https://sonarcloud.io/summary/new_code?id=dperezcabrera_pico-ioc)
|
|
61
|
+
[](https://sonarcloud.io/summary/new_code?id=dperezcabrera_pico-ioc)
|
|
62
|
+
[](https://sonarcloud.io/summary/new_code?id=dperezcabrera_pico-ioc)
|
|
63
|
+
|
|
64
|
+
**Pico-IoC** is a tiny, zero-dependency, decorator-based Inversion of Control container for Python.
|
|
65
|
+
Build loosely-coupled, testable apps without manual wiring. Inspired by the Spring ecosystem.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## ✨ Key Features
|
|
70
|
+
|
|
71
|
+
* **Zero dependencies** — pure Python.
|
|
72
|
+
* **Decorator API** — `@component`, `@factory_component`, `@provides`.
|
|
73
|
+
* **Auto discovery** — scans a package and registers components.
|
|
74
|
+
* **Eager by default, fail-fast** — non-lazy bindings are instantiated immediately after `init()`. Missing deps fail startup.
|
|
75
|
+
* **Opt-in lazy** — set `lazy=True` to defer creation (wrapped in `ComponentProxy`).
|
|
76
|
+
* **Factories** — encapsulate complex creation logic.
|
|
77
|
+
* **Smart resolution order** — **parameter name** takes precedence over **type annotation**, then **MRO fallback**, then **string(name)**.
|
|
78
|
+
* **Re-entrancy guard** — prevents `get()` during scanning.
|
|
79
|
+
* **Auto-exclude caller** — `init()` skips the calling module to avoid double scanning.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 📦 Installation
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pip install pico-ioc
|
|
87
|
+
````
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 🚀 Quick Start
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from pico_ioc import component, init
|
|
95
|
+
|
|
96
|
+
@component
|
|
97
|
+
class AppConfig:
|
|
98
|
+
def get_db_url(self):
|
|
99
|
+
return "postgresql://user:pass@host/db"
|
|
100
|
+
|
|
101
|
+
@component
|
|
102
|
+
class DatabaseService:
|
|
103
|
+
def __init__(self, config: AppConfig):
|
|
104
|
+
self._cs = config.get_db_url()
|
|
105
|
+
def get_data(self):
|
|
106
|
+
return f"Data from {self._cs}"
|
|
107
|
+
|
|
108
|
+
container = init(__name__) # blueprint runs here (eager + fail-fast)
|
|
109
|
+
db = container.get(DatabaseService)
|
|
110
|
+
print(db.get_data())
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 🧩 Custom Component Keys
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from pico_ioc import component, init
|
|
119
|
+
|
|
120
|
+
@component(name="config") # custom key
|
|
121
|
+
class AppConfig:
|
|
122
|
+
db_url = "postgresql://user:pass@localhost/db"
|
|
123
|
+
|
|
124
|
+
@component
|
|
125
|
+
class Repository:
|
|
126
|
+
def __init__(self, config: "config"): # resolve by NAME
|
|
127
|
+
self.url = config.db_url
|
|
128
|
+
|
|
129
|
+
container = init(__name__)
|
|
130
|
+
print(container.get("config").db_url)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## 🏭 Factories and `@provides`
|
|
136
|
+
|
|
137
|
+
* Default is **eager** (`lazy=False`). Eager bindings are constructed at the end of `init()`.
|
|
138
|
+
* Use `lazy=True` for on-first-use creation via `ComponentProxy`.
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from pico_ioc import factory_component, provides, init
|
|
142
|
+
|
|
143
|
+
COUNTER = {"value": 0}
|
|
144
|
+
|
|
145
|
+
@factory_component
|
|
146
|
+
class ServicesFactory:
|
|
147
|
+
@provides(key="heavy_service", lazy=True)
|
|
148
|
+
def heavy(self):
|
|
149
|
+
COUNTER["value"] += 1
|
|
150
|
+
return {"payload": "hello"}
|
|
151
|
+
|
|
152
|
+
container = init(__name__)
|
|
153
|
+
svc = container.get("heavy_service") # not created yet
|
|
154
|
+
print(COUNTER["value"]) # 0
|
|
155
|
+
print(svc["payload"]) # triggers creation
|
|
156
|
+
print(COUNTER["value"]) # 1
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## 🧠 Dependency Resolution Order (Updated in v0.5.0)
|
|
162
|
+
|
|
163
|
+
Starting with **v0.5.0**, Pico-IoC enforces **name-first resolution**:
|
|
164
|
+
|
|
165
|
+
1. **Parameter name** (highest priority)
|
|
166
|
+
2. **Exact type annotation**
|
|
167
|
+
3. **MRO fallback** (walk base classes)
|
|
168
|
+
4. **String(name)**
|
|
169
|
+
|
|
170
|
+
This means that if a dependency could match both by name and type, **the name match wins**.
|
|
171
|
+
|
|
172
|
+
Example:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
from pico_ioc import component, factory_component, provides, init
|
|
176
|
+
|
|
177
|
+
class BaseType: ...
|
|
178
|
+
class Impl(BaseType): ...
|
|
179
|
+
|
|
180
|
+
@component(name="inject_by_name")
|
|
181
|
+
class InjectByName:
|
|
182
|
+
def __init__(self):
|
|
183
|
+
self.value = "by-name"
|
|
184
|
+
|
|
185
|
+
@factory_component
|
|
186
|
+
class NameVsTypeFactory:
|
|
187
|
+
@provides("choose", lazy=True)
|
|
188
|
+
def make(self, inject_by_name, hint: BaseType = None):
|
|
189
|
+
return inject_by_name.value
|
|
190
|
+
|
|
191
|
+
container = init(__name__)
|
|
192
|
+
assert container.get("choose") == "by-name"
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## ⚡ Eager vs. Lazy (Blueprint Behavior)
|
|
198
|
+
|
|
199
|
+
At the end of `init()`, Pico-IoC performs a **blueprint**:
|
|
200
|
+
|
|
201
|
+
* **Eager** (`lazy=False`, default): instantiated immediately; failures stop startup.
|
|
202
|
+
* **Lazy** (`lazy=True`): returns a `ComponentProxy`; instantiated on first real use.
|
|
203
|
+
|
|
204
|
+
**Lifecycle:**
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
┌───────────────────────┐
|
|
208
|
+
│ init() │
|
|
209
|
+
└───────────────────────┘
|
|
210
|
+
│
|
|
211
|
+
▼
|
|
212
|
+
┌───────────────────────┐
|
|
213
|
+
│ Scan & bind deps │
|
|
214
|
+
└───────────────────────┘
|
|
215
|
+
│
|
|
216
|
+
▼
|
|
217
|
+
┌─────────────────────────────┐
|
|
218
|
+
│ Blueprint instantiates all │
|
|
219
|
+
│ non-lazy (eager) beans │
|
|
220
|
+
└─────────────────────────────┘
|
|
221
|
+
│
|
|
222
|
+
┌───────────────────────┐
|
|
223
|
+
│ Container ready │
|
|
224
|
+
└───────────────────────┘
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## 🛠 API Reference
|
|
230
|
+
|
|
231
|
+
### `init(root, *, exclude=None, auto_exclude_caller=True) -> PicoContainer`
|
|
232
|
+
|
|
233
|
+
Scan and bind components in `root` (str module name or module).
|
|
234
|
+
Skips the calling module if `auto_exclude_caller=True`.
|
|
235
|
+
Runs blueprint (instantiate all `lazy=False` bindings).
|
|
236
|
+
|
|
237
|
+
### `@component(cls=None, *, name=None, lazy=False)`
|
|
238
|
+
|
|
239
|
+
Register a class as a component.
|
|
240
|
+
Use `name` for a custom key.
|
|
241
|
+
Set `lazy=True` to defer creation.
|
|
242
|
+
|
|
243
|
+
### `@factory_component`
|
|
244
|
+
|
|
245
|
+
Mark a class as a component factory (its methods can `@provides` bindings).
|
|
246
|
+
|
|
247
|
+
### `@provides(key, *, lazy=False)`
|
|
248
|
+
|
|
249
|
+
Declare that a factory method provides a component under `key`.
|
|
250
|
+
Set `lazy=True` for deferred creation (`ComponentProxy`).
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## 🧪 Testing
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
pip install tox
|
|
258
|
+
tox
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
**New in v0.5.0:**
|
|
262
|
+
Additional tests verify:
|
|
263
|
+
|
|
264
|
+
* Name vs. type precedence.
|
|
265
|
+
* Mixed binding key resolution in factories.
|
|
266
|
+
* Eager vs. lazy instantiation edge cases.
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## 🔌 Extensibility: Plugins, Binder, and Lifecycle Hooks
|
|
271
|
+
|
|
272
|
+
From `v0.4.0` onward, Pico-IoC can be cleanly extended without patching the core.
|
|
273
|
+
|
|
274
|
+
*(plugin API docs unchanged from before)*
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## 📜 License
|
|
279
|
+
|
|
280
|
+
MIT — see [LICENSE](https://opensource.org/licenses/MIT)
|
|
281
|
+
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
|
|
@@ -0,0 +1,7 @@
|
|
|
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,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 David Pérez Cabrera
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,321 +0,0 @@
|
|
|
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
|
-
|
pico_ioc-0.4.0.dist-info/RECORD
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
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,,
|
|
File without changes
|
|
File without changes
|