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.
Files changed (78) hide show
  1. {pico_ioc-1.3.0 → pico_ioc-1.4.0}/.llm/ARCHITECTURE.md +73 -32
  2. {pico_ioc-1.3.0 → pico_ioc-1.4.0}/.llm/DECISIONS.md +11 -0
  3. pico_ioc-1.4.0/.llm/GUIDE-CONFIGURATION-INJECTION.md +129 -0
  4. {pico_ioc-1.3.0 → pico_ioc-1.4.0}/.llm/GUIDE.md +35 -5
  5. pico_ioc-1.4.0/.llm/OVERVIEW.md +167 -0
  6. {pico_ioc-1.3.0 → pico_ioc-1.4.0}/CHANGELOG.md +20 -4
  7. {pico_ioc-1.3.0 → pico_ioc-1.4.0}/PKG-INFO +7 -1
  8. {pico_ioc-1.3.0 → pico_ioc-1.4.0}/README.md +6 -0
  9. {pico_ioc-1.3.0 → pico_ioc-1.4.0}/src/pico_ioc/__init__.py +13 -0
  10. pico_ioc-1.4.0/src/pico_ioc/_state.py +75 -0
  11. pico_ioc-1.4.0/src/pico_ioc/_version.py +1 -0
  12. {pico_ioc-1.3.0 → pico_ioc-1.4.0}/src/pico_ioc/api.py +36 -55
  13. {pico_ioc-1.3.0 → pico_ioc-1.4.0}/src/pico_ioc/builder.py +93 -41
  14. pico_ioc-1.4.0/src/pico_ioc/config.py +332 -0
  15. {pico_ioc-1.3.0 → pico_ioc-1.4.0}/src/pico_ioc/container.py +30 -11
  16. {pico_ioc-1.3.0 → pico_ioc-1.4.0}/src/pico_ioc/decorators.py +30 -6
  17. {pico_ioc-1.3.0 → pico_ioc-1.4.0}/src/pico_ioc/interceptors.py +13 -7
  18. pico_ioc-1.4.0/src/pico_ioc/policy.py +245 -0
  19. {pico_ioc-1.3.0 → pico_ioc-1.4.0}/src/pico_ioc/proxy.py +30 -18
  20. {pico_ioc-1.3.0 → pico_ioc-1.4.0}/src/pico_ioc/resolver.py +28 -25
  21. {pico_ioc-1.3.0 → pico_ioc-1.4.0}/src/pico_ioc/scanner.py +21 -21
  22. pico_ioc-1.4.0/src/pico_ioc/scope.py +46 -0
  23. {pico_ioc-1.3.0 → pico_ioc-1.4.0}/src/pico_ioc.egg-info/PKG-INFO +7 -1
  24. pico_ioc-1.4.0/src/pico_ioc.egg-info/SOURCES.txt +69 -0
  25. pico_ioc-1.4.0/tests/test_api.py +269 -0
  26. pico_ioc-1.4.0/tests/test_conditional_with_predicate.py +42 -0
  27. pico_ioc-1.4.0/tests/test_config_injection.py +176 -0
  28. pico_ioc-1.4.0/tests/test_container.py +204 -0
  29. pico_ioc-1.4.0/tests/test_core_helpers_and_errors.py +86 -0
  30. pico_ioc-1.4.0/tests/test_decorators_unit.py +138 -0
  31. pico_ioc-1.4.0/tests/test_defaults_and_overrides.py +71 -0
  32. pico_ioc-1.4.0/tests/test_factory_policy_and_defaults.py +179 -0
  33. {pico_ioc-1.3.0 → pico_ioc-1.4.0}/tests/test_fingerprint_public.py +8 -4
  34. pico_ioc-1.4.0/tests/test_interceptors.py +269 -0
  35. pico_ioc-1.4.0/tests/test_no_overrides_needed_with_on_missing.py +30 -0
  36. pico_ioc-1.4.0/tests/test_on_missing_and_primary_mix.py +29 -0
  37. pico_ioc-1.4.0/tests/test_on_missing_blackbox.py +90 -0
  38. pico_ioc-1.4.0/tests/test_on_missing_component.py +50 -0
  39. pico_ioc-1.4.0/tests/test_on_missing_factory.py +29 -0
  40. pico_ioc-1.4.0/tests/test_pico_ioc.py +280 -0
  41. pico_ioc-1.4.0/tests/test_pico_ioc_additional.py +192 -0
  42. pico_ioc-1.4.0/tests/test_pico_ioc_discovery.py +114 -0
  43. pico_ioc-1.4.0/tests/test_policy_and_container_helpers.py +106 -0
  44. pico_ioc-1.4.0/tests/test_policy_env_activation.py +33 -0
  45. pico_ioc-1.4.0/tests/test_policy_profile_primary.py +35 -0
  46. pico_ioc-1.4.0/tests/test_proxy_unit.py +188 -0
  47. pico_ioc-1.4.0/tests/test_public_api.py +220 -0
  48. pico_ioc-1.4.0/tests/test_qualifiers_unit.py +70 -0
  49. pico_ioc-1.4.0/tests/test_resolver_unit.py +121 -0
  50. pico_ioc-1.4.0/tests/test_scanner_providers.py +151 -0
  51. pico_ioc-1.4.0/tests/test_scanner_unit.py +190 -0
  52. pico_ioc-1.4.0/tests/test_scope_defaults.py +69 -0
  53. pico_ioc-1.4.0/tests/test_scope_defaults_and_policy.py +38 -0
  54. pico_ioc-1.4.0/tests/test_scope_unit.py +232 -0
  55. pico_ioc-1.3.0/.llm/OVERVIEW.md +0 -143
  56. pico_ioc-1.3.0/src/pico_ioc/_state.py +0 -40
  57. pico_ioc-1.3.0/src/pico_ioc/_version.py +0 -1
  58. pico_ioc-1.3.0/src/pico_ioc/policy.py +0 -332
  59. pico_ioc-1.3.0/src/pico_ioc.egg-info/SOURCES.txt +0 -39
  60. pico_ioc-1.3.0/tests/test_api.py +0 -145
  61. pico_ioc-1.3.0/tests/test_interceptors_autoreg.py +0 -214
  62. {pico_ioc-1.3.0 → pico_ioc-1.4.0}/.coveragerc +0 -0
  63. {pico_ioc-1.3.0 → pico_ioc-1.4.0}/.github/workflows/ci.yml +0 -0
  64. {pico_ioc-1.3.0 → pico_ioc-1.4.0}/.github/workflows/publish-to-pypi.yml +0 -0
  65. {pico_ioc-1.3.0 → pico_ioc-1.4.0}/.llm/GUIDE_CQRS.md +0 -0
  66. {pico_ioc-1.3.0 → pico_ioc-1.4.0}/.llm/GUIDE_CREATING_PLUGINS_AND_INTERCEPTORS.md +0 -0
  67. {pico_ioc-1.3.0 → pico_ioc-1.4.0}/LICENSE +0 -0
  68. {pico_ioc-1.3.0 → pico_ioc-1.4.0}/MANIFEST.in +0 -0
  69. {pico_ioc-1.3.0 → pico_ioc-1.4.0}/pyproject.toml +0 -0
  70. {pico_ioc-1.3.0 → pico_ioc-1.4.0}/setup.cfg +0 -0
  71. {pico_ioc-1.3.0 → pico_ioc-1.4.0}/src/pico_ioc/plugins.py +0 -0
  72. {pico_ioc-1.3.0 → pico_ioc-1.4.0}/src/pico_ioc/public_api.py +0 -0
  73. {pico_ioc-1.3.0 → pico_ioc-1.4.0}/src/pico_ioc/utils.py +0 -0
  74. {pico_ioc-1.3.0 → pico_ioc-1.4.0}/src/pico_ioc.egg-info/dependency_links.txt +0 -0
  75. {pico_ioc-1.3.0 → pico_ioc-1.4.0}/src/pico_ioc.egg-info/top_level.txt +0 -0
  76. {pico_ioc-1.3.0 → pico_ioc-1.4.0}/tests/test_decorators_and_policy.py +0 -0
  77. {pico_ioc-1.3.0 → pico_ioc-1.4.0}/tests/test_scope.py +0 -0
  78. {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->>App: scan decorators (@component, @factory_component, @interceptor)
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
- Treat config as a **component**:
157
+ Configuration is treated as a first-class, type-safe component using a dedicated injection system.
155
158
 
156
- ```python
157
- @component
158
- class Config:
159
- WORKERS: int = int(os.getenv("WORKERS", "4"))
160
- DEBUG: bool = os.getenv("DEBUG", "0") == "1"
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
- Inject `Config` where needed; avoid scattered `os.getenv` calls.
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(), # constant instance
180
- "fast_model": lambda: {"mock": True}, # provider
181
- "expensive": (lambda: object(), True), # provider with lazy=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\) Configuration patterns
119
+ ## 4) Configuration patterns
120
120
 
121
- **Env-backed config:**
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, cfg: Config):
139
- self._debug = cfg.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 now 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.
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.0
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.