pico-ioc 1.0.0__tar.gz → 1.1.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.
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/.llm/ARCHITECTURE.md +28 -4
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/.llm/DECISIONS.md +17 -0
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/.llm/GUIDE.md +129 -14
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/.llm/OVERVIEW.md +21 -0
- pico_ioc-1.1.0/CHANGELOG.md +56 -0
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/PKG-INFO +22 -1
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/README.md +21 -0
- pico_ioc-1.1.0/src/pico_ioc/_version.py +1 -0
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/src/pico_ioc/api.py +23 -11
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/src/pico_ioc/container.py +3 -0
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/src/pico_ioc.egg-info/PKG-INFO +22 -1
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/src/pico_ioc.egg-info/SOURCES.txt +1 -0
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/tests/test_api_unit.py +74 -0
- pico_ioc-1.0.0/src/pico_ioc/_version.py +0 -1
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/.coveragerc +0 -0
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/.github/workflows/ci.yml +0 -0
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/.github/workflows/publish-to-pypi.yml +0 -0
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/LICENSE +0 -0
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/MANIFEST.in +0 -0
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/pyproject.toml +0 -0
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/setup.cfg +0 -0
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/src/pico_ioc/__init__.py +0 -0
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/src/pico_ioc/_state.py +0 -0
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/src/pico_ioc/decorators.py +0 -0
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/src/pico_ioc/plugins.py +0 -0
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/src/pico_ioc/proxy.py +0 -0
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/src/pico_ioc/public_api.py +0 -0
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/src/pico_ioc/resolver.py +0 -0
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/src/pico_ioc/scanner.py +0 -0
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/src/pico_ioc/typing_utils.py +0 -0
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/src/pico_ioc.egg-info/dependency_links.txt +0 -0
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/src/pico_ioc.egg-info/top_level.txt +0 -0
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/tests/test_container_get_all.py +0 -0
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/tests/test_container_unit.py +0 -0
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/tests/test_decorators_unit.py +0 -0
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/tests/test_pico_ioc.py +0 -0
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/tests/test_pico_ioc_additional.py +0 -0
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/tests/test_pico_ioc_discovery.py +0 -0
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/tests/test_proxy_unit.py +0 -0
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/tests/test_public_api.py +0 -0
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/tests/test_qualifiers_unit.py +0 -0
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/tests/test_resolver_unit.py +0 -0
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/tests/test_scanner_unit.py +0 -0
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/tests/test_typing_utils_unit.py +0 -0
- {pico_ioc-1.0.0 → pico_ioc-1.1.0}/tox.ini +0 -0
|
@@ -165,7 +165,9 @@ class Config:
|
|
|
165
165
|
|
|
166
166
|
## 10) Extensibility & overrides
|
|
167
167
|
|
|
168
|
-
|
|
168
|
+
### Test overrides (module-based)
|
|
169
|
+
|
|
170
|
+
Register a factory after the app module:
|
|
169
171
|
|
|
170
172
|
```python
|
|
171
173
|
@factory_component
|
|
@@ -173,7 +175,7 @@ class TestOverrides:
|
|
|
173
175
|
@provides(key=Repo) # same key as production
|
|
174
176
|
def provide_repo(self) -> Repo:
|
|
175
177
|
return FakeRepo()
|
|
176
|
-
|
|
178
|
+
````
|
|
177
179
|
|
|
178
180
|
Then:
|
|
179
181
|
|
|
@@ -181,7 +183,29 @@ Then:
|
|
|
181
183
|
c = init([app, test_overrides]) # overrides win by order
|
|
182
184
|
```
|
|
183
185
|
|
|
184
|
-
|
|
186
|
+
### Direct overrides argument
|
|
187
|
+
|
|
188
|
+
`init()` also accepts an `overrides` dict for ad-hoc replacement:
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
c = init(app, overrides={
|
|
192
|
+
Repo: FakeRepo(), # constant instance
|
|
193
|
+
"fast_model": lambda: {"mock": True}, # provider
|
|
194
|
+
"expensive": (lambda: object(), True), # provider with lazy=True
|
|
195
|
+
})
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
* **Applied before eager instantiation** → replaced providers never run.
|
|
199
|
+
* **Formats supported**:
|
|
200
|
+
|
|
201
|
+
* `key: instance` → constant
|
|
202
|
+
* `key: callable` → non-lazy provider
|
|
203
|
+
* `key: (callable, lazy_bool)` → provider with explicit laziness
|
|
204
|
+
* With `reuse=True`, calling `init(..., overrides=...)` again mutates the cached container bindings.
|
|
205
|
+
|
|
206
|
+
### Multi-env composition
|
|
207
|
+
|
|
208
|
+
Define `app.prod`, `app.dev`, `app.test` packages that import different provider sets, then call `init([app.prod])`, etc.
|
|
185
209
|
|
|
186
210
|
### Plugins
|
|
187
211
|
|
|
@@ -238,7 +262,7 @@ classDiagram
|
|
|
238
262
|
flowchart TD
|
|
239
263
|
A[Request instance for Type T] --> B{Cached?}
|
|
240
264
|
B -- yes --> Z[Return cached]
|
|
241
|
-
B -- no --> C[Find provider for T
|
|
265
|
+
B -- no --> C[Find provider for T: type / MRO / token]
|
|
242
266
|
C -- none --> E[Raise bootstrap error]
|
|
243
267
|
C -- found --> D[Resolve constructor args recursively]
|
|
244
268
|
D --> F[Instantiate]
|
|
@@ -75,6 +75,22 @@ Each entry has a rationale and implications. If a decision is revoked, it should
|
|
|
75
75
|
|
|
76
76
|
---
|
|
77
77
|
|
|
78
|
+
### 7. Overrides in `init()`
|
|
79
|
+
- **Decision**: `init(..., overrides={...})` allows binding custom providers/instances at bootstrap time.
|
|
80
|
+
- **Rationale**:
|
|
81
|
+
* Simplifies unit testing and ad-hoc mocking.
|
|
82
|
+
* Avoids creating dedicated override modules when only a few dependencies need to be replaced.
|
|
83
|
+
* Keeps semantics explicit: last binding wins.
|
|
84
|
+
- **Implications**:
|
|
85
|
+
* Overrides are applied **before eager instantiation** — ensuring replaced providers never run.
|
|
86
|
+
* Accepted formats:
|
|
87
|
+
- `key: instance` → constant binding
|
|
88
|
+
- `key: provider_callable` → non-lazy binding
|
|
89
|
+
- `key: (provider_callable, lazy_bool)` → binding with explicit laziness
|
|
90
|
+
* If `reuse=True`, calling `init(..., overrides=...)` again applies overrides on the cached container.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
78
94
|
## ❌ Won’t-Do Decisions
|
|
79
95
|
|
|
80
96
|
### 1. Alternative scopes (request/session)
|
|
@@ -115,4 +131,5 @@ Features that add complexity (alternative scopes, async providers, hot reload) a
|
|
|
115
131
|
- **2025-08**: Dropped Python 3.8/3.9 support, minimum 3.10.
|
|
116
132
|
- **2025-08**: Clarified resolution order as *name-first*.
|
|
117
133
|
- **2025-08**: Documented lifecycle, plugins, and fail-fast policy.
|
|
134
|
+
- **2025-09**: Added `init(..., overrides)` feature for test/mocking convenience.
|
|
118
135
|
|
|
@@ -12,7 +12,7 @@ This guide shows how to structure a Python app with **pico-ioc**: define compone
|
|
|
12
12
|
* **Component**: a class managed by the container. Use `@component`.
|
|
13
13
|
* **Factory component**: a class that *provides* concrete instances (e.g., `Flask()`, clients). Use `@factory_component`.
|
|
14
14
|
* **Provider**: a method on a factory that returns a dependency and declares its **key** (usually a type). Use `@provides(key=Type)` so consumers can request by type.
|
|
15
|
-
* **Container**: built via `pico_ioc.init(package_or_module)`. Resolve with `container.get(TypeOrClass)`.
|
|
15
|
+
* **Container**: built via `pico_ioc.init(package_or_module, ..., overrides=...)`. Resolve with `container.get(TypeOrClass)`.
|
|
16
16
|
|
|
17
17
|
Resolution rule of thumb: **ask by type** (e.g., `container.get(Flask)` or inject `def __init__(..., app: Flask)`).
|
|
18
18
|
|
|
@@ -156,10 +156,15 @@ class Runner:
|
|
|
156
156
|
|
|
157
157
|
## 5) Testing & overrides
|
|
158
158
|
|
|
159
|
+
You often want to replace real dependencies with fakes or mocks in tests.
|
|
160
|
+
There are two main patterns:
|
|
161
|
+
|
|
162
|
+
### 5.1 Composition modules
|
|
163
|
+
|
|
159
164
|
Define a **test factory** that provides fakes using the same keys:
|
|
160
165
|
|
|
161
166
|
```python
|
|
162
|
-
# tests/
|
|
167
|
+
# tests/test_overrides_module.py
|
|
163
168
|
from pico_ioc import factory_component, provides
|
|
164
169
|
from app.repo import Repo
|
|
165
170
|
|
|
@@ -172,27 +177,55 @@ class TestOverrides:
|
|
|
172
177
|
@provides(key=Repo)
|
|
173
178
|
def provide_repo(self) -> Repo:
|
|
174
179
|
return FakeRepo()
|
|
175
|
-
|
|
180
|
+
````
|
|
176
181
|
|
|
177
182
|
Build the container for tests with both packages (app + overrides):
|
|
178
183
|
|
|
179
184
|
```python
|
|
180
|
-
# tests/test_service.py
|
|
181
185
|
from pico_ioc import init
|
|
182
186
|
import app
|
|
183
|
-
from tests import
|
|
187
|
+
from tests import test_overrides_module
|
|
184
188
|
|
|
185
189
|
def test_service_fetch():
|
|
186
|
-
c = init([app,
|
|
190
|
+
c = init([app, test_overrides_module])
|
|
187
191
|
svc = c.get(app.service.Service)
|
|
188
192
|
assert svc.run() == "fake-data"
|
|
189
193
|
```
|
|
190
194
|
|
|
191
|
-
|
|
195
|
+
### 5.2 Direct `overrides` argument
|
|
196
|
+
|
|
197
|
+
`init()` also accepts an `overrides` dict for ad-hoc mocks. Each entry can be:
|
|
198
|
+
|
|
199
|
+
* **Instance** → bound as constant.
|
|
200
|
+
* **Callable** (0 args) → bound as provider, non-lazy.
|
|
201
|
+
* **(provider, lazy\_bool)** → provider with explicit laziness.
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
from pico_ioc import init
|
|
205
|
+
import app
|
|
206
|
+
|
|
207
|
+
fake_repo = object()
|
|
208
|
+
|
|
209
|
+
c = init(app, overrides={
|
|
210
|
+
app.repo.Repo: fake_repo, # constant
|
|
211
|
+
"fast_model": lambda: {"id": 123}, # provider
|
|
212
|
+
"expensive": (lambda: object(), True) # lazy provider
|
|
213
|
+
})
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
This is handy for **quick mocking in unit tests**:
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
def test_with_direct_overrides():
|
|
220
|
+
c = init(app, overrides={"fast_model": {"fake": True}}, reuse=False)
|
|
221
|
+
svc = c.get(app.service.Service)
|
|
222
|
+
assert svc.repo.fetch() == "fake-data"
|
|
223
|
+
```
|
|
192
224
|
|
|
225
|
+
> Note: if you call `init(..., reuse=True, overrides=...)` on an already built container, the overrides are applied on the cached container.
|
|
193
226
|
---
|
|
194
227
|
|
|
195
|
-
##
|
|
228
|
+
## 6) Qualifiers & collection injection
|
|
196
229
|
|
|
197
230
|
```python
|
|
198
231
|
from typing import Protocol, Annotated
|
|
@@ -218,14 +251,14 @@ class Orchestrator:
|
|
|
218
251
|
|
|
219
252
|
def run(self, s: str) -> list[str]:
|
|
220
253
|
return [h.handle("ok") for h in self.handlers]
|
|
221
|
-
```
|
|
222
254
|
|
|
223
|
-
|
|
224
|
-
If you request
|
|
255
|
+
```
|
|
256
|
+
If you request list[Handler] you get all implementations.
|
|
257
|
+
If you request list[Annotated[Handler, PAYMENTS]], you only get the tagged ones.
|
|
225
258
|
|
|
226
259
|
---
|
|
227
260
|
|
|
228
|
-
##
|
|
261
|
+
## 7) Plugins & Public API helper
|
|
229
262
|
|
|
230
263
|
```python
|
|
231
264
|
from pico_ioc import plugin
|
|
@@ -264,7 +297,7 @@ from app import Service, Config, TracingPlugin
|
|
|
264
297
|
|
|
265
298
|
---
|
|
266
299
|
|
|
267
|
-
##
|
|
300
|
+
## 8) Tips & guardrails
|
|
268
301
|
|
|
269
302
|
* **Ask by type**: inject `Flask`, `Config`, `Repo` instead of strings.
|
|
270
303
|
* **Keep constructors cheap**: do not perform I/O in `__init__`.
|
|
@@ -275,7 +308,7 @@ from app import Service, Config, TracingPlugin
|
|
|
275
308
|
|
|
276
309
|
---
|
|
277
310
|
|
|
278
|
-
##
|
|
311
|
+
## 9) Troubleshooting
|
|
279
312
|
|
|
280
313
|
* **“No provider for X”**
|
|
281
314
|
Ensure a `@provides(key=X)` exists in a module passed to `init(...)`, and your constructor type annotation is exactly `X`.
|
|
@@ -291,6 +324,88 @@ from app import Service, Config, TracingPlugin
|
|
|
291
324
|
|
|
292
325
|
---
|
|
293
326
|
|
|
327
|
+
## 10) Examples
|
|
328
|
+
|
|
329
|
+
### 10.1 Bootstrap & auto-imports
|
|
330
|
+
|
|
331
|
+
```python
|
|
332
|
+
# src/__init__.py
|
|
333
|
+
from pico_ioc.public_api import export_public_symbols_decorated
|
|
334
|
+
__getattr__, __dir__ = export_public_symbols_decorated("src", include_plugins=True)
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Now you can import cleanly:
|
|
338
|
+
|
|
339
|
+
```python
|
|
340
|
+
from src import Service, Config, TracingPlugin
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### 10.2 Flask with waitress
|
|
344
|
+
|
|
345
|
+
```python
|
|
346
|
+
# main_flask.py
|
|
347
|
+
import logging
|
|
348
|
+
from waitress import serve
|
|
349
|
+
import pico_ioc, src
|
|
350
|
+
from flask import Flask
|
|
351
|
+
|
|
352
|
+
def main():
|
|
353
|
+
logging.basicConfig(level=logging.INFO)
|
|
354
|
+
c = pico_ioc.init(src)
|
|
355
|
+
app = c.get(Flask)
|
|
356
|
+
serve(app, host="0.0.0.0", port=5001, threads=8)
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### 10.3 FastAPI with uvicorn
|
|
360
|
+
|
|
361
|
+
```python
|
|
362
|
+
# main_fastapi.py
|
|
363
|
+
import logging
|
|
364
|
+
import pico_ioc, src, uvicorn
|
|
365
|
+
from fastapi import FastAPI
|
|
366
|
+
|
|
367
|
+
def main():
|
|
368
|
+
logging.basicConfig(level=logging.INFO)
|
|
369
|
+
c = pico_ioc.init(src)
|
|
370
|
+
app = c.get(FastAPI)
|
|
371
|
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### 10.4 App Factory for externals
|
|
375
|
+
|
|
376
|
+
```python
|
|
377
|
+
# src/app_factory.py
|
|
378
|
+
from pico_ioc import factory_component, provides
|
|
379
|
+
from flask import Flask
|
|
380
|
+
from fastapi import FastAPI
|
|
381
|
+
import docker
|
|
382
|
+
from .config import Config
|
|
383
|
+
|
|
384
|
+
@factory_component
|
|
385
|
+
class AppFactory:
|
|
386
|
+
def __init__(self):
|
|
387
|
+
self._config = Config()
|
|
388
|
+
|
|
389
|
+
@provides(key=Config)
|
|
390
|
+
def provide_config(self) -> Config:
|
|
391
|
+
return self._config
|
|
392
|
+
|
|
393
|
+
@provides(key=Flask)
|
|
394
|
+
def provide_flask(self) -> Flask:
|
|
395
|
+
return Flask(__name__)
|
|
396
|
+
|
|
397
|
+
@provides(key=FastAPI)
|
|
398
|
+
def provide_fastapi(self) -> FastAPI:
|
|
399
|
+
return FastAPI()
|
|
400
|
+
|
|
401
|
+
@provides(key=docker.DockerClient)
|
|
402
|
+
def provide_docker(self) -> docker.DockerClient:
|
|
403
|
+
return docker.from_env()
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
294
408
|
**TL;DR**
|
|
295
409
|
Decorate components, provide externals by type, `init()` once, and let the container do the wiring—so you can **run tests, serve web apps, or batch jobs with minimal glue**.
|
|
296
410
|
|
|
411
|
+
|
|
@@ -67,6 +67,7 @@ fetching from sqlite:///demo.db
|
|
|
67
67
|
* **Test-friendly** → swap out components via `@provides`.
|
|
68
68
|
* **Universal** → works with Flask, FastAPI, CLIs, or plain scripts.
|
|
69
69
|
* **Extensible** → add tracing, logging, or metrics via plugins.
|
|
70
|
+
* **Overrides for testing** → inject mocks/fakes directly via `init(overrides={...})`.
|
|
70
71
|
|
|
71
72
|
---
|
|
72
73
|
|
|
@@ -102,6 +103,26 @@ This keeps `__init__.py` **clean, declarative, and convention-driven**.
|
|
|
102
103
|
|
|
103
104
|
---
|
|
104
105
|
|
|
106
|
+
## Testing with overrides
|
|
107
|
+
|
|
108
|
+
You can replace providers on the fly during tests:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from pico_ioc import init
|
|
112
|
+
import myapp
|
|
113
|
+
|
|
114
|
+
fake = {"repo": "fake-data"}
|
|
115
|
+
c = init(myapp, overrides={
|
|
116
|
+
"fast_model": fake, # constant instance
|
|
117
|
+
"user_service": lambda: {"id": 1}, # provider
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
svc = c.get("fast_model")
|
|
121
|
+
assert svc == {"repo": "fake-data"}
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
105
126
|
|
|
106
127
|
👉 Next steps:
|
|
107
128
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## [1.0.0] — 2025-08-28
|
|
8
|
+
|
|
9
|
+
### 🚀 Highlights
|
|
10
|
+
- **Dropped legacy runtimes**
|
|
11
|
+
- Minimum Python version is now **3.10+**
|
|
12
|
+
- Simplifies internals by relying on `typing.Annotated` and `include_extras=True`
|
|
13
|
+
|
|
14
|
+
- **Qualifiers support**
|
|
15
|
+
- Components can be tagged with `Qualifier` via `@qualifier(Q)`
|
|
16
|
+
- Enables fine-grained grouping of implementations
|
|
17
|
+
|
|
18
|
+
- **Collection injection**
|
|
19
|
+
- Inject `list[T]` or `tuple[T]` to receive all registered implementations
|
|
20
|
+
- Supports filtered injection with `list[Annotated[T, Q]]`
|
|
21
|
+
|
|
22
|
+
### 🔌 Core principles reaffirmed
|
|
23
|
+
- **Singleton per container** — no request/session scopes
|
|
24
|
+
- **Fail-fast bootstrap** — eager instantiation by default
|
|
25
|
+
- **Explicit plugins** — passed to `init()` directly, no magic auto-discovery
|
|
26
|
+
- **Public API helper** — `export_public_symbols_decorated` keeps `__init__.py` clean
|
|
27
|
+
|
|
28
|
+
### ❌ Won’t-do decisions
|
|
29
|
+
- Alternative scopes (request/session)
|
|
30
|
+
- Async providers (`async def`)
|
|
31
|
+
- Hot reload / dynamic re-scan
|
|
32
|
+
|
|
33
|
+
These were evaluated and **rejected** to keep pico-ioc simple, deterministic, and testable.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## [1.1.0] — 2025-09-08
|
|
38
|
+
|
|
39
|
+
### ✨ New
|
|
40
|
+
- **Overrides in `init()`**
|
|
41
|
+
- Added `overrides` argument to `init(...)` for ad-hoc mocking/testing.
|
|
42
|
+
- Accepted formats:
|
|
43
|
+
- `key: instance` → constant binding
|
|
44
|
+
- `key: callable` → non-lazy provider
|
|
45
|
+
- `key: (callable, lazy_bool)` → provider with explicit laziness
|
|
46
|
+
- Applied **before eager instantiation**, so replaced providers never run.
|
|
47
|
+
- If `reuse=True`, calling `init(..., overrides=...)` again mutates the cached container.
|
|
48
|
+
|
|
49
|
+
### 📚 Docs
|
|
50
|
+
- Updated **README.md**, **GUIDE.md**, **OVERVIEW.md**, **DECISIONS.md**, and **ARCHITECTURE.md** to document overrides support.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## [Unreleased]
|
|
55
|
+
- Upcoming improvements and fixes will be listed here.
|
|
56
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pico-ioc
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: A minimalist, zero-dependency Inversion of Control (IoC) container for Python.
|
|
5
5
|
Author-email: David Perez Cabrera <dperezcabrera@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -72,6 +72,7 @@ It helps you build loosely-coupled, testable apps without manual wiring. Inspire
|
|
|
72
72
|
- **Plugins** — lifecycle hooks (`before_scan`, `after_ready`).
|
|
73
73
|
- **Public API helper** — auto-export decorated symbols in `__init__.py`.
|
|
74
74
|
- **Thread/async safe** — isolation via `ContextVar`.
|
|
75
|
+
- **Overrides for testing** — inject mocks/fakes directly via `init(overrides={...})`.
|
|
75
76
|
|
|
76
77
|
---
|
|
77
78
|
|
|
@@ -117,7 +118,21 @@ print(svc.run())
|
|
|
117
118
|
```
|
|
118
119
|
fetching from sqlite:///demo.db
|
|
119
120
|
```
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
### Quick overrides for testing
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from pico_ioc import init
|
|
127
|
+
import myapp
|
|
120
128
|
|
|
129
|
+
fake = {"repo": "fake-data"}
|
|
130
|
+
c = init(myapp, overrides={
|
|
131
|
+
"fast_model": fake, # constant instance
|
|
132
|
+
"user_service": lambda: {"id": 1}, # provider
|
|
133
|
+
})
|
|
134
|
+
assert c.get("fast_model") == {"repo": "fake-data"}
|
|
135
|
+
```
|
|
121
136
|
---
|
|
122
137
|
|
|
123
138
|
## 📖 Documentation
|
|
@@ -137,6 +152,12 @@ tox
|
|
|
137
152
|
|
|
138
153
|
---
|
|
139
154
|
|
|
155
|
+
## 📜 Changelog
|
|
156
|
+
|
|
157
|
+
See [CHANGELOG.md](./CHANGELOG.md) for version history.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
140
161
|
## 📜 License
|
|
141
162
|
|
|
142
163
|
MIT — see [LICENSE](https://opensource.org/licenses/MIT)
|
|
@@ -27,6 +27,7 @@ It helps you build loosely-coupled, testable apps without manual wiring. Inspire
|
|
|
27
27
|
- **Plugins** — lifecycle hooks (`before_scan`, `after_ready`).
|
|
28
28
|
- **Public API helper** — auto-export decorated symbols in `__init__.py`.
|
|
29
29
|
- **Thread/async safe** — isolation via `ContextVar`.
|
|
30
|
+
- **Overrides for testing** — inject mocks/fakes directly via `init(overrides={...})`.
|
|
30
31
|
|
|
31
32
|
---
|
|
32
33
|
|
|
@@ -72,7 +73,21 @@ print(svc.run())
|
|
|
72
73
|
```
|
|
73
74
|
fetching from sqlite:///demo.db
|
|
74
75
|
```
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
### Quick overrides for testing
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from pico_ioc import init
|
|
82
|
+
import myapp
|
|
75
83
|
|
|
84
|
+
fake = {"repo": "fake-data"}
|
|
85
|
+
c = init(myapp, overrides={
|
|
86
|
+
"fast_model": fake, # constant instance
|
|
87
|
+
"user_service": lambda: {"id": 1}, # provider
|
|
88
|
+
})
|
|
89
|
+
assert c.get("fast_model") == {"repo": "fake-data"}
|
|
90
|
+
```
|
|
76
91
|
---
|
|
77
92
|
|
|
78
93
|
## 📖 Documentation
|
|
@@ -92,6 +107,12 @@ tox
|
|
|
92
107
|
|
|
93
108
|
---
|
|
94
109
|
|
|
110
|
+
## 📜 Changelog
|
|
111
|
+
|
|
112
|
+
See [CHANGELOG.md](./CHANGELOG.md) for version history.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
95
116
|
## 📜 License
|
|
96
117
|
|
|
97
118
|
MIT — see [LICENSE](https://opensource.org/licenses/MIT)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '1.1.0'
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import inspect
|
|
4
4
|
import logging
|
|
5
5
|
from contextlib import contextmanager
|
|
6
|
-
from typing import Callable, Optional, Tuple
|
|
6
|
+
from typing import Callable, Optional, Tuple, Any, Dict # ⬅️ Any, Dict
|
|
7
7
|
|
|
8
8
|
from .container import PicoContainer, Binder
|
|
9
9
|
from .plugins import PicoPlugin
|
|
@@ -24,14 +24,14 @@ def init(
|
|
|
24
24
|
auto_exclude_caller: bool = True,
|
|
25
25
|
plugins: Tuple[PicoPlugin, ...] = (),
|
|
26
26
|
reuse: bool = True,
|
|
27
|
+
overrides: Optional[Dict[Any, Any]] = None, # ⬅️ NUEVO
|
|
27
28
|
) -> PicoContainer:
|
|
28
|
-
|
|
29
|
-
Initialize and configure a PicoContainer by scanning a root package.
|
|
30
|
-
"""
|
|
29
|
+
|
|
31
30
|
root_name = root_package if isinstance(root_package, str) else getattr(root_package, "__name__", None)
|
|
32
31
|
|
|
33
|
-
# Reuse only if the existing container was built for the same root
|
|
34
32
|
if reuse and _state._container and _state._root_name == root_name:
|
|
33
|
+
if overrides:
|
|
34
|
+
_apply_overrides(_state._container, overrides)
|
|
35
35
|
return _state._container
|
|
36
36
|
|
|
37
37
|
combined_exclude = _build_exclude(exclude, auto_exclude_caller, root_name=root_name)
|
|
@@ -48,6 +48,9 @@ def init(
|
|
|
48
48
|
plugins=plugins,
|
|
49
49
|
)
|
|
50
50
|
|
|
51
|
+
if overrides:
|
|
52
|
+
_apply_overrides(container, overrides)
|
|
53
|
+
|
|
51
54
|
_run_hooks(plugins, "after_bind", container, binder)
|
|
52
55
|
_run_hooks(plugins, "before_eager", container, binder)
|
|
53
56
|
|
|
@@ -57,22 +60,32 @@ def init(
|
|
|
57
60
|
|
|
58
61
|
logging.info("Container configured and ready.")
|
|
59
62
|
_state._container = container
|
|
60
|
-
_state._root_name = root_name
|
|
63
|
+
_state._root_name = root_name
|
|
61
64
|
return container
|
|
62
65
|
|
|
63
66
|
|
|
64
67
|
# -------------------- helpers --------------------
|
|
65
68
|
|
|
69
|
+
def _apply_overrides(container: PicoContainer, overrides: Dict[Any, Any]) -> None:
|
|
70
|
+
for key, val in overrides.items():
|
|
71
|
+
lazy = False
|
|
72
|
+
if isinstance(val, tuple) and len(val) == 2 and callable(val[0]) and isinstance(val[1], bool):
|
|
73
|
+
provider = val[0]
|
|
74
|
+
lazy = val[1]
|
|
75
|
+
elif callable(val):
|
|
76
|
+
provider = val
|
|
77
|
+
else:
|
|
78
|
+
def provider(v=val):
|
|
79
|
+
return v
|
|
80
|
+
container.bind(key, provider, lazy=lazy)
|
|
81
|
+
|
|
82
|
+
|
|
66
83
|
def _build_exclude(
|
|
67
84
|
exclude: Optional[Callable[[str], bool]],
|
|
68
85
|
auto_exclude_caller: bool,
|
|
69
86
|
*,
|
|
70
87
|
root_name: Optional[str] = None,
|
|
71
88
|
) -> Optional[Callable[[str], bool]]:
|
|
72
|
-
"""
|
|
73
|
-
Compose the exclude predicate. When auto_exclude_caller=True, exclude only
|
|
74
|
-
the exact calling module, but never exclude modules under the root being scanned.
|
|
75
|
-
"""
|
|
76
89
|
if not auto_exclude_caller:
|
|
77
90
|
return exclude
|
|
78
91
|
|
|
@@ -91,7 +104,6 @@ def _build_exclude(
|
|
|
91
104
|
|
|
92
105
|
|
|
93
106
|
def _get_caller_module_name() -> Optional[str]:
|
|
94
|
-
"""Return the module name that called `init`."""
|
|
95
107
|
try:
|
|
96
108
|
f = inspect.currentframe()
|
|
97
109
|
# frame -> _get_caller_module_name -> _build_exclude -> init
|
|
@@ -28,6 +28,9 @@ class PicoContainer:
|
|
|
28
28
|
self._singletons: Dict[Any, Any] = {}
|
|
29
29
|
|
|
30
30
|
def bind(self, key: Any, provider, *, lazy: bool):
|
|
31
|
+
# 🔧 rebind must evict any cached singleton for this key
|
|
32
|
+
self._singletons.pop(key, None)
|
|
33
|
+
|
|
31
34
|
meta = {"factory": provider, "lazy": bool(lazy)}
|
|
32
35
|
try:
|
|
33
36
|
q = getattr(key, QUALIFIERS_KEY, ())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pico-ioc
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: A minimalist, zero-dependency Inversion of Control (IoC) container for Python.
|
|
5
5
|
Author-email: David Perez Cabrera <dperezcabrera@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -72,6 +72,7 @@ It helps you build loosely-coupled, testable apps without manual wiring. Inspire
|
|
|
72
72
|
- **Plugins** — lifecycle hooks (`before_scan`, `after_ready`).
|
|
73
73
|
- **Public API helper** — auto-export decorated symbols in `__init__.py`.
|
|
74
74
|
- **Thread/async safe** — isolation via `ContextVar`.
|
|
75
|
+
- **Overrides for testing** — inject mocks/fakes directly via `init(overrides={...})`.
|
|
75
76
|
|
|
76
77
|
---
|
|
77
78
|
|
|
@@ -117,7 +118,21 @@ print(svc.run())
|
|
|
117
118
|
```
|
|
118
119
|
fetching from sqlite:///demo.db
|
|
119
120
|
```
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
### Quick overrides for testing
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from pico_ioc import init
|
|
127
|
+
import myapp
|
|
120
128
|
|
|
129
|
+
fake = {"repo": "fake-data"}
|
|
130
|
+
c = init(myapp, overrides={
|
|
131
|
+
"fast_model": fake, # constant instance
|
|
132
|
+
"user_service": lambda: {"id": 1}, # provider
|
|
133
|
+
})
|
|
134
|
+
assert c.get("fast_model") == {"repo": "fake-data"}
|
|
135
|
+
```
|
|
121
136
|
---
|
|
122
137
|
|
|
123
138
|
## 📖 Documentation
|
|
@@ -137,6 +152,12 @@ tox
|
|
|
137
152
|
|
|
138
153
|
---
|
|
139
154
|
|
|
155
|
+
## 📜 Changelog
|
|
156
|
+
|
|
157
|
+
See [CHANGELOG.md](./CHANGELOG.md) for version history.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
140
161
|
## 📜 License
|
|
141
162
|
|
|
142
163
|
MIT — see [LICENSE](https://opensource.org/licenses/MIT)
|
|
@@ -121,3 +121,77 @@ def test_logging_messages_emitted(caplog, monkeypatch):
|
|
|
121
121
|
assert any("Initializing pico-ioc..." in m for m in msgs)
|
|
122
122
|
assert any("Container configured and ready." in m for m in msgs)
|
|
123
123
|
|
|
124
|
+
|
|
125
|
+
# --- Overrides tests -----------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
def test_overrides_bind_instance_provider_and_lazy(monkeypatch):
|
|
128
|
+
"""
|
|
129
|
+
Verify that `overrides` supports:
|
|
130
|
+
- constant instance bindings,
|
|
131
|
+
- provider callable bindings (non-lazy),
|
|
132
|
+
- (provider, lazy_bool) bindings (lazy).
|
|
133
|
+
And that overrides are applied BEFORE eager instantiation so that
|
|
134
|
+
scanned providers don't run if they are replaced.
|
|
135
|
+
"""
|
|
136
|
+
calls = {"scanned_x": 0, "prov_y": 0, "lazy_z": 0}
|
|
137
|
+
|
|
138
|
+
# Fake scan that would bind an eager provider for key "x"
|
|
139
|
+
def fake_scan(root_package, container, *, exclude, plugins):
|
|
140
|
+
container.bind("x", lambda: ("scanned", calls.__setitem__("scanned_x", calls["scanned_x"] + 1)), lazy=False)
|
|
141
|
+
|
|
142
|
+
monkeypatch.setattr(api, "scan_and_configure", fake_scan)
|
|
143
|
+
|
|
144
|
+
# Prepare overrides:
|
|
145
|
+
# - "x": constant instance → must replace scanned provider and avoid its execution
|
|
146
|
+
# - "y": provider (non-lazy) → executed during eager
|
|
147
|
+
# - "z": (provider, True) lazy → executed only on first get()
|
|
148
|
+
def prov_y():
|
|
149
|
+
calls["prov_y"] += 1
|
|
150
|
+
return {"y": "from-provider"}
|
|
151
|
+
|
|
152
|
+
def prov_z():
|
|
153
|
+
calls["lazy_z"] += 1
|
|
154
|
+
return {"z": "lazy-provider"}
|
|
155
|
+
|
|
156
|
+
c = api.init(
|
|
157
|
+
"pkg",
|
|
158
|
+
reuse=False,
|
|
159
|
+
overrides={
|
|
160
|
+
"x": {"x": "constant"}, # instance
|
|
161
|
+
"y": prov_y, # provider, non-lazy
|
|
162
|
+
"z": (prov_z, True), # provider, lazy=True
|
|
163
|
+
},
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# "x" should be the constant instance; scanned provider must not have run
|
|
167
|
+
assert c.get("x") == {"x": "constant"}
|
|
168
|
+
assert calls["scanned_x"] == 0
|
|
169
|
+
|
|
170
|
+
# "y" is non-lazy → created during eager (exactly once)
|
|
171
|
+
assert c.get("y") == {"y": "from-provider"}
|
|
172
|
+
assert calls["prov_y"] == 1
|
|
173
|
+
|
|
174
|
+
# "z" is lazy → not created at eager time
|
|
175
|
+
assert calls["lazy_z"] == 0
|
|
176
|
+
assert c.get("z") == {"z": "lazy-provider"}
|
|
177
|
+
assert calls["lazy_z"] == 1
|
|
178
|
+
# Cached singleton on subsequent gets
|
|
179
|
+
assert c.get("z") == {"z": "lazy-provider"}
|
|
180
|
+
assert calls["lazy_z"] == 1
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def test_overrides_apply_on_reused_container(monkeypatch):
|
|
184
|
+
"""
|
|
185
|
+
If reuse=True and the root matches, calling init() again with new overrides
|
|
186
|
+
should apply them on the cached container.
|
|
187
|
+
"""
|
|
188
|
+
monkeypatch.setattr(api, "scan_and_configure", lambda *a, **k: None)
|
|
189
|
+
|
|
190
|
+
c1 = api.init("pkg", reuse=True, overrides={"k": {"v": 1}})
|
|
191
|
+
assert c1.get("k") == {"v": 1}
|
|
192
|
+
|
|
193
|
+
# Reuse the same root, override with a different value
|
|
194
|
+
c2 = api.init("pkg", reuse=True, overrides={"k": {"v": 2}})
|
|
195
|
+
assert c1 is c2
|
|
196
|
+
assert c2.get("k") == {"v": 2} # updated
|
|
197
|
+
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = '1.0.0'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|