pico-ioc 1.3.0__tar.gz → 1.5.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 (77) hide show
  1. {pico_ioc-1.3.0 → pico_ioc-1.5.0}/.llm/ARCHITECTURE.md +114 -64
  2. {pico_ioc-1.3.0 → pico_ioc-1.5.0}/.llm/DECISIONS.md +37 -14
  3. pico_ioc-1.5.0/.llm/FEATURES/FEATURE-2025-0001-scope-subgraphs.md +151 -0
  4. pico_ioc-1.5.0/.llm/FEATURES/FEATURE-2025-0003-interceptor-auto-registration.md +148 -0
  5. pico_ioc-1.5.0/.llm/FEATURES/FEATURE-2025-0004-config-injection.md +158 -0
  6. pico_ioc-1.5.0/.llm/GUIDE-CONFIGURATION-INJECTION.md +129 -0
  7. pico_ioc-1.5.0/.llm/GUIDE-CREATING-PLUGINS-AND-INTERCEPTORS.md +224 -0
  8. {pico_ioc-1.3.0 → pico_ioc-1.5.0}/.llm/GUIDE.md +96 -40
  9. pico_ioc-1.5.0/.llm/OVERVIEW.md +167 -0
  10. {pico_ioc-1.3.0 → pico_ioc-1.5.0}/CHANGELOG.md +49 -4
  11. {pico_ioc-1.3.0 → pico_ioc-1.5.0}/PKG-INFO +15 -1
  12. {pico_ioc-1.3.0 → pico_ioc-1.5.0}/README.md +14 -0
  13. {pico_ioc-1.3.0 → pico_ioc-1.5.0}/src/pico_ioc/__init__.py +28 -5
  14. pico_ioc-1.5.0/src/pico_ioc/_state.py +75 -0
  15. pico_ioc-1.5.0/src/pico_ioc/_version.py +1 -0
  16. {pico_ioc-1.3.0 → pico_ioc-1.5.0}/src/pico_ioc/api.py +38 -56
  17. {pico_ioc-1.3.0 → pico_ioc-1.5.0}/src/pico_ioc/builder.py +68 -100
  18. pico_ioc-1.5.0/src/pico_ioc/config.py +332 -0
  19. {pico_ioc-1.3.0 → pico_ioc-1.5.0}/src/pico_ioc/container.py +26 -44
  20. {pico_ioc-1.3.0 → pico_ioc-1.5.0}/src/pico_ioc/decorators.py +15 -29
  21. pico_ioc-1.5.0/src/pico_ioc/infra.py +196 -0
  22. pico_ioc-1.5.0/src/pico_ioc/interceptors.py +76 -0
  23. pico_ioc-1.5.0/src/pico_ioc/policy.py +245 -0
  24. {pico_ioc-1.3.0 → pico_ioc-1.5.0}/src/pico_ioc/proxy.py +22 -24
  25. {pico_ioc-1.3.0 → pico_ioc-1.5.0}/src/pico_ioc/resolver.py +12 -40
  26. {pico_ioc-1.3.0 → pico_ioc-1.5.0}/src/pico_ioc/scanner.py +42 -67
  27. pico_ioc-1.5.0/src/pico_ioc/scope.py +41 -0
  28. {pico_ioc-1.3.0 → pico_ioc-1.5.0}/src/pico_ioc.egg-info/PKG-INFO +15 -1
  29. pico_ioc-1.5.0/src/pico_ioc.egg-info/SOURCES.txt +65 -0
  30. pico_ioc-1.5.0/tests/conftest.py +22 -0
  31. pico_ioc-1.5.0/tests/helpers.py +46 -0
  32. {pico_ioc-1.3.0 → pico_ioc-1.5.0}/tests/test_api.py +37 -47
  33. pico_ioc-1.5.0/tests/test_config_injection.py +176 -0
  34. pico_ioc-1.5.0/tests/test_container.py +181 -0
  35. pico_ioc-1.5.0/tests/test_core_helpers_and_errors.py +86 -0
  36. pico_ioc-1.5.0/tests/test_decorator_on_missing.py +103 -0
  37. pico_ioc-1.5.0/tests/test_decorators.py +139 -0
  38. pico_ioc-1.5.0/tests/test_defaults.py +69 -0
  39. pico_ioc-1.5.0/tests/test_factory_policy_and_defaults.py +179 -0
  40. {pico_ioc-1.3.0 → pico_ioc-1.5.0}/tests/test_fingerprint_public.py +5 -7
  41. pico_ioc-1.5.0/tests/test_infrastructure.py +364 -0
  42. pico_ioc-1.5.0/tests/test_init.py +316 -0
  43. pico_ioc-1.5.0/tests/test_no_overrides_needed_with_on_missing.py +30 -0
  44. pico_ioc-1.5.0/tests/test_policy_and_container_helpers.py +106 -0
  45. pico_ioc-1.5.0/tests/test_policy_env_activation.py +33 -0
  46. pico_ioc-1.5.0/tests/test_policy_profile_primary.py +35 -0
  47. pico_ioc-1.5.0/tests/test_proxy_unit.py +188 -0
  48. pico_ioc-1.5.0/tests/test_public_api.py +220 -0
  49. pico_ioc-1.5.0/tests/test_qualifiers_unit.py +70 -0
  50. pico_ioc-1.5.0/tests/test_resolver_unit.py +121 -0
  51. pico_ioc-1.5.0/tests/test_scanner_providers.py +146 -0
  52. pico_ioc-1.5.0/tests/test_scanner_unit.py +190 -0
  53. pico_ioc-1.5.0/tests/test_scope.py +326 -0
  54. pico_ioc-1.3.0/.llm/GUIDE_CREATING_PLUGINS_AND_INTERCEPTORS.md +0 -182
  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/interceptors.py +0 -50
  59. pico_ioc-1.3.0/src/pico_ioc/policy.py +0 -332
  60. pico_ioc-1.3.0/src/pico_ioc.egg-info/SOURCES.txt +0 -39
  61. pico_ioc-1.3.0/tests/test_decorators_and_policy.py +0 -216
  62. pico_ioc-1.3.0/tests/test_interceptors_autoreg.py +0 -214
  63. pico_ioc-1.3.0/tests/test_scope.py +0 -80
  64. {pico_ioc-1.3.0 → pico_ioc-1.5.0}/.coveragerc +0 -0
  65. {pico_ioc-1.3.0 → pico_ioc-1.5.0}/.github/workflows/ci.yml +0 -0
  66. {pico_ioc-1.3.0 → pico_ioc-1.5.0}/.github/workflows/publish-to-pypi.yml +0 -0
  67. /pico_ioc-1.3.0/.llm/GUIDE_CQRS.md → /pico_ioc-1.5.0/.llm/GUIDE-CQRS.md +0 -0
  68. {pico_ioc-1.3.0 → pico_ioc-1.5.0}/LICENSE +0 -0
  69. {pico_ioc-1.3.0 → pico_ioc-1.5.0}/MANIFEST.in +0 -0
  70. {pico_ioc-1.3.0 → pico_ioc-1.5.0}/pyproject.toml +0 -0
  71. {pico_ioc-1.3.0 → pico_ioc-1.5.0}/setup.cfg +0 -0
  72. {pico_ioc-1.3.0 → pico_ioc-1.5.0}/src/pico_ioc/plugins.py +0 -0
  73. {pico_ioc-1.3.0 → pico_ioc-1.5.0}/src/pico_ioc/public_api.py +0 -0
  74. {pico_ioc-1.3.0 → pico_ioc-1.5.0}/src/pico_ioc/utils.py +0 -0
  75. {pico_ioc-1.3.0 → pico_ioc-1.5.0}/src/pico_ioc.egg-info/dependency_links.txt +0 -0
  76. {pico_ioc-1.3.0 → pico_ioc-1.5.0}/src/pico_ioc.egg-info/top_level.txt +0 -0
  77. {pico_ioc-1.3.0 → pico_ioc-1.5.0}/tox.ini +0 -0
