pico-ioc 0.3.0__tar.gz → 0.4.0__tar.gz

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