pico-ioc 0.5.2__tar.gz → 1.0.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 (49) hide show
  1. {pico_ioc-0.5.2 → pico_ioc-1.0.0}/.github/workflows/ci.yml +1 -1
  2. pico_ioc-1.0.0/.llm/ARCHITECTURE.md +282 -0
  3. pico_ioc-1.0.0/.llm/DECISIONS.md +118 -0
  4. pico_ioc-1.0.0/.llm/GUIDE.md +296 -0
  5. pico_ioc-1.0.0/.llm/OVERVIEW.md +112 -0
  6. pico_ioc-1.0.0/PKG-INFO +145 -0
  7. pico_ioc-1.0.0/README.md +100 -0
  8. {pico_ioc-0.5.2 → pico_ioc-1.0.0}/pyproject.toml +1 -3
  9. pico_ioc-1.0.0/src/pico_ioc/__init__.py +31 -0
  10. pico_ioc-1.0.0/src/pico_ioc/_state.py +10 -0
  11. pico_ioc-1.0.0/src/pico_ioc/_version.py +1 -0
  12. pico_ioc-1.0.0/src/pico_ioc/api.py +128 -0
  13. pico_ioc-1.0.0/src/pico_ioc/container.py +155 -0
  14. pico_ioc-1.0.0/src/pico_ioc/decorators.py +76 -0
  15. pico_ioc-1.0.0/src/pico_ioc/plugins.py +12 -0
  16. pico_ioc-1.0.0/src/pico_ioc/proxy.py +77 -0
  17. pico_ioc-1.0.0/src/pico_ioc/public_api.py +76 -0
  18. pico_ioc-1.0.0/src/pico_ioc/resolver.py +110 -0
  19. pico_ioc-1.0.0/src/pico_ioc/scanner.py +238 -0
  20. pico_ioc-1.0.0/src/pico_ioc/typing_utils.py +29 -0
  21. pico_ioc-1.0.0/src/pico_ioc.egg-info/PKG-INFO +145 -0
  22. pico_ioc-1.0.0/src/pico_ioc.egg-info/SOURCES.txt +41 -0
  23. pico_ioc-1.0.0/tests/test_api_unit.py +123 -0
  24. pico_ioc-1.0.0/tests/test_container_get_all.py +23 -0
  25. pico_ioc-1.0.0/tests/test_container_unit.py +125 -0
  26. pico_ioc-1.0.0/tests/test_decorators_unit.py +138 -0
  27. {pico_ioc-0.5.2 → pico_ioc-1.0.0}/tests/test_pico_ioc.py +4 -1
  28. {pico_ioc-0.5.2 → pico_ioc-1.0.0}/tests/test_pico_ioc_additional.py +8 -2
  29. pico_ioc-1.0.0/tests/test_pico_ioc_discovery.py +114 -0
  30. pico_ioc-1.0.0/tests/test_proxy_unit.py +322 -0
  31. pico_ioc-1.0.0/tests/test_public_api.py +220 -0
  32. pico_ioc-1.0.0/tests/test_qualifiers_unit.py +70 -0
  33. pico_ioc-1.0.0/tests/test_resolver_unit.py +121 -0
  34. pico_ioc-1.0.0/tests/test_scanner_unit.py +190 -0
  35. pico_ioc-1.0.0/tests/test_typing_utils_unit.py +102 -0
  36. {pico_ioc-0.5.2 → pico_ioc-1.0.0}/tox.ini +1 -1
  37. pico_ioc-0.5.2/PKG-INFO +0 -278
  38. pico_ioc-0.5.2/README.md +0 -231
  39. pico_ioc-0.5.2/src/pico_ioc/__init__.py +0 -408
  40. pico_ioc-0.5.2/src/pico_ioc/_version.py +0 -1
  41. pico_ioc-0.5.2/src/pico_ioc.egg-info/PKG-INFO +0 -278
  42. pico_ioc-0.5.2/src/pico_ioc.egg-info/SOURCES.txt +0 -16
  43. {pico_ioc-0.5.2 → pico_ioc-1.0.0}/.coveragerc +0 -0
  44. {pico_ioc-0.5.2 → pico_ioc-1.0.0}/.github/workflows/publish-to-pypi.yml +0 -0
  45. {pico_ioc-0.5.2 → pico_ioc-1.0.0}/LICENSE +0 -0
  46. {pico_ioc-0.5.2 → pico_ioc-1.0.0}/MANIFEST.in +0 -0
  47. {pico_ioc-0.5.2 → pico_ioc-1.0.0}/setup.cfg +0 -0
  48. {pico_ioc-0.5.2 → pico_ioc-1.0.0}/src/pico_ioc.egg-info/dependency_links.txt +0 -0
  49. {pico_ioc-0.5.2 → pico_ioc-1.0.0}/src/pico_ioc.egg-info/top_level.txt +0 -0