@@ -5,9 +5,9 @@
5
5
  >
6
6
  > ⚠️ **Requires Python 3.10+** (uses `typing.Annotated` with `include_extras=True`).
7
7
 
8
- -----
8
+ ---
9
9
 
10
- ## 1\) Design goals & non-goals
10
+ ## 1) Design goals & non-goals
11
11
 
12
12
  ### Goals
13
13
 
@@ -23,13 +23,14 @@
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
- ## 2\) High-level model
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
- - **Interceptor** → class or function marked with `@interceptor`. Discovered automatically to apply cross-cutting logic.
33
+ - **Infrastructure** → class marked with `@infrastructure`. Discovered automatically to apply cross-cutting logic, such as registering interceptors.
33
34
  - **Container** → built by `pico_ioc.init(mod_or_list, ...)`; resolve with `container.get(KeyOrType)`.
34
35
 
35
36
  ### Bootstrap sequence
@@ -38,10 +39,11 @@
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)
43
- IOC->>IOC: register providers and collect interceptor declarations
44
- IOC->>IOC: build and activate interceptors
42
+ App->>IOC: init(packages, config, ...)
43
+ IOC->>IOC: Create ConfigRegistry from sources
44
+ IOC->>App: scan decorators (@component, @config_component, @infrastructure)
45
+ IOC->>IOC: register providers and collect infrastructure declarations
46
+ IOC->>IOC: build and activate infrastructure (which registers interceptors)
45
47
  IOC->>IOC: apply policy (e.g., @primary, @on_missing aliases)
