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.
Files changed (45) hide show
  1. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/.llm/ARCHITECTURE.md +28 -4
  2. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/.llm/DECISIONS.md +17 -0
  3. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/.llm/GUIDE.md +129 -14
  4. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/.llm/OVERVIEW.md +21 -0
  5. pico_ioc-1.1.0/CHANGELOG.md +56 -0
  6. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/PKG-INFO +22 -1
  7. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/README.md +21 -0
  8. pico_ioc-1.1.0/src/pico_ioc/_version.py +1 -0
  9. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/src/pico_ioc/api.py +23 -11
  10. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/src/pico_ioc/container.py +3 -0
  11. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/src/pico_ioc.egg-info/PKG-INFO +22 -1
  12. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/src/pico_ioc.egg-info/SOURCES.txt +1 -0
  13. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/tests/test_api_unit.py +74 -0
  14. pico_ioc-1.0.0/src/pico_ioc/_version.py +0 -1
  15. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/.coveragerc +0 -0
  16. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/.github/workflows/ci.yml +0 -0
  17. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/.github/workflows/publish-to-pypi.yml +0 -0
  18. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/LICENSE +0 -0
  19. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/MANIFEST.in +0 -0
  20. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/pyproject.toml +0 -0
  21. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/setup.cfg +0 -0
  22. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/src/pico_ioc/__init__.py +0 -0
  23. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/src/pico_ioc/_state.py +0 -0
  24. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/src/pico_ioc/decorators.py +0 -0
  25. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/src/pico_ioc/plugins.py +0 -0
  26. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/src/pico_ioc/proxy.py +0 -0
  27. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/src/pico_ioc/public_api.py +0 -0
  28. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/src/pico_ioc/resolver.py +0 -0
  29. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/src/pico_ioc/scanner.py +0 -0
  30. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/src/pico_ioc/typing_utils.py +0 -0
  31. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/src/pico_ioc.egg-info/dependency_links.txt +0 -0
  32. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/src/pico_ioc.egg-info/top_level.txt +0 -0
  33. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/tests/test_container_get_all.py +0 -0
  34. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/tests/test_container_unit.py +0 -0
  35. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/tests/test_decorators_unit.py +0 -0
  36. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/tests/test_pico_ioc.py +0 -0
  37. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/tests/test_pico_ioc_additional.py +0 -0
  38. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/tests/test_pico_ioc_discovery.py +0 -0
  39. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/tests/test_proxy_unit.py +0 -0
  40. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/tests/test_public_api.py +0 -0
  41. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/tests/test_qualifiers_unit.py +0 -0
  42. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/tests/test_resolver_unit.py +0 -0
  43. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/tests/test_scanner_unit.py +0 -0
  44. {pico_ioc-1.0.0 → pico_ioc-1.1.0}/tests/test_typing_utils_unit.py +0 -0
  45. {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
- * **Test overrides**: register a factory after the app module.
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
- * **Multi-env composition**: define `app.prod`, `app.dev`, `app.test` packages that import different provider sets, then `init([app.prod])` etc.
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 (typeMROtoken)]
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/test_overrides.py
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 test_overrides
187
+ from tests import test_overrides_module
184
188
 
185
189
  def test_service_fetch():
186
- c = init([app, test_overrides]) # pass a list (composition root for tests)
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
- > Pattern: use a **composition module** per environment (prod/test) and call `init([...])` with the modules that declare your providers. Providers registered later override earlier ones for the same key.
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
- ## 5b) Qualifiers & collection injection
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
- If you request `list[Handler]` you get **all** implementations.
224
- If you request `list[Annotated[Handler, PAYMENTS]]`, you only get the tagged ones.
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
- ## 5c) Plugins & Public API helper
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
- ## 6) Tips & guardrails
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
- ## 7) Troubleshooting
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.0.0
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 # remember which root this container represents
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.0.0
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)
@@ -1,4 +1,5 @@
1
1
  .coveragerc
2
+ CHANGELOG.md
2
3
  LICENSE
3
4
  MANIFEST.in
4
5
  README.md
@@ -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