@@ -13,7 +13,7 @@ jobs:
13
13
  strategy:
14
14
  fail-fast: false
15
15
  matrix:
16
- python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12", "3.13" ]
16
+ python-version: [ "3.10", "3.11", "3.12", "3.13" ]
17
17
 
18
18
  steps:
19
19
  - name: Checkout
@@ -0,0 +1,282 @@
1
+ # pico-ioc — Architecture
2
+
3
+ > Scope: internal model, wiring algorithm, lifecycle, and design trade-offs.
4
+ > Non-goals: user tutorials or recipes (see `GUIDE.md`), product pitch (see `OVERVIEW.md`).
5
+
6
+ > ⚠️ **Requires Python 3.10+** (uses `typing.Annotated` and `include_extras=True`).
7
+
8
+ ---
9
+
10
+ ## 1) Design goals & non-goals
11
+
12
+ **Goals**
13
+
14
+ * **Tiny, predictable DI** for Python apps (CLIs, Flask/FastAPI, services).
15
+ * **Fail fast** at bootstrap; deterministic resolution.
16
+ * **Ergonomic**: annotate constructors, avoid runtime reflection tricks.
17
+ * **Framework-agnostic**: no hard dependency on Flask/FastAPI/etc.
18
+ * **Safe by default**: thread/async friendly, no global mutable singletons.
19
+
20
+ **Non-goals**
21
+
22
+ * Full Spring-like feature set (AOP, complex scopes, bean post-processors).
23
+ * Runtime reconfiguration or hot-swap of graphs.
24
+ * Magical auto-imports across the filesystem.
25
+
26
+ ---
27
+
28
+ ## 2) High-level model
29
+
30
+ * **Component**: class marked with `@component`. Instantiated by the container.
31
+ * **Factory component**: class marked with `@factory_component` that owns one or more **providers** declared via `@provides(key=TypeOrToken)`. Providers return *externals* (e.g., `Flask`, clients, engines).
32
+ * **Container**: built by `pico_ioc.init(module_or_list)`; resolves dependencies with `container.get(KeyOrType)`.
33
+
34
+ ### Bootstrap sequence (simplified)
35
+
36
+ ```mermaid
37
+ sequenceDiagram
38
+ participant App as Your package(s)
39
+ participant IOC as pico-ioc Container
40
+ App->>IOC: init([app, overrides?])
41
+ IOC->>App: scan decorators (@component, @factory_component)
42
+ IOC->>IOC: register providers (key -> factory)
43
+ IOC->>IOC: validate graph (constructor types, duplicates)
44
+ App->>IOC: get(Service)
45
+ IOC->>IOC: resolve deps (Repo, Config, ...)
46
+ IOC-->>App: instance(Service)
47
+ ````
48
+
49
+ ---
50
+
51
+ ## 3) Discovery & registration
52
+
53
+ 1. **Scan inputs** passed to `init(...)`: a module or list of modules/packages.
54
+ 2. **Collect**:
55
+
56
+ * `@component` classes → registered by **type** (the class itself).
57
+ * `@factory_component` classes → introspected for `@provides(key=...)` methods.
58
+ * `@plugin` classes → collected if explicitly passed to `init(…, plugins=(…))`.
59
+ 3. **Registry** (immutable after bootstrap):
60
+
61
+ * `key -> provider` map. Keys are usually **types**. String tokens are supported but discouraged.
62
+
63
+ > **Invariant:** A key has **at most one** active provider; the **last registration wins**.
64
+ > This enables test overrides by ordering `init([app, test_overrides])`.
65
+
66
+ ---
67
+
68
+ ## 4) Resolution algorithm (deterministic)
69
+
70
+ When constructing a component `C`:
71
+
72
+ 1. Inspect `__init__(self, ...)` and collect **type-annotated** parameters (excluding `self`).
73
+ 2. For each parameter `p: T` resolve **by key** using this order:
74
+
75
+ 1. **Exact type** `T`
76
+ 2. **MRO walk**: first registered base class or protocol
77
+ 3. **String key** (only if `@provides(key="token")` was used)
78
+ 3. Instantiate dependencies depth-first; memoize singletons.
79
+ 4. Construct `C` with resolved instances.
80
+
81
+ **Failure modes**
82
+
83
+ * No provider for a required key → **bootstrap error** (fail fast).
84
+ * Ambiguous/incompatible registrations → **bootstrap error** with a precise hint.
85
+
86
+ ### 4b) Collection resolution
87
+
88
+ If a constructor requests `list[T]` or `list[Annotated[T, Q]]`:
89
+
90
+ * The container resolves **all** registered providers compatible with `T`.
91
+ * If qualifiers are present (`Q`), only matching components are returned.
92
+ * Order is stable by discovery/registration; no implicit sorting.
93
+ * Empty list if no matches.
94
+
95
+ ---
96
+
97
+ ## 5) Lifecycles & scopes
98
+
99
+ * **Singleton per container** (default): each key is created once and cached.
100
+ * **Lazy proxies (optional)**: `@component(lazy=True)` defers instantiation until first use. Use sparingly; prefer eager to surface errors early.
101
+
102
+ > **Why singleton:** It matches typical app composition (config, DB clients, HTTP apps) and keeps DI simple and fast.
103
+
104
+ ---
105
+
106
+ ## 6) Factories & providers
107
+
108
+ Use `@factory_component` to integrate **externals**:
109
+
110
+ ```python
111
+ from pico_ioc import factory_component, provides
112
+ from flask import Flask
113
+
114
+ @factory_component
115
+ class AppFactory:
116
+ @provides(key=Flask)
117
+ def provide_flask(self) -> Flask:
118
+ app = Flask(__name__)
119
+ app.config["JSON_AS_ASCII"] = False
120
+ return app
121
+ ```
122
+
123
+ * **Providers** should be **pure constructors** (no long-running work).
124
+ * Prefer **typed keys** (e.g., `Flask`) over string tokens.
125
+
126
+ ---
127
+
128
+ ## 7) Concurrency model
129
+
130
+ * Container state is **immutable after init**.
131
+ * Resolution cache is **thread/async safe** via internal isolation (no global singletons).
132
+ * Instances you create **must** be thread-safe if shared (container can’t fix non-safe libs).
133
+
134
+ ---
135
+
136
+ ## 8) Error handling & diagnostics
137
+
138
+ * **Bootstrap phase**:
139
+
140
+ * Missing providers → raise with the exact parameter/type.
141
+ * Duplicate keys → last wins; log/trace retains registration order for debugging.
142
+ * Type annotation mismatch → explicit error naming the offending `__init__` param.
143
+ * **Runtime**:
144
+
145
+ * Exceptions bubble from provider/constructor; container stack traces include the dependency chain (key path).
146
+
147
+ Tip: Keep constructors **cheap** (I/O at the edges, e.g., in start/serve methods).
148
+
149
+ ---
150
+
151
+ ## 9) Configuration
152
+
153
+ * Treat config as a **component**:
154
+
155
+ ```python
156
+ @component
157
+ class Config:
158
+ WORKERS: int = int(os.getenv("WORKERS", "4"))
159
+ DEBUG: bool = os.getenv("DEBUG", "0") == "1"
160
+ ```
161
+
162
+ * Inject `Config` wherever needed; do not read `os.getenv` scattered across the codebase.
163
+
164
+ ---
165
+
166
+ ## 10) Extensibility & overrides
167
+
168
+ * **Test overrides**: register a factory after the app module.
169
+
170
+ ```python
171
+ @factory_component
172
+ class TestOverrides:
173
+ @provides(key=Repo) # same key as production
174
+ def provide_repo(self) -> Repo:
175
+ return FakeRepo()
176
+ ```
177
+
178
+ Then:
179
+
180
+ ```python
181
+ c = init([app, test_overrides]) # overrides win by order
182
+ ```
183
+
184
+ * **Multi-env composition**: define `app.prod`, `app.dev`, `app.test` packages that import different provider sets, then `init([app.prod])` etc.
185
+
186
+ ### Plugins
187
+
188
+ Classes decorated with `@plugin` and implementing `PicoPlugin` can observe lifecycle events:
189
+
190
+ * `before_scan(package, binder)`
191
+ * `after_ready(container, binder)`
192
+
193
+ They are passed explicitly to `init(..., plugins=(MyPlugin(),))`.
194
+
195
+ ---
196
+
197
+ ## 11) Performance notes
198
+
199
+ * **O(1)** provider lookup by key; no reflection at call sites after bootstrap.
200
+ * Eager construction surfaces failures early and warms caches.
201
+ * Keep providers idempotent and cheap; push heavy I/O to runtime methods.
202
+
203
+ ---
204
+
205
+ ## 12) Security & boundaries
206
+
207
+ * The container **does not** open sockets/files or spawn threads by itself.
208
+ * Side effects belong to **your components**. Keep providers deterministic.
209
+ * Avoid storing secrets in code; pass secrets via your own config component.
210
+
211
+ ---
212
+
213
+ ## 13) Reference diagrams
214
+
215
+ ### Registry & resolution (class diagram)
216
+
217
+ ```mermaid
218
+ classDiagram
219
+ class Container {
220
+ +get(key) instance
221
+ -registry: Map[key, Provider]
222
+ -cache: Map[key, instance]
223
+ }
224
+ class Provider {
225
+ +create() instance
226
+ +key
227
+ }
228
+ class ComponentProvider
229
+ class FactoryProvider
230
+ Container "1" --> "*" Provider
231
+ Provider <|-- ComponentProvider
232
+ Provider <|-- FactoryProvider
233
+ ```
234
+
235
+ ### Resolution flow (activity)
236
+
237
+ ```mermaid
238
+ flowchart TD
239
+ A[Request instance for Type T] --> B{Cached?}
240
+ B -- yes --> Z[Return cached]
241
+ B -- no --> C[Find provider for T (type→MRO→token)]
242
+ C -- none --> E[Raise bootstrap error]
243
+ C -- found --> D[Resolve constructor args recursively]
244
+ D --> F[Instantiate]
245
+ F --> G[Cache instance]
246
+ G --> Z[Return instance]
247
+ ```
248
+
249
+ ---
250
+
251
+ ## 14) Rationale & trade-offs
252
+
253
+ * **Typed keys first** → better ergonomics, editor support, less foot-guns than strings.
254
+ * **Singleton scope** → keeps mental model simple; Python apps rarely need per-request scoping at the container level (push that to frameworks).
255
+ * **No global state** → explicit container ownership clarifies boundaries and testability.
256
+ * **Fail fast** → configuration/graph problems are caught at start, not mid-request.
257
+
258
+ ---
259
+
260
+ ## 15) Appendix: FAQs (architecture-level)
261
+
262
+ * *Why not metaclass magic to auto-wire everything?*
263
+ Determinism and debuggability. Explicit decorators and typed constructors win.
264
+
265
+ * *Can I lazy-load everything?*
266
+ You can, but you shouldn’t. Lazy only where it truly reduces cost without hiding errors.
267
+
268
+ * *How do I integrate with Flask/FastAPI?*
269
+ Provide the app/client via a factory (`@provides(key=Flask)`), then `container.get(Flask)` in your bootstrap. See `GUIDE.md`.
270
+
271
+ ### Public API helper
272
+
273
+ `export_public_symbols_decorated` builds `__getattr__`/`__dir__` for a package’s `__init__.py`,
274
+ auto-exporting all `@component`, `@factory_component`, and `@plugin` classes, plus any `__all__`.
275
+
276
+ ---
277
+
278
+ **TL;DR**
279
+ `pico-ioc` builds a **deterministic dependency graph** at startup from decorated components, factories, and plugins.
280
+ It resolves by **type**, supports **collection injection with qualifiers**, memoizes singletons, and fails fast — so your app wiring stays **predictable, testable, and framework-agnostic**.
281
+
282
+
@@ -0,0 +1,118 @@
1
+ # DECISIONS.md — pico-ioc
2
+
3
+ This document records **technical and architectural decisions** made for pico-ioc.
4
+ Each entry has a rationale and implications. If a decision is revoked, it should be marked as **[REVOKED]** with a link to the replacement.
5
+
6
+ ---
7
+
8
+ ## ✅ Current Decisions
9
+
10
+ ### 1. Minimum Python version: **3.10**
11
+ - **Decision**: Drop support for Python 3.8 and 3.9. Require Python **3.10+**.
12
+ - **Rationale**:
13
+ * Uses `typing.Annotated` and `typing.get_type_hints(..., include_extras=True)`.
14
+ * Simplifies code paths and avoids backports/conditionals.
15
+ * Encourages modern typing practices.
16
+ - **Implications**:
17
+ * Users on older runtimes must upgrade.
18
+ * CI/CD matrix only tests Python 3.10+.
19
+
20
+ ---
21
+
22
+ ### 2. Resolution order: **name-first**
23
+ - **Decision**: Parameter name has precedence over type annotation.
24
+ - **Order**: **param name → exact type → MRO fallback → string token**.
25
+ - **Rationale**:
26
+ * Matches ergonomic use-cases (config by name).
27
+ * Keeps deterministic behavior and avoids ambiguity.
28
+ - **Implications**:
29
+ * Breaking change from earlier versions (<0.5.0).
30
+ * Documented in README and GUIDE.
31
+
32
+ ---
33
+
34
+ ### 3. Lifecycle: **singleton per container**
35
+ - **Decision**: Every provider produces exactly one instance per container.
36
+ - **Rationale**:
37
+ * Matches common Python app architecture (config, DB clients, service classes).
38
+ * Simple mental model, cheap lookups.
39
+ - **Implications**:
40
+ * No per-request or session scopes at the IoC level (delegate that to frameworks).
41
+ * Lazy proxies supported via `lazy=True`.
42
+
43
+ ---
44
+
45
+ ### 4. Fail-fast bootstrap
46
+ - **Decision**: All eager components are instantiated immediately after `init()`.
47
+ - **Rationale**:
48
+ * Surfaces missing dependencies early.
49
+ * Avoids hidden runtime errors deep into request handling.
50
+ - **Implications**:
51
+ * Slower startup if many components are eager.
52
+ * Recommended to keep constructors cheap.
53
+
54
+ ---
55
+
56
+ ### 5. Plugins: **explicit registration**
57
+ - **Decision**: Plugins must be passed explicitly to `init(..., plugins=(...))`.
58
+ - **Rationale**:
59
+ * Keeps scanning predictable and avoids magical discovery.
60
+ * Encourages explicit boundaries in app wiring.
61
+ - **Implications**:
62
+ * More verbose in user code, but safer and testable.
63
+ * `@plugin` decorator only marks, does not auto-register.
64
+
65
+ ---
66
+
67
+ ### 6. Public API helper (`export_public_symbols_decorated`)
68
+ - **Decision**: Auto-export all decorated classes/components/plugins via `__getattr__`/`__dir__`.
69
+ - **Rationale**:
70
+ * Reduces `__init__.py` boilerplate.
71
+ * Encourages convention-over-configuration.
72
+ - **Implications**:
73
+ * Explicit imports may be replaced by dynamic export.
74
+ * Errors in scanning are suppressed (non-fatal).
75
+
76
+ ---
77
+
78
+ ## ❌ Won’t-Do Decisions
79
+
80
+ ### 1. Alternative scopes (request/session)
81
+ - **Decision**: no additional scopes beyond *singleton per container* will be implemented.
82
+ - **Rationale**:
83
+ * The simplicity of the current model (one instance per container) is a core value of pico-ioc.
84
+ * Web frameworks (Flask, FastAPI, etc.) already manage request/session lifecycles.
85
+ * Adding scopes would introduce unnecessary complexity and ambiguity about object ownership.
86
+
87
+ ---
88
+
89
+ ### 2. Asynchronous providers
90
+ - **Decision**: no support for `async def` components or asynchronous resolution inside the container.
91
+ - **Rationale**:
92
+ * Keeping the library **100% synchronous** preserves the current API (`container.get(...)` is always immediate).
93
+ * Async support would require event-loop integration, `await` semantics, and multiple runtime strategies → breaking simplicity.
94
+ * If a dependency needs async initialization, it should be handled inside the component itself, not by the container.
95
+
96
+ ---
97
+
98
+ ### 3. Hot reload / dynamic re-scan
99
+ - **Decision**: no hot reload or dynamic re-scan of modules will be supported.
100
+ - **Rationale**:
101
+ * Contradicts the **fail-fast** philosophy (surface errors at startup).
102
+ * Breaks the **determinism** guarantee (container is immutable after `init()`).
103
+ * Makes debugging harder: old instances may linger, state may become inconsistent, resources may leak.
104
+ * Development-time hot reload is already handled by frameworks (`uvicorn --reload`, etc.).
105
+
106
+ ---
107
+
108
+ 📌 **Summary:** pico-ioc stays **simple, deterministic, and fail-fast**.
109
+ Features that add complexity (alternative scopes, async providers, hot reload) are intentionally excluded.
110
+
111
+ ---
112
+
113
+ ## 📜 Changelog of Decisions
114
+
115
+ - **2025-08**: Dropped Python 3.8/3.9 support, minimum 3.10.
116
+ - **2025-08**: Clarified resolution order as *name-first*.
117
+ - **2025-08**: Documented lifecycle, plugins, and fail-fast policy.
118
+
@@ -0,0 +1,296 @@
1
+ # GUIDE.md — pico-ioc
2
+
3
+ > **Mission:** make dependency wiring trivial so you can ship faster and shorten feedback cycles.
4
+ > ⚠️ **Requires Python 3.10+** (uses `typing.Annotated` and `include_extras=True`).
5
+
6
+ This guide shows how to structure a Python app with **pico-ioc**: define components, provide dependencies, bootstrap a container, and run web/CLI code predictably.
7
+
8
+ ---
9
+
10
+ ## 1) Core concepts
11
+
12
+ * **Component**: a class managed by the container. Use `@component`.
13
+ * **Factory component**: a class that *provides* concrete instances (e.g., `Flask()`, clients). Use `@factory_component`.
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)`.
16
+
17
+ Resolution rule of thumb: **ask by type** (e.g., `container.get(Flask)` or inject `def __init__(..., app: Flask)`).
18
+
19
+ ---
20
+
21
+ ## 2) Quick start (Hello DI)
22
+
23
+ ```python
24
+ # app/config.py
25
+ from pico_ioc import component
26
+
27
+ @component
28
+ class Config:
29
+ DB_URL = "sqlite:///demo.db"
30
+ ````
31
+
32
+ ```python
33
+ # app/repo.py
34
+ from pico_ioc import component
35
+ from .config import Config
36
+
37
+ @component
38
+ class Repo:
39
+ def __init__(self, cfg: Config):
40
+ self._url = cfg.DB_URL
41
+ def fetch(self) -> str:
42
+ return f"fetching from {self._url}"
43
+ ```
44
+
45
+ ```python
46
+ # app/service.py
47
+ from pico_ioc import component
48
+ from .repo import Repo
49
+
50
+ @component
51
+ class Service:
52
+ def __init__(self, repo: Repo):
53
+ self.repo = repo
54
+ def run(self) -> str:
55
+ return self.repo.fetch()
56
+ ```
57
+
58
+ ```python
59
+ # main.py
60
+ from pico_ioc import init
61
+ import app
62
+
63
+ container = init(app) # build the container from the package
64
+ svc = container.get(app.service.Service)
65
+ print(svc.run()) # -> "fetching from sqlite:///demo.db"
66
+ ```
67
+
68
+ Run:
69
+
70
+ ```bash
71
+ python main.py
72
+ ```
73
+
74
+ ---
75
+
76
+ ## 3) Web example (Flask)
77
+
78
+ ```python
79
+ # app/app_factory.py
80
+ from pico_ioc import factory_component, provides
81
+ from flask import Flask
82
+
83
+ @factory_component
84
+ class AppFactory:
85
+ @provides(key=Flask)
86
+ def provide_flask(self) -> Flask:
87
+ app = Flask(__name__)
88
+ app.config["JSON_AS_ASCII"] = False
89
+ return app
90
+ ```
91
+
92
+ ```python
93
+ # app/api.py
94
+ from pico_ioc import component
95
+ from flask import Flask, jsonify
96
+
97
+ @component
98
+ class ApiApp:
99
+ def __init__(self, app: Flask):
100
+ self.app = app
101
+ self._routes()
102
+
103
+ def _routes(self):
104
+ @self.app.get("/health")
105
+ def health():
106
+ return jsonify(status="ok")
107
+ ```
108
+
109
+ ```python
110
+ # web.py
111
+ from pico_ioc import init
112
+ from flask import Flask
113
+ import app
114
+
115
+ c = init(app)
116
+ flask_app = c.get(Flask)
117
+
118
+ if __name__ == "__main__":
119
+ flask_app.run(host="0.0.0.0", port=5000)
120
+ ```
121
+
122
+ Run:
123
+
124
+ ```bash
125
+ python web.py
126
+ # GET http://localhost:5000/health -> {"status":"ok"}
127
+ ```
128
+
129
+ ---
130
+
131
+ ## 4) Configuration patterns
132
+
133
+ **Environment-backed config:**
134
+
135
+ ```python
136
+ # app/config.py
137
+ import os
138
+ from pico_ioc import component
139
+
140
+ @component
141
+ class Config:
142
+ WORKERS: int = int(os.getenv("WORKERS", "4"))
143
+ DEBUG: bool = os.getenv("DEBUG", "0") == "1"
144
+ ```
145
+
146
+ **Inject where needed** (constructor injection keeps code testable):
147
+
148
+ ```python
149
+ @component
150
+ class Runner:
151
+ def __init__(self, cfg: Config):
152
+ self._debug = cfg.DEBUG
153
+ ```
154
+
155
+ ---
156
+
157
+ ## 5) Testing & overrides
158
+
159
+ Define a **test factory** that provides fakes using the same keys:
160
+
161
+ ```python
162
+ # tests/test_overrides.py
163
+ from pico_ioc import factory_component, provides
164
+ from app.repo import Repo
165
+
166
+ class FakeRepo(Repo):
167
+ def fetch(self) -> str:
168
+ return "fake-data"
169
+
170
+ @factory_component
171
+ class TestOverrides:
172
+ @provides(key=Repo)
173
+ def provide_repo(self) -> Repo:
174
+ return FakeRepo()
175
+ ```
176
+
177
+ Build the container for tests with both packages (app + overrides):
178
+
179
+ ```python
180
+ # tests/test_service.py
181
+ from pico_ioc import init
182
+ import app
183
+ from tests import test_overrides
184
+
185
+ def test_service_fetch():
186
+ c = init([app, test_overrides]) # pass a list (composition root for tests)
187
+ svc = c.get(app.service.Service)
188
+ assert svc.run() == "fake-data"
189
+ ```
190
+
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.
192
+
193
+ ---
194
+
195
+ ## 5b) Qualifiers & collection injection
196
+
197
+ ```python
198
+ from typing import Protocol, Annotated
199
+ from pico_ioc import component, Qualifier, qualifier
200
+
201
+ class Handler(Protocol):
202
+ def handle(self, s: str) -> str: ...
203
+
204
+ PAYMENTS = Qualifier("payments")
205
+
206
+ @component
207
+ @qualifier(PAYMENTS)
208
+ class StripeHandler(Handler): ...
209
+
210
+ @component
211
+ @qualifier(PAYMENTS)
212
+ class PaypalHandler(Handler): ...
213
+
214
+ @component
215
+ class Orchestrator:
216
+ def __init__(self, handlers: list[Annotated[Handler, PAYMENTS]]):
217
+ self.handlers = handlers
218
+
219
+ def run(self, s: str) -> list[str]:
220
+ return [h.handle("ok") for h in self.handlers]
221
+ ```
222
+
223
+ If you request `list[Handler]` you get **all** implementations.
224
+ If you request `list[Annotated[Handler, PAYMENTS]]`, you only get the tagged ones.
225
+
226
+ ---
227
+
228
+ ## 5c) Plugins & Public API helper
229
+
230
+ ```python
231
+ from pico_ioc import plugin
232
+ from pico_ioc.plugins import PicoPlugin
233
+
234
+ @plugin
235
+ class TracingPlugin(PicoPlugin):
236
+ def before_scan(self, package, binder):
237
+ print(f"Scanning {package}")
238
+ def after_ready(self, container, binder):
239
+ print("Container ready")
240
+ ```
241
+
242
+ Register explicitly:
243
+
244
+ ```python
245
+ from pico_ioc import init
246
+ import app
247
+
248
+ c = init(app, plugins=(TracingPlugin(),))
249
+ ```
250
+
251
+ And to expose your app’s API cleanly:
252
+
253
+ ```python
254
+ # app/__init__.py
255
+ from pico_ioc.public_api import export_public_symbols_decorated
256
+ __getattr__, __dir__ = export_public_symbols_decorated("app", include_plugins=True)
257
+ ```
258
+
259
+ Now you can import directly:
260
+
261
+ ```python
262
+ from app import Service, Config, TracingPlugin
263
+ ```
264
+
265
+ ---
266
+
267
+ ## 6) Tips & guardrails
268
+
269
+ * **Ask by type**: inject `Flask`, `Config`, `Repo` instead of strings.
270
+ * **Keep constructors cheap**: do not perform I/O in `__init__`.
271
+ * **Small components**: one responsibility per component; wire them in service classes.
272
+ * **Factories provide externals**: frameworks, clients, DB engines belong in `@factory_component` providers.
273
+ * **Fail fast**: build the container at startup and crash early if a dependency is missing.
274
+ * **No globals**: let the container own lifecycle; fetch via `container.get(...)` only at the edges (bootstrap).
275
+
276
+ ---
277
+
278
+ ## 7) Troubleshooting
279
+
280
+ * **“No provider for X”**
281
+ Ensure a `@provides(key=X)` exists in a module passed to `init(...)`, and your constructor type annotation is exactly `X`.
282
+
283
+ * **Wrong instance injected**
284
+ Check for duplicate providers for the same key. The last registered wins; control order via the list passed to `init([module_a, module_b])`.
285
+
286
+ * **Circular imports**
287
+ Split components or move heavy imports into providers. Keep modules acyclic where possible.
288
+
289
+ * **Flask not found**
290
+ Verify `from flask import Flask` and that your factory uses `@provides(key=Flask)`. Resolve it with `container.get(Flask)`.
291
+
292
+ ---
293
+
294
+ **TL;DR**
295
+ 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
+