46
48
  IOC->>IOC: apply overrides (replace providers/constants)
47
49
  IOC->>IOC: instantiate eager components
@@ -50,22 +52,23 @@ 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
- * `@interceptor` classes/functions → collected for activation.
64
+ * `@infrastructure` classes → collected for activation.
62
65
  * `@plugin` classes → if explicitly passed via `init(..., plugins=(...))`.
63
66
  3. **Registry** (frozen after bootstrap):
64
67
  * Map **key → provider**. Keys are typically **types**; string tokens are also supported.
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.
162
189
 
163
- Inject `Config` where needed; avoid scattered `os.getenv` calls.
190
+ ```python
191
+ from pico_ioc import component
164
192
 
165
- -----
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.
203
+
204
+ This system ensures that configuration is **type-safe**, **externalized**, and **testable**, while remaining simple for the common cases.
205
+
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
 
@@ -189,31 +230,44 @@ c = init(app, overrides={
189
230
  * `key: instance`
190
231
  * `key: callable`
191
232
  * `key: (callable, lazy_bool)`
192
- * With `reuse=True`, re-calling `init(..., overrides=...)` applies new overrides to the cached container.
233
+ * With `reuse=True`, re-calling `init()` with different `overrides` will create a new container, not mutate a cached one, as the configuration fingerprint changes.
193
234
 
194
- -----
235
+ ---
195
236
 
196
237
  ## 11\) Interceptors (AOP & Lifecycle Hooks)
197
238
 
198
- Interceptors are components that apply cross-cutting logic like logging, metrics, or policy enforcement. They are discovered automatically via the `@interceptor` decorator.
239
+ Interceptors apply cross-cutting logic like logging, metrics, or policy enforcement. They are **not discovered automatically**. Instead, they must be registered by an `@infrastructure` component during the container's bootstrap phase.
240
+
241
+ This is a two-step process:
199
242
 
200
- `pico-ioc` supports two kinds of interceptors:
243
+ 1. **Define the Interceptor**: Create a class that implements the `MethodInterceptor` or `ContainerInterceptor` protocol.
244
+ 2. **Register it via an Infrastructure Component**: Create a class decorated with `@infrastructure` and use the `infra.intercept.add()` method inside its `configure` function to activate the interceptor and define which components it applies to.
201
245
 
202
246
  ### Method Interceptors
203
247
 
204
248
  These implement the `MethodInterceptor` protocol and wrap method calls on any component, enabling Aspect-Oriented Programming (AOP). They are ideal for tracing, timing, caching, or feature toggles.
205
249
 
206
250
  ```python
