pico-ioc 1.3.0__tar.gz → 1.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pico_ioc-1.3.0 → pico_ioc-1.4.0}/.llm/ARCHITECTURE.md +73 -32
- {pico_ioc-1.3.0 → pico_ioc-1.4.0}/.llm/DECISIONS.md +11 -0
- pico_ioc-1.4.0/.llm/GUIDE-CONFIGURATION-INJECTION.md +129 -0
- {pico_ioc-1.3.0 → pico_ioc-1.4.0}/.llm/GUIDE.md +35 -5
- pico_ioc-1.4.0/.llm/OVERVIEW.md +167 -0
- {pico_ioc-1.3.0 → pico_ioc-1.4.0}/CHANGELOG.md +20 -4
- {pico_ioc-1.3.0 → pico_ioc-1.4.0}/PKG-INFO +7 -1
- {pico_ioc-1.3.0 → pico_ioc-1.4.0}/README.md +6 -0
- {pico_ioc-1.3.0 → pico_ioc-1.4.0}/src/pico_ioc/__init__.py +13 -0
- pico_ioc-1.4.0/src/pico_ioc/_state.py +75 -0
- pico_ioc-1.4.0/src/pico_ioc/_version.py +1 -0
- {pico_ioc-1.3.0 → pico_ioc-1.4.0}/src/pico_ioc/api.py +36 -55
- {pico_ioc-1.3.0 → pico_ioc-1.4.0}/src/pico_ioc/builder.py +93 -41
- pico_ioc-1.4.0/src/pico_ioc/config.py +332 -0
- {pico_ioc-1.3.0 → pico_ioc-1.4.0}/src/pico_ioc/container.py +30 -11
- {pico_ioc-1.3.0 → pico_ioc-1.4.0}/src/pico_ioc/decorators.py +30 -6
- {pico_ioc-1.3.0 → pico_ioc-1.4.0}/src/pico_ioc/interceptors.py +13 -7
- pico_ioc-1.4.0/src/pico_ioc/policy.py +245 -0
- {pico_ioc-1.3.0 → pico_ioc-1.4.0}/src/pico_ioc/proxy.py +30 -18
- {pico_ioc-1.3.0 → pico_ioc-1.4.0}/src/pico_ioc/resolver.py +28 -25
- {pico_ioc-1.3.0 → pico_ioc-1.4.0}/src/pico_ioc/scanner.py +21 -21
- pico_ioc-1.4.0/src/pico_ioc/scope.py +46 -0
- {pico_ioc-1.3.0 → pico_ioc-1.4.0}/src/pico_ioc.egg-info/PKG-INFO +7 -1
- pico_ioc-1.4.0/src/pico_ioc.egg-info/SOURCES.txt +69 -0
- pico_ioc-1.4.0/tests/test_api.py +269 -0
- pico_ioc-1.4.0/tests/test_conditional_with_predicate.py +42 -0
- pico_ioc-1.4.0/tests/test_config_injection.py +176 -0
- pico_ioc-1.4.0/tests/test_container.py +204 -0
- pico_ioc-1.4.0/tests/test_core_helpers_and_errors.py +86 -0
- pico_ioc-1.4.0/tests/test_decorators_unit.py +138 -0
- pico_ioc-1.4.0/tests/test_defaults_and_overrides.py +71 -0
- pico_ioc-1.4.0/tests/test_factory_policy_and_defaults.py +179 -0
- {pico_ioc-1.3.0 → pico_ioc-1.4.0}/tests/test_fingerprint_public.py +8 -4
- pico_ioc-1.4.0/tests/test_interceptors.py +269 -0
- pico_ioc-1.4.0/tests/test_no_overrides_needed_with_on_missing.py +30 -0
- pico_ioc-1.4.0/tests/test_on_missing_and_primary_mix.py +29 -0
- pico_ioc-1.4.0/tests/test_on_missing_blackbox.py +90 -0
- pico_ioc-1.4.0/tests/test_on_missing_component.py +50 -0
- pico_ioc-1.4.0/tests/test_on_missing_factory.py +29 -0
- pico_ioc-1.4.0/tests/test_pico_ioc.py +280 -0
- pico_ioc-1.4.0/tests/test_pico_ioc_additional.py +192 -0
- pico_ioc-1.4.0/tests/test_pico_ioc_discovery.py +114 -0
- pico_ioc-1.4.0/tests/test_policy_and_container_helpers.py +106 -0
- pico_ioc-1.4.0/tests/test_policy_env_activation.py +33 -0
- pico_ioc-1.4.0/tests/test_policy_profile_primary.py +35 -0
- pico_ioc-1.4.0/tests/test_proxy_unit.py +188 -0
- pico_ioc-1.4.0/tests/test_public_api.py +220 -0
- pico_ioc-1.4.0/tests/test_qualifiers_unit.py +70 -0
- pico_ioc-1.4.0/tests/test_resolver_unit.py +121 -0
- pico_ioc-1.4.0/tests/test_scanner_providers.py +151 -0
- pico_ioc-1.4.0/tests/test_scanner_unit.py +190 -0
- pico_ioc-1.4.0/tests/test_scope_defaults.py +69 -0
- pico_ioc-1.4.0/tests/test_scope_defaults_and_policy.py +38 -0
- pico_ioc-1.4.0/tests/test_scope_unit.py +232 -0
- pico_ioc-1.3.0/.llm/OVERVIEW.md +0 -143
- pico_ioc-1.3.0/src/pico_ioc/_state.py +0 -40
- pico_ioc-1.3.0/src/pico_ioc/_version.py +0 -1
- pico_ioc-1.3.0/src/pico_ioc/policy.py +0 -332
- pico_ioc-1.3.0/src/pico_ioc.egg-info/SOURCES.txt +0 -39
- pico_ioc-1.3.0/tests/test_api.py +0 -145
- pico_ioc-1.3.0/tests/test_interceptors_autoreg.py +0 -214
- {pico_ioc-1.3.0 → pico_ioc-1.4.0}/.coveragerc +0 -0
- {pico_ioc-1.3.0 → pico_ioc-1.4.0}/.github/workflows/ci.yml +0 -0
- {pico_ioc-1.3.0 → pico_ioc-1.4.0}/.github/workflows/publish-to-pypi.yml +0 -0
- {pico_ioc-1.3.0 → pico_ioc-1.4.0}/.llm/GUIDE_CQRS.md +0 -0
- {pico_ioc-1.3.0 → pico_ioc-1.4.0}/.llm/GUIDE_CREATING_PLUGINS_AND_INTERCEPTORS.md +0 -0
- {pico_ioc-1.3.0 → pico_ioc-1.4.0}/LICENSE +0 -0
- {pico_ioc-1.3.0 → pico_ioc-1.4.0}/MANIFEST.in +0 -0
- {pico_ioc-1.3.0 → pico_ioc-1.4.0}/pyproject.toml +0 -0
- {pico_ioc-1.3.0 → pico_ioc-1.4.0}/setup.cfg +0 -0
- {pico_ioc-1.3.0 → pico_ioc-1.4.0}/src/pico_ioc/plugins.py +0 -0
- {pico_ioc-1.3.0 → pico_ioc-1.4.0}/src/pico_ioc/public_api.py +0 -0
- {pico_ioc-1.3.0 → pico_ioc-1.4.0}/src/pico_ioc/utils.py +0 -0
- {pico_ioc-1.3.0 → pico_ioc-1.4.0}/src/pico_ioc.egg-info/dependency_links.txt +0 -0
- {pico_ioc-1.3.0 → pico_ioc-1.4.0}/src/pico_ioc.egg-info/top_level.txt +0 -0
- {pico_ioc-1.3.0 → pico_ioc-1.4.0}/tests/test_decorators_and_policy.py +0 -0
- {pico_ioc-1.3.0 → pico_ioc-1.4.0}/tests/test_scope.py +0 -0
- {pico_ioc-1.3.0 → pico_ioc-1.4.0}/tox.ini +0 -0
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
>
|
|
6
6
|
> ⚠️ **Requires Python 3.10+** (uses `typing.Annotated` with `include_extras=True`).
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
---
|
|
9
9
|
|
|
10
10
|
## 1\) Design goals & non-goals
|
|
11
11
|
|
|
@@ -23,11 +23,12 @@
|
|
|
23
23
|
- Hot reload or runtime graph mutation beyond explicit overrides.
|
|
24
24
|
- Magical filesystem-wide auto-imports.
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
---
|
|
27
27
|
|
|
28
28
|
## 2\) High-level model
|
|
29
29
|
|
|
30
30
|
- **Component** → class marked with `@component`. Instantiated by the container.
|
|
31
|
+
- **Config Component** → class marked with `@config_component`. Instantiated and populated from external sources like files or environment variables.
|
|
31
32
|
- **Factory component** → class marked with `@factory_component`; owns provider methods via `@provides(key=TypeOrToken)`. Providers return *externals* (e.g., `Flask`, DB clients).
|
|
32
33
|
- **Interceptor** → class or function marked with `@interceptor`. Discovered automatically to apply cross-cutting logic.
|
|
33
34
|
- **Container** → built by `pico_ioc.init(mod_or_list, ...)`; resolve with `container.get(KeyOrType)`.
|
|
@@ -38,8 +39,9 @@
|
|
|
38
39
|
sequenceDiagram
|
|
39
40
|
participant App as Your package(s)
|
|
40
41
|
participant IOC as pico-ioc Container
|
|
41
|
-
App->>IOC: init(packages, ...)
|
|
42
|
-
IOC->>
|
|
42
|
+
App->>IOC: init(packages, config, ...)
|
|
43
|
+
IOC->>IOC: Create ConfigRegistry from sources
|
|
44
|
+
IOC->>App: scan decorators (@component, @config_component, @interceptor)
|
|
43
45
|
IOC->>IOC: register providers and collect interceptor declarations
|
|
44
46
|
IOC->>IOC: build and activate interceptors
|
|
45
47
|
IOC->>IOC: apply policy (e.g., @primary, @on_missing aliases)
|
|
@@ -50,13 +52,14 @@ sequenceDiagram
|
|
|
50
52
|
IOC-->>App: instance(Service)
|
|
51
53
|
```
|
|
52
54
|
|
|
53
|
-
|
|
55
|
+
---
|
|
54
56
|
|
|
55
57
|
## 3\) Discovery & registration
|
|
56
58
|
|
|
57
59
|
1. **Scan inputs** passed to `init(...)`: module or list of modules/packages.
|
|
58
60
|
2. **Collect**:
|
|
59
61
|
* `@component` classes → registered by a **key** (defaults to the class type).
|
|
62
|
+
* `@config_component` classes → registered as special components whose instances are built from external configuration sources.
|
|
60
63
|
* `@factory_component` classes → introspected for `@provides(key=...)` methods.
|
|
61
64
|
* `@interceptor` classes/functions → collected for activation.
|
|
62
65
|
* `@plugin` classes → if explicitly passed via `init(..., plugins=(...))`.
|
|
@@ -65,7 +68,7 @@ sequenceDiagram
|
|
|
65
68
|
|
|
66
69
|
**Precedence:** If multiple providers are active for the same key (e.g., one with `@primary`, another regular), a deterministic policy is applied to choose the winner. Direct overrides are applied last, having the final say.
|
|
67
70
|
|
|
68
|
-
|
|
71
|
+
---
|
|
69
72
|
|
|
70
73
|
## 4\) Resolution algorithm (deterministic)
|
|
71
74
|
|
|
@@ -94,7 +97,7 @@ If the constructor requests `list[T]` or `list[Annotated[T, Q]]`:
|
|
|
94
97
|
* Registration order is preserved; no implicit sorting.
|
|
95
98
|
* Returns an empty list if no matches.
|
|
96
99
|
|
|
97
|
-
|
|
100
|
+
---
|
|
98
101
|
|
|
99
102
|
## 5\) Lifecycles & scopes
|
|
100
103
|
|
|
@@ -103,7 +106,7 @@ If the constructor requests `list[T]` or `list[Annotated[T, Q]]`:
|
|
|
103
106
|
|
|
104
107
|
**Rationale:** Most Python app composition (config, clients, web apps) fits singleton-per-container; it’s simple and fast.
|
|
105
108
|
|
|
106
|
-
|
|
109
|
+
---
|
|
107
110
|
|
|
108
111
|
## 6\) Factories & providers
|
|
109
112
|
|
|
@@ -127,7 +130,7 @@ Guidelines:
|
|
|
127
130
|
* Providers should be **pure constructors** (no long-running work).
|
|
128
131
|
* Prefer **typed keys** (e.g., `Flask`) over strings.
|
|
129
132
|
|
|
130
|
-
|
|
133
|
+
---
|
|
131
134
|
|
|
132
135
|
## 7\) Concurrency model
|
|
133
136
|
|
|
@@ -135,7 +138,7 @@ Guidelines:
|
|
|
135
138
|
* Caches & resolution are **thread/async safe** (internal isolation; no global singletons).
|
|
136
139
|
* Instances you create **must** be safe for your usage patterns; the container cannot fix non-thread-safe libraries.
|
|
137
140
|
|
|
138
|
-
|
|
141
|
+
---
|
|
139
142
|
|
|
140
143
|
## 8\) Error handling & diagnostics
|
|
141
144
|
|
|
@@ -147,22 +150,60 @@ Guidelines:
|
|
|
147
150
|
|
|
148
151
|
**Tip:** Keep constructors **cheap**; push I/O to explicit start/serve methods.
|
|
149
152
|
|
|
150
|
-
|
|
153
|
+
---
|
|
151
154
|
|
|
152
155
|
## 9\) Configuration
|
|
153
156
|
|
|
154
|
-
|
|
157
|
+
Configuration is treated as a first-class, type-safe component using a dedicated injection system.
|
|
155
158
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
159
|
+
1. **Define a Config Class**: Create a class (preferably a `dataclass`) and mark it with `@config_component`. An optional `prefix` can be used for environment variables.
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
from pico_ioc import config_component
|
|
163
|
+
from dataclasses import dataclass
|
|
164
|
+
|
|
165
|
+
@config_component(prefix="APP_")
|
|
166
|
+
@dataclass(frozen=True)
|
|
167
|
+
class Settings:
|
|
168
|
+
db_url: str
|
|
169
|
+
timeout: int = 30
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
2. **Provide Sources**: At bootstrap, pass an ordered tuple of `ConfigSource` objects to the `config` parameter of `init()`. The order defines precedence (first source wins).
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
from pico_ioc import init
|
|
176
|
+
from pico_ioc.config import EnvSource, FileSource
|
|
177
|
+
|
|
178
|
+
container = init(
|
|
179
|
+
"my_app",
|
|
180
|
+
config=(
|
|
181
|
+
EnvSource(prefix="APP_"), # Highest priority
|
|
182
|
+
FileSource("config.prod.yml", optional=True),
|
|
183
|
+
FileSource("config.yml"), # Lowest priority
|
|
184
|
+
),
|
|
185
|
+
)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
3. **Inject and Use**: Inject the config class into other components just like any other dependency.
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
from pico_ioc import component
|
|
192
|
+
|
|
193
|
+
@component
|
|
194
|
+
class Database:
|
|
195
|
+
def __init__(self, settings: Settings):
|
|
196
|
+
self.connection = connect(settings.db_url)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Resolution Logic
|
|
200
|
+
|
|
201
|
+
- **Automatic Binding**: By default, `pico-ioc` binds fields automatically. For a field like `db_url`, it checks for keys like `APP_DB_URL` (in `EnvSource`), `DB_URL`, or `db_url` (in `FileSource`).
|
|
202
|
+
- **Manual Overrides**: For more complex cases where keys don't align, you can use field-level helpers like `Env["CUSTOM_VAR"]`, `File["key.in.file"]`, or `Path.file["nested.key"]` to specify the exact key to use.
|
|
162
203
|
|
|
163
|
-
|
|
204
|
+
This system ensures that configuration is **type-safe**, **externalized**, and **testable**, while remaining simple for the common cases.
|
|
164
205
|
|
|
165
|
-
|
|
206
|
+
---
|
|
166
207
|
|
|
167
208
|
## 10\) Overrides & composition
|
|
168
209
|
|
|
@@ -176,9 +217,9 @@ The policy engine respects definition order. While not a strict "last-wins", pro
|
|
|
176
217
|
|
|
177
218
|
```python
|
|
178
219
|
c = init(app, overrides={
|
|
179
|
-
Repo: FakeRepo(),
|
|
180
|
-
"fast_model": lambda: {"mock": True},
|
|
181
|
-
"expensive": (lambda: object(), True),
|
|
220
|
+
Repo: FakeRepo(), # constant instance
|
|
221
|
+
"fast_model": lambda: {"mock": True}, # provider
|
|
222
|
+
"expensive": (lambda: object(), True), # provider with lazy=True
|
|
182
223
|
})
|
|
183
224
|
```
|
|
184
225
|
|
|
@@ -191,7 +232,7 @@ c = init(app, overrides={
|
|
|
191
232
|
* `key: (callable, lazy_bool)`
|
|
192
233
|
* With `reuse=True`, re-calling `init(..., overrides=...)` applies new overrides to the cached container.
|
|
193
234
|
|
|
194
|
-
|
|
235
|
+
---
|
|
195
236
|
|
|
196
237
|
## 11\) Interceptors (AOP & Lifecycle Hooks)
|
|
197
238
|
|
|
@@ -229,7 +270,7 @@ These implement the `ContainerInterceptor` protocol and hook into the container'
|
|
|
229
270
|
|
|
230
271
|
**Registration:** Interceptors are discovered by the scanner during `init()` or `scope()`. There is no need to pass them manually. Their activation can be controlled with the same `@conditional` decorator and gates (`profiles`, `require_env`) used for other components.
|
|
231
272
|
|
|
232
|
-
|
|
273
|
+
---
|
|
233
274
|
|
|
234
275
|
## 12\) Profiles & conditional providers
|
|
235
276
|
|
|
@@ -260,7 +301,7 @@ class InMemoryCache(Cache): ...
|
|
|
260
301
|
* `predicate=callable` → must return a truthy value to activate.
|
|
261
302
|
* If no active provider satisfies a required type and something depends on it → **bootstrap error** (fail fast).
|
|
262
303
|
|
|
263
|
-
|
|
304
|
+
---
|
|
264
305
|
|
|
265
306
|
## 13\) Qualifiers & collection injection
|
|
266
307
|
|
|
@@ -271,7 +312,7 @@ Attach qualifiers to group/select implementations using `@qualifier`.
|
|
|
271
312
|
|
|
272
313
|
This preserves registration order and returns a stable list.
|
|
273
314
|
|
|
274
|
-
|
|
315
|
+
---
|
|
275
316
|
|
|
276
317
|
## 14\) Plugins
|
|
277
318
|
|
|
@@ -285,7 +326,7 @@ This preserves registration order and returns a stable list.
|
|
|
285
326
|
|
|
286
327
|
Plugins are passed **explicitly** via `init(..., plugins=(MyPlugin(),))`. Prefer **interceptors** for fine-grained wiring events; use **plugins** for coarse lifecycle integration.
|
|
287
328
|
|
|
288
|
-
|
|
329
|
+
---
|
|
289
330
|
|
|
290
331
|
## 15\) Scoped subgraphs (`scope`)
|
|
291
332
|
|
|
@@ -320,7 +361,7 @@ Providers may carry `tags` (via `@component(tags=...)` or `@provides(..., tags=.
|
|
|
320
361
|
|
|
321
362
|
**Use cases:** fast unit tests, integration-lite, CLI tools, microbenchmarks.
|
|
322
363
|
|
|
323
|
-
|
|
364
|
+
---
|
|
324
365
|
|
|
325
366
|
## 16\) Diagnostics & diagrams
|
|
326
367
|
|
|
@@ -371,7 +412,7 @@ flowchart TD
|
|
|
371
412
|
G --> Z
|
|
372
413
|
```
|
|
373
414
|
|
|
374
|
-
|
|
415
|
+
---
|
|
375
416
|
|
|
376
417
|
## 17\) Rationale & trade-offs
|
|
377
418
|
|
|
@@ -381,7 +422,7 @@ flowchart TD
|
|
|
381
422
|
* **Fail fast**: configuration and graph issues surface at startup, not mid-request.
|
|
382
423
|
* **Interceptors over AOP**: precise, opt-in hooks without full-blown aspect weavers.
|
|
383
424
|
|
|
384
|
-
|
|
425
|
+
---
|
|
385
426
|
|
|
386
427
|
**TL;DR**
|
|
387
|
-
`pico-ioc` builds a **deterministic, typed dependency graph** from decorated components, factories, and interceptors. It resolves by **type** (with qualifiers and collections), memoizes **singletons**, supports **overrides**, **plugins**, **conditionals/profiles**, and **scoped subgraphs**—keeping wiring **predictable, testable, and framework-agnostic**.
|
|
428
|
+
`pico-ioc` builds a **deterministic, typed dependency graph** from decorated components, factories, and interceptors. It resolves by **type** (with qualifiers and collections), memoizes **singletons**, supports **type-safe configuration injection**, **overrides**, **plugins**, **conditionals/profiles**, and **scoped subgraphs**—keeping wiring **predictable, testable, and framework-agnostic**.
|
|
@@ -131,6 +131,17 @@ A true "last-wins" only occurs when binding the *exact same key* multiple times,
|
|
|
131
131
|
|
|
132
132
|
---
|
|
133
133
|
|
|
134
|
+
### 15) Configuration Injection
|
|
135
|
+
**Decision**: Provide `@config_component` for strongly typed configuration classes, populated from ordered `ConfigSource`s (`EnvSource`, `FileSource`, etc.).
|
|
136
|
+
**Rationale**: Type-safe configuration with minimal boilerplate, supporting both automatic autowiring by field name and manual overrides (`Env`, `File`, `Path`, `Value`).
|
|
137
|
+
**Implications**:
|
|
138
|
+
- Precedence is explicit: `overrides` > sources (in order) > field defaults.
|
|
139
|
+
- Missing required fields (no default and not resolvable) raise `NameError`.
|
|
140
|
+
- Supported formats: env vars, JSON, INI, dotenv, YAML (if available).
|
|
141
|
+
- Encourages using `dataclass(frozen=True)` for immutable, validated settings.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
134
145
|
## ❌ Won’t-Do Decisions
|
|
135
146
|
|
|
136
147
|
### A) Alternative scopes (request/session)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# GUIDE-CONFIGURATION-INJECTION
|
|
2
|
+
|
|
3
|
+
`pico-ioc` includes a powerful and flexible configuration injection system that allows you to decouple your application's configuration in a type-safe way. You can load values from environment variables, files (YAML, JSON, INI, .env), and more.
|
|
4
|
+
|
|
5
|
+
## 1\. Basic Concepts
|
|
6
|
+
|
|
7
|
+
Configuration injection is built on three pillars:
|
|
8
|
+
|
|
9
|
+
1. **`@config_component`**: A decorator to mark a class (preferably a `dataclass`) that will hold your configuration values.
|
|
10
|
+
2. **`ConfigSource`**: Objects that tell `pico-ioc` *where* to find configuration values (e.g., `EnvSource` for environment variables, `FileSource` for files).
|
|
11
|
+
3. **The `config` parameter in `pico.init()`**: An ordered tuple of `ConfigSource`s. The first source that contains a key wins.
|
|
12
|
+
|
|
13
|
+
## 2\. Basic Usage: Autowiring
|
|
14
|
+
|
|
15
|
+
In the most common use case, `pico-ioc` can populate the fields of your configuration class automatically.
|
|
16
|
+
|
|
17
|
+
#### Step 1: Define Your Configuration Class
|
|
18
|
+
|
|
19
|
+
Use `@config_component` and, optionally, a prefix for environment variables.
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
# in settings.py
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from pico_ioc import config_component
|
|
25
|
+
|
|
26
|
+
@config_component(prefix="APP_")
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class Settings:
|
|
29
|
+
# Will look for APP_DB_URL or DB_URL in the environment, or db_url in files.
|
|
30
|
+
db_url: str
|
|
31
|
+
|
|
32
|
+
# Will look for APP_TIMEOUT or TIMEOUT, or timeout in files.
|
|
33
|
+
timeout: int = 10 # Default value if not found in any source
|
|
34
|
+
|
|
35
|
+
# Will look for APP_DEBUG or DEBUG, or debug in files.
|
|
36
|
+
debug: bool = False
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
#### Step 2: Provide the Sources on Initialization
|
|
40
|
+
|
|
41
|
+
Imagine you have these files:
|
|
42
|
+
|
|
43
|
+
**`config.yml`**
|
|
44
|
+
|
|
45
|
+
```yaml
|
|
46
|
+
db_url: "postgresql://user:pass@host:5432/prod_db"
|
|
47
|
+
timeout: 30
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Environment Variables**
|
|
51
|
+
|
|
52
|
+
```shell
|
|
53
|
+
export APP_DEBUG=true
|
|
54
|
+
export APP_DB_URL="postgresql://user:pass@host:5432/env_db"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Now, initialize the container, specifying the priority order of the sources.
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
# in main.py
|
|
61
|
+
from pico_ioc import init
|
|
62
|
+
from pico_ioc.config import EnvSource, FileSource
|
|
63
|
+
from .settings import Settings
|
|
64
|
+
|
|
65
|
+
container = init(
|
|
66
|
+
__name__,
|
|
67
|
+
config=(
|
|
68
|
+
# 1. First, look in environment variables with the "APP_" prefix
|
|
69
|
+
EnvSource(prefix="APP_"),
|
|
70
|
+
|
|
71
|
+
# 2. If not found, look in config.yml
|
|
72
|
+
FileSource("config.yml"),
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Request your configuration like any other component!
|
|
77
|
+
settings = container.get(Settings)
|
|
78
|
+
|
|
79
|
+
print(f"Database URL: {settings.db_url}") # -> postgresql://user:pass@host:5432/env_db (from environment)
|
|
80
|
+
print(f"Timeout: {settings.timeout}") # -> 30 (from file, as it's not in the environment)
|
|
81
|
+
print(f"Debug: {settings.debug}") # -> True (from environment)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## 3\. Advanced Usage: Manual Field Overrides
|
|
85
|
+
|
|
86
|
+
Sometimes, the field name in your class doesn't match the key in the configuration source, or you need more granular control. For that, you can use the `Env`, `File`, `Path`, and `Value` helpers.
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from dataclasses import dataclass
|
|
90
|
+
from pico_ioc import config_component
|
|
91
|
+
from pico_ioc.config import Env, File, Path, Value
|
|
92
|
+
|
|
93
|
+
@config_component
|
|
94
|
+
@dataclass(frozen=True)
|
|
95
|
+
class AdvancedSettings:
|
|
96
|
+
# 1. Only look in environment variables with a specific name
|
|
97
|
+
api_key: str = Env["THIRD_PARTY_API_KEY"]
|
|
98
|
+
|
|
99
|
+
# 2. Only look for a top-level key in files
|
|
100
|
+
pool_size: int = File["database.pool.size", 10] # With a default value
|
|
101
|
+
|
|
102
|
+
# 3. Look for a nested path within a structured file (YAML/JSON)
|
|
103
|
+
# Will look in config.yml -> services -> auth -> url
|
|
104
|
+
auth_service_url: str = Path.file["services.auth.url"]
|
|
105
|
+
|
|
106
|
+
# 4. Control the source order for a specific field
|
|
107
|
+
# For this field, the file has precedence over the environment.
|
|
108
|
+
region: str = Value["aws.region", sources=("file", "env"), default="eu-west-1"]
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## 4\. Configuration Sources (`ConfigSource`)
|
|
112
|
+
|
|
113
|
+
`pico-ioc` comes with two primary sources:
|
|
114
|
+
|
|
115
|
+
* `EnvSource(prefix: str = "")`: Reads from environment variables. The `prefix` is optional but recommended. It will look for `PREFIX_KEY` and then `KEY`.
|
|
116
|
+
* `FileSource(path: str, optional: bool = False)`: Reads from a file.
|
|
117
|
+
* **Supported formats**: Automatically detects YAML, JSON, INI, and `.env` (`KEY=VALUE` format).
|
|
118
|
+
* **`optional=True`**: If the file does not exist, it won't raise an error. This is very useful for environment-specific configuration files (e.g., `config.local.yml`).
|
|
119
|
+
|
|
120
|
+
## 5\. Summary of Precedence Rules
|
|
121
|
+
|
|
122
|
+
For a given configuration field, the value is resolved in the following order:
|
|
123
|
+
|
|
124
|
+
1. **Manual Override (`Env`, `File`, etc.)**: If used, its specific rules are followed.
|
|
125
|
+
2. **Automatic Lookup**:
|
|
126
|
+
1. It searches in the **first `ConfigSource`** provided in the `config` tuple during `init()`.
|
|
127
|
+
2. If not found, it checks the **second `ConfigSource`**, and so on.
|
|
128
|
+
3. **Python Default Value**: If the value is not found in any source, the default value defined in the class is used (e.g., `timeout: int = 10`).
|
|
129
|
+
4. **Error**: If the value isn't found in any source and the field has no default, `pico-ioc` will raise a `NameError` when trying to create the instance. Fail-fast\!
|
|
@@ -116,9 +116,12 @@ if __name__ == "__main__":
|
|
|
116
116
|
|
|
117
117
|
-----
|
|
118
118
|
|
|
119
|
-
## 4
|
|
119
|
+
## 4) Configuration patterns
|
|
120
120
|
|
|
121
|
-
**
|
|
121
|
+
You can either hardcode config with `os.getenv` **or** use the new
|
|
122
|
+
**configuration injection system** for type-safe settings.
|
|
123
|
+
|
|
124
|
+
### 4.1 Env-backed config (manual)
|
|
122
125
|
|
|
123
126
|
```python
|
|
124
127
|
import os
|
|
@@ -130,15 +133,42 @@ class Config:
|
|
|
130
133
|
DEBUG: bool = os.getenv("DEBUG", "0") == "1"
|
|
131
134
|
```
|
|
132
135
|
|
|
136
|
+
### 4.2 Config injection (recommended)
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from dataclasses import dataclass
|
|
140
|
+
from pico_ioc import config_component
|
|
141
|
+
from pico_ioc.config import EnvSource, FileSource
|
|
142
|
+
|
|
143
|
+
@config_component(prefix="APP_")
|
|
144
|
+
@dataclass(frozen=True)
|
|
145
|
+
class Settings:
|
|
146
|
+
db_url: str
|
|
147
|
+
timeout: int = 10
|
|
148
|
+
debug: bool = False
|
|
149
|
+
|
|
150
|
+
# main.py
|
|
151
|
+
from pico_ioc import init
|
|
152
|
+
container = init(
|
|
153
|
+
__name__,
|
|
154
|
+
config=(EnvSource(prefix="APP_"), FileSource("config.json")),
|
|
155
|
+
)
|
|
156
|
+
settings = container.get(Settings)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
* Supports **Env**, **File** (YAML/JSON/INI/dotenv), dotted `Path.file[...]`,
|
|
160
|
+
and per-field overrides.
|
|
161
|
+
* Precedence: `overrides` > sources (in order) > class defaults.
|
|
162
|
+
* Missing required fields raise `NameError`.
|
|
163
|
+
|
|
133
164
|
**Inject into consumers:**
|
|
134
165
|
|
|
135
166
|
```python
|
|
136
167
|
@component
|
|
137
168
|
class Runner:
|
|
138
|
-
def __init__(self,
|
|
139
|
-
self._debug =
|
|
169
|
+
def __init__(self, s: Settings):
|
|
170
|
+
self._debug = s.debug
|
|
140
171
|
```
|
|
141
|
-
|
|
142
172
|
-----
|
|
143
173
|
|
|
144
174
|
## 5\) Testing & overrides
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# 📦 pico-ioc — Overview
|
|
2
|
+
|
|
3
|
+
## 🎯 Mission
|
|
4
|
+
|
|
5
|
+
**pico-ioc’s mission is to simplify dependency management and accelerate development by shortening feedback loops.** It gives Python projects a tiny, predictable IoC container that removes boilerplate wiring, making apps easier to test, extend, and run.
|
|
6
|
+
|
|
7
|
+
> ⚠️ **Requires Python 3.10+** (relies on `typing.Annotated` and `include_extras=True`).
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 🔍 What is pico-ioc?
|
|
12
|
+
|
|
13
|
+
pico-ioc is a **minimal Inversion of Control (IoC) and Dependency Injection (DI) container for Python**.
|
|
14
|
+
|
|
15
|
+
- **Zero dependencies** → pure Python, framework-agnostic.
|
|
16
|
+
- **Decorator API** → `@component`, `@factory_component`, `@provides`, `@plugin`.
|
|
17
|
+
- **Type-safe Configuration** → `@config_component` classes are auto-populated from environment variables and files (YAML, JSON, .env).
|
|
18
|
+
- **Automatic wiring** → resolves by: param name → exact type → MRO base → string key.
|
|
19
|
+
- **Fail-fast bootstrap** → eager by default; opt into `lazy=True` proxies.
|
|
20
|
+
- **Scoped subgraphs** → load only what you need with `scope(...)`.
|
|
21
|
+
- **Overrides** → replace providers directly in `init(overrides={...})`.
|
|
22
|
+
- **Qualifiers & collections** → tag/group implementations; inject `list[Annotated[T, Q]]`.
|
|
23
|
+
- **Interceptors API** → observe/modify resolution, instantiation, invocation, errors.
|
|
24
|
+
- **Conditional providers** → enable components by env vars or predicates (profiles).
|
|
25
|
+
- **Plugins** → lifecycle hooks (`before_scan`, `after_ready`).
|
|
26
|
+
- **Thread/async safe** → isolation via `ContextVar`.
|
|
27
|
+
- **Public API helper** → auto-export decorated symbols, cleaner `__init__.py`.
|
|
28
|
+
|
|
29
|
+
In short: **a Spring-like container for Python — tiny, predictable, and test-oriented.**
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## ⚡ Example: Hello Service
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from pico_ioc import component, config_component, init
|
|
37
|
+
|
|
38
|
+
# This class is now populated from env vars or files (e.g., config.yml)
|
|
39
|
+
@config_component
|
|
40
|
+
class Config:
|
|
41
|
+
url: str = "sqlite:///demo.db" # Default value
|
|
42
|
+
|
|
43
|
+
@component
|
|
44
|
+
class Repo:
|
|
45
|
+
def __init__(self, config: Config):
|
|
46
|
+
self.url = config.url
|
|
47
|
+
def fetch(self): return f"fetching from {self.url}"
|
|
48
|
+
|
|
49
|
+
@component
|
|
50
|
+
class Service:
|
|
51
|
+
def __init__(self, repo: Repo):
|
|
52
|
+
self.repo = repo
|
|
53
|
+
def run(self): return self.repo.fetch()
|
|
54
|
+
|
|
55
|
+
# bootstrap
|
|
56
|
+
import myapp
|
|
57
|
+
from pico_ioc.config import EnvSource
|
|
58
|
+
|
|
59
|
+
# The container will build the Config object from environment variables
|
|
60
|
+
c = init(myapp, config=(EnvSource(),))
|
|
61
|
+
svc = c.get(Service)
|
|
62
|
+
print(svc.run())
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Output:**
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
fetching from sqlite:///demo.db
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 🚀 Why pico-ioc?
|
|
74
|
+
|
|
75
|
+
* **Less glue code** — no manual wiring.
|
|
76
|
+
* **Predictable lifecycle** — fail early, debug easily.
|
|
77
|
+
* **Test-friendly** — overrides & scoped subgraphs make mocking trivial.
|
|
78
|
+
* **Externalized Configuration** — Manage settings for different environments without code changes.
|
|
79
|
+
* **Universal** — works with Flask, FastAPI, CLIs, or scripts.
|
|
80
|
+
* **Extensible** — logging, metrics, tracing via interceptors or plugins.
|
|
81
|
+
* **Profiles** — conditionals let you switch implementations by env/config.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 🧪 Testing patterns
|
|
86
|
+
|
|
87
|
+
Replace providers quickly in tests:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from pico_ioc import init
|
|
91
|
+
import myapp
|
|
92
|
+
|
|
93
|
+
fake = {"repo": "fake-data"}
|
|
94
|
+
c = init(myapp, overrides={
|
|
95
|
+
"fast_model": fake, # constant
|
|
96
|
+
"user_service": lambda: {"id": 1}, # provider
|
|
97
|
+
})
|
|
98
|
+
assert c.get("fast_model") == {"repo": "fake-data"}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Or use `scope()` to build only a subgraph:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from pico_ioc import scope
|
|
105
|
+
from src.runner_service import RunnerService
|
|
106
|
+
from tests.fakes import FakeDocker
|
|
107
|
+
import src
|
|
108
|
+
|
|
109
|
+
c = scope(
|
|
110
|
+
modules=[src],
|
|
111
|
+
roots=[RunnerService],
|
|
112
|
+
overrides={"docker.DockerClient": FakeDocker()},
|
|
113
|
+
strict=True, lazy=True,
|
|
114
|
+
)
|
|
115
|
+
svc = c.get(RunnerService)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## 📦 Public API Helper
|
|
121
|
+
|
|
122
|
+
Instead of manual exports in `__init__.py`:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
# app/__init__.py
|
|
126
|
+
from pico_ioc.public_api import export_public_symbols_decorated
|
|
127
|
+
__getattr__, __dir__ = export_public_symbols_decorated("app", include_plugins=True)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
This auto-exposes:
|
|
131
|
+
|
|
132
|
+
* All `@component` and `@factory_component` classes
|
|
133
|
+
* All `@plugin` classes (if `include_plugins=True`)
|
|
134
|
+
* Any symbols in `__all__`
|
|
135
|
+
|
|
136
|
+
So you can import cleanly:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from app import Service, Config, TracingPlugin
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## 📖 Documentation
|
|
145
|
+
|
|
146
|
+
* **🚀 New to pico-ioc? Start with the User Guide.**
|
|
147
|
+
|
|
148
|
+
* [**GUIDE.md**](GUIDE.md) — Learn with practical examples: testing, configuration, collection injection, and web framework integration.
|
|
149
|
+
|
|
150
|
+
* **⚙️ Feature & Pattern Guides**
|
|
151
|
+
|
|
152
|
+
* [**Guide: Configuration Injection**](GUIDE-CONFIGURATION-INJECTION.md) — A deep dive into the type-safe configuration system.
|
|
153
|
+
* [**Guide: Creating Plugins and Interceptors**](GUIDE_CREATING_PLUGINS_AND_INTERCEPTORS.md) — Learn how to extend pico-ioc with custom logic.
|
|
154
|
+
* [**Pattern: Implementing a CQRS Command Bus**](GUIDE_CQRS.md) — An example of building clean architectures with pico-ioc.
|
|
155
|
+
|
|
156
|
+
* **🏗️ Want to understand the internals? See the Architecture.**
|
|
157
|
+
|
|
158
|
+
* [**ARCHITECTURE.md**](ARCHITECTURE.md) — A deep dive into the algorithms, lifecycle, and internal diagrams. Perfect for contributors.
|
|
159
|
+
|
|
160
|
+
* **🤔 Want to know *why* it's designed this way? Read the Decisions.**
|
|
161
|
+
|
|
162
|
+
* [**DECISIONS.md**](DECISIONS.md) — The history and rationale behind key technical decisions.
|
|
163
|
+
|
|
164
|
+
* [Readme](../README.md) — readme.md file.
|
|
165
|
+
|
|
166
|
+
* [Changelog](../CHANGELOG.md) — release history.
|
|
167
|
+
|
|
@@ -73,11 +73,8 @@ These were evaluated and **rejected** to keep pico-ioc simple, deterministic, an
|
|
|
73
73
|
|
|
74
74
|
## [1.3.0] — 2025-09-14
|
|
75
75
|
|
|
76
|
-
### 💥 Breaking Changes
|
|
77
|
-
- **Interceptor API Rework**: The interceptor registration mechanism has been completely changed. The `interceptors` and `method_interceptors` parameters have been **removed** from `pico_ioc.init()` and `pico_ioc.scope()`. Interceptors are now discovered and registered automatically.
|
|
78
|
-
|
|
79
76
|
### ✨ New
|
|
80
|
-
- **`@interceptor` Decorator**: Interceptors are
|
|
77
|
+
- **`@interceptor` Decorator**: Interceptors are declared in-place using the `@interceptor` decorator on a class or a provider method. The scanner discovers and activates them automatically based on their metadata (`kind`, `order`, `profiles`, etc.). This simplifies the bootstrap process and co-locates cross-cutting concerns with their implementation.
|
|
81
78
|
|
|
82
79
|
- **Conditional providers**
|
|
83
80
|
- `@conditional(require_env=("VAR",))` activates a component only if env vars are present.
|
|
@@ -90,6 +87,25 @@ These were evaluated and **rejected** to keep pico-ioc simple, deterministic, an
|
|
|
90
87
|
|
|
91
88
|
---
|
|
92
89
|
|
|
90
|
+
## [1.4.0] — 2025-09-16
|
|
91
|
+
|
|
92
|
+
### ✨ New
|
|
93
|
+
- **Configuration Injection**
|
|
94
|
+
- Added `@config_component` for strongly typed settings classes.
|
|
95
|
+
- Supports environment variables and property files (YAML, JSON, INI, dotenv).
|
|
96
|
+
- Automatic field autowiring by name, with manual overrides (`Env`, `File`, `Path`, `Value`).
|
|
97
|
+
- Precedence: `overrides` > declared config sources > field defaults.
|
|
98
|
+
- Strict mode: missing required fields (no default and not resolvable) raise `NameError`.
|
|
99
|
+
|
|
100
|
+
### 🧪 Testing
|
|
101
|
+
- Added tests for precedence (env > file > default), dotted-path resolution, lazy instantiation, and required-field validation.
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
### 📚 Docs
|
|
105
|
+
- Added **GUIDE-CONFIGURATION-INJECTION.md** with examples for the new configuration injection system.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
93
109
|
## [Unreleased]
|
|
94
110
|
- Upcoming improvements and fixes will be listed here.
|
|
95
111
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pico-ioc
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.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
|
|
@@ -221,6 +221,12 @@ tox
|
|
|
221
221
|
|
|
222
222
|
---
|
|
223
223
|
|
|
224
|
+
## 📜 Overview
|
|
225
|
+
|
|
226
|
+
See [OVERVIEW.md](.llm/OVERVIEW.md) Just need a quick summary?
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
224
230
|
## 📜 Changelog
|
|
225
231
|
|
|
226
232
|
See [CHANGELOG.md](./CHANGELOG.md) for version history.
|