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.
- {pico_ioc-0.5.2 → pico_ioc-1.0.0}/.github/workflows/ci.yml +1 -1
- pico_ioc-1.0.0/.llm/ARCHITECTURE.md +282 -0
- pico_ioc-1.0.0/.llm/DECISIONS.md +118 -0
- pico_ioc-1.0.0/.llm/GUIDE.md +296 -0
- pico_ioc-1.0.0/.llm/OVERVIEW.md +112 -0
- pico_ioc-1.0.0/PKG-INFO +145 -0
- pico_ioc-1.0.0/README.md +100 -0
- {pico_ioc-0.5.2 → pico_ioc-1.0.0}/pyproject.toml +1 -3
- pico_ioc-1.0.0/src/pico_ioc/__init__.py +31 -0
- pico_ioc-1.0.0/src/pico_ioc/_state.py +10 -0
- pico_ioc-1.0.0/src/pico_ioc/_version.py +1 -0
- pico_ioc-1.0.0/src/pico_ioc/api.py +128 -0
- pico_ioc-1.0.0/src/pico_ioc/container.py +155 -0
- pico_ioc-1.0.0/src/pico_ioc/decorators.py +76 -0
- pico_ioc-1.0.0/src/pico_ioc/plugins.py +12 -0
- pico_ioc-1.0.0/src/pico_ioc/proxy.py +77 -0
- pico_ioc-1.0.0/src/pico_ioc/public_api.py +76 -0
- pico_ioc-1.0.0/src/pico_ioc/resolver.py +110 -0
- pico_ioc-1.0.0/src/pico_ioc/scanner.py +238 -0
- pico_ioc-1.0.0/src/pico_ioc/typing_utils.py +29 -0
- pico_ioc-1.0.0/src/pico_ioc.egg-info/PKG-INFO +145 -0
- pico_ioc-1.0.0/src/pico_ioc.egg-info/SOURCES.txt +41 -0
- pico_ioc-1.0.0/tests/test_api_unit.py +123 -0
- pico_ioc-1.0.0/tests/test_container_get_all.py +23 -0
- pico_ioc-1.0.0/tests/test_container_unit.py +125 -0
- pico_ioc-1.0.0/tests/test_decorators_unit.py +138 -0
- {pico_ioc-0.5.2 → pico_ioc-1.0.0}/tests/test_pico_ioc.py +4 -1
- {pico_ioc-0.5.2 → pico_ioc-1.0.0}/tests/test_pico_ioc_additional.py +8 -2
- pico_ioc-1.0.0/tests/test_pico_ioc_discovery.py +114 -0
- pico_ioc-1.0.0/tests/test_proxy_unit.py +322 -0
- pico_ioc-1.0.0/tests/test_public_api.py +220 -0
- pico_ioc-1.0.0/tests/test_qualifiers_unit.py +70 -0
- pico_ioc-1.0.0/tests/test_resolver_unit.py +121 -0
- pico_ioc-1.0.0/tests/test_scanner_unit.py +190 -0
- pico_ioc-1.0.0/tests/test_typing_utils_unit.py +102 -0
- {pico_ioc-0.5.2 → pico_ioc-1.0.0}/tox.ini +1 -1
- pico_ioc-0.5.2/PKG-INFO +0 -278
- pico_ioc-0.5.2/README.md +0 -231
- pico_ioc-0.5.2/src/pico_ioc/__init__.py +0 -408
- pico_ioc-0.5.2/src/pico_ioc/_version.py +0 -1
- pico_ioc-0.5.2/src/pico_ioc.egg-info/PKG-INFO +0 -278
- pico_ioc-0.5.2/src/pico_ioc.egg-info/SOURCES.txt +0 -16
- {pico_ioc-0.5.2 → pico_ioc-1.0.0}/.coveragerc +0 -0
- {pico_ioc-0.5.2 → pico_ioc-1.0.0}/.github/workflows/publish-to-pypi.yml +0 -0
- {pico_ioc-0.5.2 → pico_ioc-1.0.0}/LICENSE +0 -0
- {pico_ioc-0.5.2 → pico_ioc-1.0.0}/MANIFEST.in +0 -0
- {pico_ioc-0.5.2 → pico_ioc-1.0.0}/setup.cfg +0 -0
- {pico_ioc-0.5.2 → pico_ioc-1.0.0}/src/pico_ioc.egg-info/dependency_links.txt +0 -0
- {pico_ioc-0.5.2 → pico_ioc-1.0.0}/src/pico_ioc.egg-info/top_level.txt +0 -0
|
@@ -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
|
+
|