207
- from pico_ioc import interceptor
208
- from pico_ioc.interceptors import MethodInterceptor, Invocation
251
+ from pico_ioc import infrastructure
252
+ from pico_ioc.infra import Infra, Select
253
+ from pico_ioc.interceptors import MethodInterceptor, MethodCtx
209
254
 
210
- @interceptor(order=-10) # lower order runs first
255
+ # 1. Define the Interceptor
211
256
  class LoggingInterceptor(MethodInterceptor):
212
- def __call__(self, inv: Invocation, proceed):
213
- print(f"Calling {inv.method_name}...")
214
- result = proceed()
215
- print(f"Finished {inv.method_name}.")
257
+ def invoke(self, ctx: MethodCtx, call_next):
258
+ print(f"Calling {ctx.name}...")
259
+ result = call_next(ctx)
260
+ print(f"Finished {ctx.name}.")
216
261
  return result
262
+
263
+ # 2. Register it
264
+ @infrastructure
265
+ class MyInfra:
266
+ def configure(self, infra: Infra):
267
+ infra.intercept.add(
268
+ interceptor=LoggingInterceptor(),
269
+ where=Select().class_name(".*") # Apply to all components
270
+ )
217
271
  ```
218
272
 
219
273
  ### Container Interceptors
@@ -222,14 +276,10 @@ These implement the `ContainerInterceptor` protocol and hook into the container'
222
276
 
223
277
  **Hook points**:
224
278
 
225
- * `on_resolve(key, annotation, qualifiers)`
226
- * `on_before_create(key)`
227
- * `on_after_create(key, instance)` → may return a **wrapped/replaced** instance.
228
- * `on_exception(key, exc)`
229
-
230
- **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.
279
+ * `around_resolve(self, ctx: ResolveCtx, call_next)`: Wraps the dependency resolution process for a specific key.
280
+ * `around_create(self, ctx: CreateCtx, call_next)`: Wraps the instantiation of a component. It can modify the provider or even return a completely different instance.
231
281
 
232
- -----
282
+ ---
233
283
 
234
284
  ## 12\) Profiles & conditional providers
235
285
 
@@ -260,7 +310,7 @@ class InMemoryCache(Cache): ...
260
310
  * `predicate=callable` → must return a truthy value to activate.
261
311
  * If no active provider satisfies a required type and something depends on it → **bootstrap error** (fail fast).
262
312
 
263
- -----
313
+ ---
264
314
 
265
315
  ## 13\) Qualifiers & collection injection
266
316
 
@@ -271,7 +321,7 @@ Attach qualifiers to group/select implementations using `@qualifier`.
271
321
 
272
322
  This preserves registration order and returns a stable list.
273
323
 
274
- -----
324
+ ---
275
325
 
276
326
  ## 14\) Plugins
277
327
 
@@ -283,9 +333,9 @@ This preserves registration order and returns a stable list.
283
333
  * `before_eager(container, binder)`
284
334
  * `after_ready(container, binder)`
285
335
 
286
- Plugins are passed **explicitly** via `init(..., plugins=(MyPlugin(),))`. Prefer **interceptors** for fine-grained wiring events; use **plugins** for coarse lifecycle integration.
336
+ Plugins are passed **explicitly** via `init(..., plugins=(MyPlugin(),))`. Prefer **infrastructure** for fine-grained wiring events; use **plugins** for coarse lifecycle integration.
287
337
 
288
- -----
338
+ ---
289
339
 
290
340
  ## 15\) Scoped subgraphs (`scope`)
291
341
 
@@ -320,7 +370,7 @@ Providers may carry `tags` (via `@component(tags=...)` or `@provides(..., tags=.
320
370
 
321
371
  **Use cases:** fast unit tests, integration-lite, CLI tools, microbenchmarks.
322
372
 
323
- -----
373
+ ---
324
374
 
325
375
  ## 16\) Diagnostics & diagrams
326
376
 
@@ -343,13 +393,11 @@ classDiagram
343
393
  + qualifiers: tuple
344
394
  }
