pico-ioc 1.4.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 (85) hide show
  1. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/.llm/ARCHITECTURE.md +45 -36
  2. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/.llm/DECISIONS.md +26 -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-CREATING-PLUGINS-AND-INTERCEPTORS.md +224 -0
  7. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/.llm/GUIDE.md +68 -42
  8. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/.llm/OVERVIEW.md +9 -9
  9. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/CHANGELOG.md +29 -0
  10. {pico_ioc-1.4.0/src/pico_ioc.egg-info → pico_ioc-1.5.0}/PKG-INFO +10 -2
  11. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/README.md +9 -1
  12. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/src/pico_ioc/__init__.py +21 -11
  13. pico_ioc-1.5.0/src/pico_ioc/_version.py +1 -0
  14. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/src/pico_ioc/api.py +3 -2
  15. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/src/pico_ioc/builder.py +31 -115
  16. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/src/pico_ioc/container.py +18 -55
  17. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/src/pico_ioc/decorators.py +11 -49
  18. pico_ioc-1.5.0/src/pico_ioc/infra.py +196 -0
  19. pico_ioc-1.5.0/src/pico_ioc/interceptors.py +76 -0
  20. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/src/pico_ioc/proxy.py +9 -23
  21. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/src/pico_ioc/resolver.py +4 -35
  22. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/src/pico_ioc/scanner.py +30 -55
  23. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/src/pico_ioc/scope.py +2 -7
  24. {pico_ioc-1.4.0 → pico_ioc-1.5.0/src/pico_ioc.egg-info}/PKG-INFO +10 -2
  25. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/src/pico_ioc.egg-info/SOURCES.txt +14 -18
  26. pico_ioc-1.5.0/tests/conftest.py +22 -0
  27. pico_ioc-1.5.0/tests/helpers.py +46 -0
  28. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/tests/test_api.py +3 -137
  29. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/tests/test_container.py +16 -39
  30. pico_ioc-1.5.0/tests/test_decorator_on_missing.py +103 -0
  31. pico_ioc-1.5.0/tests/test_decorators.py +139 -0
  32. pico_ioc-1.4.0/tests/test_scope_defaults.py → pico_ioc-1.5.0/tests/test_defaults.py +1 -1
  33. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/tests/test_fingerprint_public.py +3 -9
  34. pico_ioc-1.5.0/tests/test_infrastructure.py +364 -0
  35. pico_ioc-1.5.0/tests/test_init.py +316 -0
  36. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/tests/test_scanner_providers.py +22 -27
  37. pico_ioc-1.5.0/tests/test_scope.py +326 -0
  38. pico_ioc-1.4.0/.llm/GUIDE_CREATING_PLUGINS_AND_INTERCEPTORS.md +0 -182
  39. pico_ioc-1.4.0/src/pico_ioc/_version.py +0 -1
  40. pico_ioc-1.4.0/src/pico_ioc/interceptors.py +0 -56
  41. pico_ioc-1.4.0/tests/test_conditional_with_predicate.py +0 -42
  42. pico_ioc-1.4.0/tests/test_decorators_and_policy.py +0 -216
  43. pico_ioc-1.4.0/tests/test_decorators_unit.py +0 -138
  44. pico_ioc-1.4.0/tests/test_defaults_and_overrides.py +0 -71
  45. pico_ioc-1.4.0/tests/test_interceptors.py +0 -269
  46. pico_ioc-1.4.0/tests/test_on_missing_and_primary_mix.py +0 -29
  47. pico_ioc-1.4.0/tests/test_on_missing_blackbox.py +0 -90
  48. pico_ioc-1.4.0/tests/test_on_missing_component.py +0 -50
  49. pico_ioc-1.4.0/tests/test_on_missing_factory.py +0 -29
  50. pico_ioc-1.4.0/tests/test_pico_ioc.py +0 -280
  51. pico_ioc-1.4.0/tests/test_pico_ioc_additional.py +0 -192
  52. pico_ioc-1.4.0/tests/test_pico_ioc_discovery.py +0 -114
  53. pico_ioc-1.4.0/tests/test_scope.py +0 -80
  54. pico_ioc-1.4.0/tests/test_scope_defaults_and_policy.py +0 -38
  55. pico_ioc-1.4.0/tests/test_scope_unit.py +0 -232
  56. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/.coveragerc +0 -0
  57. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/.github/workflows/ci.yml +0 -0
  58. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/.github/workflows/publish-to-pypi.yml +0 -0
  59. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/.llm/GUIDE-CONFIGURATION-INJECTION.md +0 -0
  60. /pico_ioc-1.4.0/.llm/GUIDE_CQRS.md → /pico_ioc-1.5.0/.llm/GUIDE-CQRS.md +0 -0
  61. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/LICENSE +0 -0
  62. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/MANIFEST.in +0 -0
  63. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/pyproject.toml +0 -0
  64. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/setup.cfg +0 -0
  65. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/src/pico_ioc/_state.py +0 -0
  66. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/src/pico_ioc/config.py +0 -0
  67. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/src/pico_ioc/plugins.py +0 -0
  68. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/src/pico_ioc/policy.py +0 -0
  69. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/src/pico_ioc/public_api.py +0 -0
  70. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/src/pico_ioc/utils.py +0 -0
  71. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/src/pico_ioc.egg-info/dependency_links.txt +0 -0
  72. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/src/pico_ioc.egg-info/top_level.txt +0 -0
  73. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/tests/test_config_injection.py +0 -0
  74. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/tests/test_core_helpers_and_errors.py +0 -0
  75. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/tests/test_factory_policy_and_defaults.py +0 -0
  76. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/tests/test_no_overrides_needed_with_on_missing.py +0 -0
  77. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/tests/test_policy_and_container_helpers.py +0 -0
  78. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/tests/test_policy_env_activation.py +0 -0
  79. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/tests/test_policy_profile_primary.py +0 -0
  80. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/tests/test_proxy_unit.py +0 -0
  81. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/tests/test_public_api.py +0 -0
  82. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/tests/test_qualifiers_unit.py +0 -0
  83. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/tests/test_resolver_unit.py +0 -0
  84. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/tests/test_scanner_unit.py +0 -0
  85. {pico_ioc-1.4.0 → pico_ioc-1.5.0}/tox.ini +0 -0