345
395
  class MethodInterceptor {
346
- +__call__(inv, proceed)
396
+ +invoke(ctx, call_next)
347
397
  }
348
398
  class ContainerInterceptor {
349
- +on_resolve()
350
- +on_before_create()
351
- +on_after_create()
352
- +on_exception()
399
+ +around_resolve(ctx, call_next)
400
+ +around_create(ctx, call_next)
353
401
  }
354
402
  PicoContainer "1" o-- "*" MethodInterceptor
355
403
  PicoContainer "1" o-- "*" ContainerInterceptor
@@ -362,16 +410,16 @@ flowchart TD
362
410
  A[get(Type T)] --> B{Cached?}
363
411
  B -- yes --> Z[Return cached instance]
364
412
  B -- no --> D[Resolve dependencies for T (recurse)]
365
- D --> I_BEFORE[ContainerInterceptors: on_before_create]
413
+ D --> I_BEFORE[ContainerInterceptors: around_create]
366
414
  I_BEFORE --> F[Instantiate T]
367
- F -- exception --> I_EXC[ContainerInterceptors: on_exception]
415
+ F -- exception --> I_EXC[Error bubbles up]
368
416
  F -- success --> H[Wrap with MethodInterceptors if needed]
369
- H --> I_AFTER[ContainerInterceptors: on_after_create]
417
+ H --> I_AFTER[around_create returns instance]
370
418
  I_AFTER --> G[Cache instance]
371
419
  G --> Z
372
420
  ```
373
421
 
374
- -----
422
+ ---
375
423
 
376
424
  ## 17\) Rationale & trade-offs
377
425
 
@@ -379,9 +427,11 @@ flowchart TD
379
427
  * **Singleton-per-container**: matches typical Python app composition; simpler mental model.
380
428
  * **Explicit decorators**: determinism and debuggability over magical auto-wiring.
381
429
  * **Fail fast**: configuration and graph issues surface at startup, not mid-request.
382
- * **Interceptors over AOP**: precise, opt-in hooks without full-blown aspect weavers.
430
+ * **Interceptors via Infrastructure**: precise, opt-in hooks without the complexity of auto-discovery.
383
431
 
384
- -----
432
+ ---
385
433
 
386
434
  **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**.
435
+ `pico-ioc` builds a **deterministic, typed dependency graph** from decorated components, factories, and infrastructure. 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**.
436
+
437
+
@@ -72,7 +72,7 @@ Each entry includes a rationale and implications. If a decision is later changed
72
72
  - `key: instance` (constant)
73
73
  - `key: callable` (provider, non-lazy)
74
74
  - `key: (callable, lazy_bool)` (provider with explicit laziness)
75
- - With `reuse=True`, subsequent `init(..., overrides=...)` mutates cached bindings.
75
+ - If `reuse=True`, calling `init()` with different `overrides` will generate a new fingerprint and build a new container, not mutate the cached one.
76
76
 
77
77
  ---
78
78
 
@@ -87,20 +87,17 @@ Each entry includes a rationale and implications. If a decision is later changed
87
87
 
88
88
  ---
89
89
 
90
- ### 11) **Interceptors API** (AOP & Lifecycle Hooks)
91
- **Decision**: Introduce two types of interceptors, auto-discovered via `@interceptor`, to observe or modify container and component behavior.
90
+ ### 11) **Infrastructure-based Interceptor API**
91
+ **Decision**: Provide an extension mechanism for AOP and lifecycle hooks through **manually registered interceptors**. Instead of auto-discovery, interceptors are activated within `@infrastructure` components, giving developers explicit control over their application and order.
92
92
  **Kinds & Hooks**:
93
- - **`MethodInterceptor`**: Implements `__call__(self, inv: Invocation, proceed)`. Wraps method calls on components for Aspect-Oriented Programming (AOP) use cases like logging, tracing, or caching.
93
+ - **`MethodInterceptor`**: Implements `invoke(self, ctx: MethodCtx, call_next)`. Wraps method calls on components for Aspect-Oriented Programming (AOP).
94
94
  - **`ContainerInterceptor`**: Implements container lifecycle hooks:
95
- - `on_resolve(key, annotation, qualifiers)`
96
- - `on_before_create(key)`
97
- - `on_after_create(key, instance)` (may wrap/replace the instance)
98
- - `on_exception(key, exc)`
99
- **Rationale**: Provides structured extension points for both cross-cutting concerns (AOP) and container lifecycle events without the complexity of full aspect weavers.
95
+ - `around_resolve(self, ctx: ResolveCtx, call_next)`
96
+ - `around_create(self, ctx: CreateCtx, call_next)`
97
+ **Rationale**: This approach is more explicit and predictable than magic auto-discovery. It makes the wiring process easier to debug and ensures that the order of interceptors is controlled by the developer via the `order` parameter in `@infrastructure`.
100
98
  **Implications**:
101
- - Deterministic ordering via `order` (lower runs first).
102
- - Auto-registered via `@interceptor(...)` on classes or provider methods.
103
- - `on_exception` must re-raise if the error is not meant to be suppressed.
99
+ - Interceptors are plain Python classes that implement a protocol; they do not require a decorator.
100
+ - Their activation is tied to the lifecycle of the `@infrastructure` components that register them.
104
101
 
105
102
  ---
106
103
 
@@ -131,6 +128,32 @@ A true "last-wins" only occurs when binding the *exact same key* multiple times,
131
128
 
132
129
  ---
133
130
 
131
+ ### 15) Configuration Injection
132
+ **Decision**: Provide `@config_component` for strongly typed configuration classes, populated from ordered `ConfigSource`s (`EnvSource`, `FileSource`, etc.).
133
+ **Rationale**: Type-safe configuration with minimal boilerplate, supporting both automatic autowiring by field name and manual overrides (`Env`, `File`, `Path`, `Value`).
134
+ **Implications**:
135
+ - Precedence is explicit: `overrides` > sources (in order) > field defaults.
136
+ - Missing required fields (no default and not resolvable) raise `NameError`.
137
+ - Supported formats: env vars, JSON, INI, dotenv, YAML (if available).
138
+ - Encourages using `dataclass(frozen=True)` for immutable, validated settings.
139
+
140
+ ---
141
+
142
+ ### 16) Container Diagnostics API (`describe()`)
143
+
144
+ **Decision**: Add a unified diagnostics method `container.describe(...)` that exports the initialization context, active interceptors, and the fully resolved dependency graph.
145
+ **Rationale**: Developers need a deterministic way to introspect the container state for debugging, documentation, and architectural analysis. A stable schema with IDs, provenance, and safe redaction makes this reliable and auditable.
146
+ **Implications**:
147
+ - Public API: `container.describe(include_values=False, include_inactive=False, only_types=None, only_tags=None, redact_patterns=None, format="dict")`.
148
+ - Default output is JSON-serializable with `schema_version`, `generated_at`, `status`, `initialization_context`, `active_interceptors`, and `dependency_graph`.
149
+ - Sensitive values are **redacted by default**; explicit opt-in required via `include_values=True`.
150
+ - Deterministic IDs and ordering allow snapshot diffs across runs.
151
+ - Alternative outputs supported (`mermaid`, `dot`) for visualization.
152
+ - Large graphs can be filtered (`only_types`, `only_tags`) to keep snapshots manageable.
153
+ - Partial snapshots with `status="partial"` are returned if initialization fails, including error metadata.
154
+
155
+ ---
156
+
134
157
  ## ❌ Won’t-Do Decisions
135
158
 
136
159
  ### A) Alternative scopes (request/session)
@@ -165,10 +188,10 @@ _No entries currently._
165
188
  - **2025-08**: Minimum Python 3.10; name-first resolution; fail-fast clarified; typed keys preferred.
166
189
  - **2025-09-08**: Introduced `init(..., overrides)` with defined precedence and laziness semantics.
167
190
  - **2025-09-13**: Added `scope(...)` for bounded containers with tag pruning and strict mode.
168
- - **2025-09-14**: Added **Interceptors API** and **Conditional providers** as first-class features; documented last-wins registration and concurrency stance.
191
+ - **2025-09-14**: Replaced auto-discovered interceptors with an infrastructure-based registration model. Added **Conditional providers** as a first-class feature.
169
192
 
170
193
  ---
171
194
 
172
195
  **Summary**: pico-ioc remains **simple, deterministic, and fail-fast**.
173
- We favor typed wiring, explicit registration, and small, composable primitives (overrides, scope, interceptors, conditionals) instead of heavyweight AOP or multi-scope lifecycles.
196
+ We favor typed wiring, explicit registration, and small, composable primitives (overrides, scope, infrastructure, conditionals) instead of heavyweight AOP or multi-scope lifecycles.
174
197
 
@@ -0,0 +1,151 @@
1
+ # FEATURE-2025-0001: Scoped Subgraphs with scope()
2
+
3
+ - **Date:** 2025-09-13
4
+ - **Status:** Delivered
5
+ - **Priority:** high
6
+ - **Related:** [ADR-0008: Lightweight Test Containers]
7
+
8
+ -----
9
+
10
+ ## 1\) Summary
11
+
12
+ This feature introduces `pico_ioc.scope()`, a powerful factory function for creating lightweight, temporary containers limited to a specific dependency subgraph. By defining one or more `roots`, the container will only include components and services required by those roots. This is extremely useful for fast, isolated unit and integration tests, as well as for CLI tools or serverless functions that only need a subset of the application's full dependency graph.
13
+
14
+ -----
15
+
16
+ ## 2\) Goals
17
+
18
+ - To provide a mechanism for creating minimal IoC containers for testing or specialized tasks.
19
+ - To drastically reduce the overhead and complexity of bootstrapping the full application graph for a limited use case.
20
+ - To improve test performance by only scanning and instantiating necessary components.
21
+ - To allow fine-grained graph pruning through tag-based filtering.
22
+
23
+ -----
24
+
25
+ ## 3\) Non-Goals
26
+
27
+ - `scope()` does not introduce new lifecycle scopes (like "request" or "session"). Components are still singletons within the created container.
28
+ - It is not intended to replace the main `pico_ioc.init()` for bootstrapping the full application.
29
+
30
+ -----
31
+
32
+ ## 4\) User Impact / Stories (Given/When/Then)
33
+
34
+ - **Story 1: Isolated Service Test**
35
+
36
+ - **Given** a developer wants to test `RunnerService`, which depends on `Repo` but not on `WebService`.
37
+ - **When** they create a container with `scope(modules=["my_app"], roots=[RunnerService])`.
38
+ - **Then** the container contains `RunnerService` and `Repo`, but not `WebService`, making the test setup faster and more focused.
39
+
40
+ - **Story 2: Mocking Dependencies in a Test**
41
+
42
+ - **Given** a developer is testing a service that depends on an external `DockerClient`.
43
+ - **When** they use `scope(..., roots=[MyService], overrides={"docker.DockerClient": FakeDocker()})`.
44
+ - **Then** the `MyService` instance they retrieve from the container is injected with the `FakeDocker` mock instead of the real one.
45
+
46
+ - **Story 3: Pruning a Subgraph with Tags**
47
+
48
+ - **Given** an application has multiple `Notifier` implementations tagged with `"email"` or `"sms"`.
49
+ - **When** a developer builds a scope for a specific task using `scope(..., include_tags={"sms"})`.
50
+ - **Then** only the `SmsNotifier` and its dependencies are included in the graph, while the `EmailNotifier` is excluded.
51
+
52
+ -----
53
+
54
+ ## 5\) Scope & Acceptance Criteria
55
+
56
+ - **In scope:**
57
+ - `pico_ioc.scope()` function with `modules`, `roots`, `overrides`, `strict`, `lazy`, `include_tags`, and `exclude_tags` parameters.
58
+ - Graph traversal logic to compute the dependency subgraph from the given roots.
59
+ - Tag filtering logic to prune providers before graph traversal.
60
+ - Support for use as a context manager.
61
+ - **Out of scope:**
62
+ - Runtime modification of a created scope.
63
+ - **Acceptance:**
64
+ - [x] A container created with `scope()` only contains providers transitively reachable from the specified `roots`.
65
+ - [x] The `overrides` parameter correctly replaces providers within the subgraph.
66
+ - [x] `strict=True` raises a `NameError` if a dependency cannot be found within the subgraph.
67
+ - [x] `include_tags` and `exclude_tags` correctly filter the set of available providers before the subgraph is calculated.
68
+ - [x] The `scope()` function works correctly when used as a `with` statement context manager.
69
+
70
+ -----
71
+
72
+ ## 6\) API / UX Contract
73
+
74
+ The feature is exposed through the `pico_ioc.scope()` function:
75
+
76
+ ```python
77
+ def scope(
78
+ *,
79
+ modules: Iterable[Any] = (),
80
+ roots: Iterable[type] = (),
81
+ overrides: Optional[Dict[Any, Any]] = None,
82
+ strict: bool = True,
83
+ lazy: bool = True,
84
+ include_tags: Optional[set[str]] = None,
85
+ exclude_tags: Optional[set[str]] = None,
86
+ ) -> PicoContainer:
87
+ # ...
88
+ ```
89
+
90
+ **Example:**
91
+
92
+ ```python
93
+ # In a test file
94
+ from pico_ioc import scope
95
+ from src.runner_service import RunnerService
96
+ from tests.fakes import FakeDocker
97
+ import src
98
+
99
+ def test_runner_in_isolation():
100
+ # Create a container with only what RunnerService needs
101
+ with scope(
102
+ modules=[src],
103
+ roots=[RunnerService],
104
+ overrides={"docker.DockerClient": FakeDocker()},
105
+ strict=True,
106
+ ) as container:
107
+ service = container.get(RunnerService)
108
+ # ... assertions
109
+ ```
110
+
111
+ -----
112
+
113
+ ## 7\) Rollout & Guardrails
114
+
115
+ - The feature was shipped as a new, non-breaking minor feature in version **1.2.0**.
116
+ - It is fully backward-compatible and does not affect the existing `init()` function.
117
+
118
+ -----
119
+
120
+ ## 8\) Telemetry
121
+
122
+ - The `pico-ioc` builder logs at the `INFO` level when a scope container is ready, which can be useful for debugging test setups.
123
+
124
+ -----
125
+
126
+ ## 9\) Risks & Open Questions
127
+
128
+ - **Risk:** Developers might misunderstand `scope()` as a lifecycle feature (e.g., request scope) rather than a container construction tool.
129
+ - **Mitigation:** The documentation explicitly states that components are still singletons within the scope and that `scope()` is primarily for testing and specialized tasks.
130
+
131
+ -----
132
+
133
+ ## 10\) Test Strategy
134
+
135
+ - **Unit Tests:** The graph traversal algorithm (`_compute_allowed_subgraph`) and tag filtering logic are unit-tested.
136
+ - **Integration Tests:** End-to-end tests verify that `scope()` correctly builds containers for various scenarios, including complex dependency chains, overrides, and strict mode enforcement.
137
+
138
+ -----
139
+
140
+ ## 11\) Milestones
141
+
142
+ - **M1 Ready:** 2025-09-12 - Scope defined and accepted.
143
+ - **M2 Planned:** 2025-09-12 - Implementation started.
144
+ - **M3 Shipped:** 2025-09-13 - Feature merged, tested, and released in v1.2.0.
145
+
146
+ -----
147
+
148
+ ## 12\) Documentation Impact
149
+
150
+ - The feature is documented in `GUIDE.md`, `ARCHITECTURE.md`, `DECISIONS.md`, `CHANGELOG.md`, `README.md`, and `OVERVIEW.md`.
151
+ - Practical examples for testing are included in the main user guide.