@@ -7,7 +7,7 @@
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
 
@@ -25,12 +25,12 @@
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
31
  - **Config Component** → class marked with `@config_component`. Instantiated and populated from external sources like files or environment variables.
32
32
  - **Factory component** → class marked with `@factory_component`; owns provider methods via `@provides(key=TypeOrToken)`. Providers return *externals* (e.g., `Flask`, DB clients).
33
- - **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.
34
34
  - **Container** → built by `pico_ioc.init(mod_or_list, ...)`; resolve with `container.get(KeyOrType)`.
35
35
 
36
36
  ### Bootstrap sequence
@@ -41,9 +41,9 @@ sequenceDiagram
41
41
  participant IOC as pico-ioc Container
42
42
  App->>IOC: init(packages, config, ...)
43
43
  IOC->>IOC: Create ConfigRegistry from sources
44
- IOC->>App: scan decorators (@component, @config_component, @interceptor)
45
- IOC->>IOC: register providers and collect interceptor declarations
46
- IOC->>IOC: build and activate interceptors
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)
47
47
  IOC->>IOC: apply policy (e.g., @primary, @on_missing aliases)
48
48
  IOC->>IOC: apply overrides (replace providers/constants)
49
49
  IOC->>IOC: instantiate eager components
@@ -61,7 +61,7 @@ sequenceDiagram
61
61
  * `@component` classes → registered by a **key** (defaults to the class type).
62
62
  * `@config_component` classes → registered as special components whose instances are built from external configuration sources.
63
63
  * `@factory_component` classes → introspected for `@provides(key=...)` methods.
64
- * `@interceptor` classes/functions → collected for activation.
64
+ * `@infrastructure` classes → collected for activation.
65
65
  * `@plugin` classes → if explicitly passed via `init(..., plugins=(...))`.
66
66
  3. **Registry** (frozen after bootstrap):
67
67
  * Map **key → provider**. Keys are typically **types**; string tokens are also supported.
@@ -217,8 +217,8 @@ The policy engine respects definition order. While not a strict "last-wins", pro
217
217
 
218
218
  ```python
219
219
  c = init(app, overrides={
220
- Repo: FakeRepo(), # constant instance
221
- "fast_model": lambda: {"mock": True}, # provider
220
+ Repo: FakeRepo(), # constant instance
221
+ "fast_model": lambda: {"mock": True}, # provider
222
222
  "expensive": (lambda: object(), True), # provider with lazy=True
223
223
  })
224
224
  ```
@@ -230,31 +230,44 @@ c = init(app, overrides={
230
230
  * `key: instance`
231
231
  * `key: callable`
232
232
  * `key: (callable, lazy_bool)`
233
- * 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.
234
234
 
235
235
  ---
236
236
 
237
237
  ## 11\) Interceptors (AOP & Lifecycle Hooks)
238
238
 
239
- 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
240
 
241
- `pico-ioc` supports two kinds of interceptors:
241
+ This is a two-step process:
242
+
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.
242
245
 
243
246
  ### Method Interceptors
244
247
 
245
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.
246
249
 
247
250
  ```python
248
- from pico_ioc import interceptor
249
- 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
250
254
 
251
- @interceptor(order=-10) # lower order runs first
255
+ # 1. Define the Interceptor
252
256
  class LoggingInterceptor(MethodInterceptor):
253
- def __call__(self, inv: Invocation, proceed):
254
- print(f"Calling {inv.method_name}...")
255
- result = proceed()
256
- 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}.")
257
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
+ )
258
271
  ```
259
272
 
260
273
  ### Container Interceptors
@@ -263,12 +276,8 @@ These implement the `ContainerInterceptor` protocol and hook into the container'
263
276
 
264
277
  **Hook points**:
265
278
 
266
- * `on_resolve(key, annotation, qualifiers)`
267
- * `on_before_create(key)`
268
- * `on_after_create(key, instance)` → may return a **wrapped/replaced** instance.
269
- * `on_exception(key, exc)`
270
-
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.
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.
272
281
 
273
282
  ---
274
283
 
@@ -324,7 +333,7 @@ This preserves registration order and returns a stable list.
324
333
  * `before_eager(container, binder)`
325
334
  * `after_ready(container, binder)`
326
335
 
327
- 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.
328
337
 
329
338
  ---
330
339
 
@@ -384,13 +393,11 @@ classDiagram
384
393
  + qualifiers: tuple
385
394
  }
386
395
  class MethodInterceptor {
387
- +__call__(inv, proceed)
396
+ +invoke(ctx, call_next)
388
397
  }
389
398
  class ContainerInterceptor {
390
- +on_resolve()
391
- +on_before_create()
392
- +on_after_create()
393
- +on_exception()
399
+ +around_resolve(ctx, call_next)
400
+ +around_create(ctx, call_next)
394
401
  }
395
402
  PicoContainer "1" o-- "*" MethodInterceptor
396
403
  PicoContainer "1" o-- "*" ContainerInterceptor
@@ -403,11 +410,11 @@ flowchart TD
403
410
  A[get(Type T)] --> B{Cached?}
404
411
  B -- yes --> Z[Return cached instance]
405
412
  B -- no --> D[Resolve dependencies for T (recurse)]
406
- D --> I_BEFORE[ContainerInterceptors: on_before_create]
413
+ D --> I_BEFORE[ContainerInterceptors: around_create]
407
414
  I_BEFORE --> F[Instantiate T]
408
- F -- exception --> I_EXC[ContainerInterceptors: on_exception]
415
+ F -- exception --> I_EXC[Error bubbles up]
409
416
  F -- success --> H[Wrap with MethodInterceptors if needed]
410
- H --> I_AFTER[ContainerInterceptors: on_after_create]
417
+ H --> I_AFTER[around_create returns instance]
411
418
  I_AFTER --> G[Cache instance]
412
419
  G --> Z
413
420
  ```
@@ -420,9 +427,11 @@ flowchart TD
420
427
  * **Singleton-per-container**: matches typical Python app composition; simpler mental model.
421
428
  * **Explicit decorators**: determinism and debuggability over magical auto-wiring.
422
429
  * **Fail fast**: configuration and graph issues surface at startup, not mid-request.
423
- * **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.
424
431
 
425
432
  ---
426
433
 
427
434
  **TL;DR**
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**.
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
 
@@ -142,6 +139,21 @@ A true "last-wins" only occurs when binding the *exact same key* multiple times,
142
139
 
143
140
  ---
144
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
+
145
157
  ## ❌ Won’t-Do Decisions
146
158
 
147
159
  ### A) Alternative scopes (request/session)
@@ -176,10 +188,10 @@ _No entries currently._
176
188
  - **2025-08**: Minimum Python 3.10; name-first resolution; fail-fast clarified; typed keys preferred.
177
189
  - **2025-09-08**: Introduced `init(..., overrides)` with defined precedence and laziness semantics.
178
190
  - **2025-09-13**: Added `scope(...)` for bounded containers with tag pruning and strict mode.
179
- - **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.
180
192
 
181
193
  ---
182
194
 
183
195
  **Summary**: pico-ioc remains **simple, deterministic, and fail-fast**.
184
- 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.
185
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.
@@ -0,0 +1,148 @@
1
+ # FEATURE-2025-0002: Interceptor Auto-Registration and Conditional Providers
2
+
3
+ - **Date:** 2025-09-14
4
+ - **Status:** Removed
5
+ - **Priority:** high
6
+ - **Related:** [ADR-0006: Declarative Cross-Cutting Concerns],[FEATURE-2025-0006]
7
+
8
+ -----
9
+
10
+ ## 1\) Summary
11
+
12
+ This feature introduces the `@interceptor` decorator to automatically discover, instantiate, and activate interceptors, simplifying how cross-cutting concerns like logging or transactions are applied. Additionally, it introduces the `@conditional` decorator, allowing any component (including interceptors) to be activated based on profiles, environment variables, or custom logic predicates. This enhances both the extensibility and environment-specific configuration of the container.
13
+
14
+ -----
15
+
16
+ ## 2\) Goals
17
+
18
+ - To automate the discovery and registration of both `MethodInterceptor` and `ContainerInterceptor` implementations.
19
+ - To allow developers to define interceptors alongside their business logic, improving modularity.
20
+ - To provide a mechanism (`@conditional`) for activating or deactivating any component based on external factors without code changes.
21
+ - To support metadata for interceptor ordering (`order`) and conditional activation (`profiles`, etc.).
22
+
23
+ -----
24
+
25
+ ## 3\) Non-Goals
26
+
27
+ - This feature does not change the underlying `MethodInterceptor` or `ContainerInterceptor` protocols.
28
+ - It does not introduce a complex Aspect-Oriented Programming (AOP) model beyond method interception.
29
+
30
+ -----
31
+
32
+ ## 4\) User Impact / Stories (Given/When/Then)
33
+
34
+ - **Story 1: Automatic Logging Interceptor**
35
+
36
+ - **Given** a developer writes a `LoggingInterceptor` class that implements `MethodInterceptor`.
37
+ - **When** they decorate the class with `@interceptor`.
38
+ - **Then** all component method calls are automatically intercepted and logged without any manual registration code.
39
+
40
+ - **Story 2: Environment-Specific Component**
41
+
42
+ - **Given** an `InMemoryCache` component should only be used for local development and testing.
43
+ - **When** the developer decorates it with `@conditional(profiles=["dev", "test"])`.
44
+ - **Then** the `InMemoryCache` is only active when the container is initialized with the "dev" or "test" profile, and a `RedisCache` marked with `@conditional(profiles=["prod"])` can be used in production.
45
+
46
+ - **Story 3: Controlling Interceptor Order**
47
+
48
+ - **Given** a developer has a `TimingInterceptor` and a `SecurityInterceptor`.
49
+ - **When** they decorate them with `@interceptor(order=10)` and `@interceptor(order=20)` respectively.
50
+ - **Then** the `TimingInterceptor` will wrap the `SecurityInterceptor`, ensuring the total execution time is measured correctly.
51
+
52
+ -----
53
+
54
+ ## 5\) Scope & Acceptance Criteria
55
+
56
+ - **In scope:**
57
+ - `@interceptor` decorator with `kind`, `order`, and conditional parameters.
58
+ - `@conditional` decorator with `profiles`, `require_env`, and `predicate` parameters.
59
+ - Integration with the scanner to discover these decorators on classes, methods, and functions.
60
+ - Integration with the builder to instantiate and register active interceptors.
61
+ - Integration with the policy engine to filter out inactive conditional components.
62
+ - **Out of scope:**
63
+ - Pointcut expressions or more advanced AOP features.
64
+ - **Acceptance:**
65
+ - [x] The scanner correctly identifies classes and functions decorated with `@interceptor`.
66
+ - [x] The builder correctly instantiates interceptors, injecting their dependencies.
67
+ - [x] The `order` parameter correctly influences the interceptor chain.
68
+ - [x] Components decorated with `@conditional` are only registered if their conditions (profiles, env vars, predicate) are met.
69
+ - [x] An inactive component causes a `NameError` at bootstrap if it's a required dependency for an eager component.
70
+
71
+ -----
72
+
73
+ ## 6\) API / UX Contract
74
+
75
+ The feature introduces the following public APIs:
76
+
77
+ - **Decorators:** `pico_ioc.interceptor(...)`, `pico_ioc.conditional(...)`
78
+
79
+ **Example (Interceptor):**
80
+
81
+ ```python
82
+ from pico_ioc import interceptor, component
83
+ from pico_ioc.interceptors import MethodInterceptor, Invocation
84
+ import time
85
+
86
+ # Before: No standard mechanism for registration.
87
+
88
+ # After: Simple, declarative, and auto-discovered.
89
+ @interceptor(order=-100)
90
+ class TimingInterceptor(MethodInterceptor):
91
+ def __call__(self, inv: Invocation, proceed):
92
+ start = time.time()
93
+ result = proceed()
94
+ duration = time.time() - start
95
+ print(f"{inv.method_name} took {duration:.2f}s")
96
+ return result
97
+
98
+ @component
99
+ class MyService:
100
+ def do_work(self):
101
+ time.sleep(1)
102
+
103
+ # In main.py, `init("my_app")` is enough. The interceptor is found and applied.
104
+ ```
105
+
106
+ -----
107
+
108
+ ## 7\) Rollout & Guardrails
109
+
110
+ - The feature was shipped as a new, non-breaking minor feature in version **1.3.0**.
111
+ - It is fully backward-compatible. Existing components without these decorators are unaffected.
112
+
113
+ -----
114
+
115
+ ## 8\) Telemetry
116
+
117
+ - The `pico-ioc` builder logs at the `DEBUG` level which interceptors are activated and which are skipped due to conditional rules, aiding in diagnostics.
118
+
119
+ -----
120
+
121
+ ## 9\) Risks & Open Questions
122
+
123
+ - **Risk:** The interaction between multiple conditionals or complex predicates could be confusing for users.
124
+ - **Mitigation:** The documentation provides clear examples for each conditional type and emphasizes the fail-fast nature of the container if a required dependency becomes inactive.
125
+
126
+ -----
127
+
128
+ ## 10\) Test Strategy
129
+
130
+ - **Unit Tests:** The `@interceptor` and `@conditional` decorators are tested to ensure they attach the correct metadata.
131
+ - **Integration Tests:** Scenarios verify that the scanner, builder, and policy engine work together to activate/deactivate components and interceptors based on profiles and environment variables. Tests for interceptor ordering are included.
132
+
133
+ -----
134
+
135
+ ## 11\) Milestones
136
+
137
+ - **M1 Ready:** 2025-09-13 - Scope defined and accepted.
138
+ - **M2 Planned:** 2025-09-13 - Implementation started.
139
+ - **M3 Shipped:** 2025-09-14 - Feature merged, tested, and released in v1.3.0.
140
+
141
+ -----
142
+
143
+ ## 12\) Documentation Impact
144
+
145
+ The following documents were created or updated:
146
+
147
+ - **Created:** `GUIDE_CREATING_PLUGINS_AND_INTERCEPTORS.md`
148
+ - **Updated:** `ARCHITECTURE.md`, `DECISIONS.md`, `GUIDE.md`, `CHANGELOG.md`, `README.md`, `OVERVIEW.md`