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.
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/.llm/ARCHITECTURE.md +45 -36
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/.llm/DECISIONS.md +26 -14
- pico_ioc-1.5.0/.llm/FEATURES/FEATURE-2025-0001-scope-subgraphs.md +151 -0
- pico_ioc-1.5.0/.llm/FEATURES/FEATURE-2025-0003-interceptor-auto-registration.md +148 -0
- pico_ioc-1.5.0/.llm/FEATURES/FEATURE-2025-0004-config-injection.md +158 -0
- pico_ioc-1.5.0/.llm/GUIDE-CREATING-PLUGINS-AND-INTERCEPTORS.md +224 -0
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/.llm/GUIDE.md +68 -42
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/.llm/OVERVIEW.md +9 -9
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/CHANGELOG.md +29 -0
- {pico_ioc-1.4.0/src/pico_ioc.egg-info → pico_ioc-1.5.0}/PKG-INFO +10 -2
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/README.md +9 -1
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/src/pico_ioc/__init__.py +21 -11
- pico_ioc-1.5.0/src/pico_ioc/_version.py +1 -0
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/src/pico_ioc/api.py +3 -2
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/src/pico_ioc/builder.py +31 -115
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/src/pico_ioc/container.py +18 -55
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/src/pico_ioc/decorators.py +11 -49
- pico_ioc-1.5.0/src/pico_ioc/infra.py +196 -0
- pico_ioc-1.5.0/src/pico_ioc/interceptors.py +76 -0
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/src/pico_ioc/proxy.py +9 -23
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/src/pico_ioc/resolver.py +4 -35
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/src/pico_ioc/scanner.py +30 -55
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/src/pico_ioc/scope.py +2 -7
- {pico_ioc-1.4.0 → pico_ioc-1.5.0/src/pico_ioc.egg-info}/PKG-INFO +10 -2
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/src/pico_ioc.egg-info/SOURCES.txt +14 -18
- pico_ioc-1.5.0/tests/conftest.py +22 -0
- pico_ioc-1.5.0/tests/helpers.py +46 -0
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/tests/test_api.py +3 -137
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/tests/test_container.py +16 -39
- pico_ioc-1.5.0/tests/test_decorator_on_missing.py +103 -0
- pico_ioc-1.5.0/tests/test_decorators.py +139 -0
- pico_ioc-1.4.0/tests/test_scope_defaults.py → pico_ioc-1.5.0/tests/test_defaults.py +1 -1
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/tests/test_fingerprint_public.py +3 -9
- pico_ioc-1.5.0/tests/test_infrastructure.py +364 -0
- pico_ioc-1.5.0/tests/test_init.py +316 -0
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/tests/test_scanner_providers.py +22 -27
- pico_ioc-1.5.0/tests/test_scope.py +326 -0
- pico_ioc-1.4.0/.llm/GUIDE_CREATING_PLUGINS_AND_INTERCEPTORS.md +0 -182
- pico_ioc-1.4.0/src/pico_ioc/_version.py +0 -1
- pico_ioc-1.4.0/src/pico_ioc/interceptors.py +0 -56
- pico_ioc-1.4.0/tests/test_conditional_with_predicate.py +0 -42
- pico_ioc-1.4.0/tests/test_decorators_and_policy.py +0 -216
- pico_ioc-1.4.0/tests/test_decorators_unit.py +0 -138
- pico_ioc-1.4.0/tests/test_defaults_and_overrides.py +0 -71
- pico_ioc-1.4.0/tests/test_interceptors.py +0 -269
- pico_ioc-1.4.0/tests/test_on_missing_and_primary_mix.py +0 -29
- pico_ioc-1.4.0/tests/test_on_missing_blackbox.py +0 -90
- pico_ioc-1.4.0/tests/test_on_missing_component.py +0 -50
- pico_ioc-1.4.0/tests/test_on_missing_factory.py +0 -29
- pico_ioc-1.4.0/tests/test_pico_ioc.py +0 -280
- pico_ioc-1.4.0/tests/test_pico_ioc_additional.py +0 -192
- pico_ioc-1.4.0/tests/test_pico_ioc_discovery.py +0 -114
- pico_ioc-1.4.0/tests/test_scope.py +0 -80
- pico_ioc-1.4.0/tests/test_scope_defaults_and_policy.py +0 -38
- pico_ioc-1.4.0/tests/test_scope_unit.py +0 -232
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/.coveragerc +0 -0
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/.github/workflows/ci.yml +0 -0
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/.github/workflows/publish-to-pypi.yml +0 -0
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/.llm/GUIDE-CONFIGURATION-INJECTION.md +0 -0
- /pico_ioc-1.4.0/.llm/GUIDE_CQRS.md → /pico_ioc-1.5.0/.llm/GUIDE-CQRS.md +0 -0
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/LICENSE +0 -0
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/MANIFEST.in +0 -0
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/pyproject.toml +0 -0
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/setup.cfg +0 -0
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/src/pico_ioc/_state.py +0 -0
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/src/pico_ioc/config.py +0 -0
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/src/pico_ioc/plugins.py +0 -0
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/src/pico_ioc/policy.py +0 -0
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/src/pico_ioc/public_api.py +0 -0
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/src/pico_ioc/utils.py +0 -0
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/src/pico_ioc.egg-info/dependency_links.txt +0 -0
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/src/pico_ioc.egg-info/top_level.txt +0 -0
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/tests/test_config_injection.py +0 -0
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/tests/test_core_helpers_and_errors.py +0 -0
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/tests/test_factory_policy_and_defaults.py +0 -0
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/tests/test_no_overrides_needed_with_on_missing.py +0 -0
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/tests/test_policy_and_container_helpers.py +0 -0
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/tests/test_policy_env_activation.py +0 -0
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/tests/test_policy_profile_primary.py +0 -0
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/tests/test_proxy_unit.py +0 -0
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/tests/test_public_api.py +0 -0
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/tests/test_qualifiers_unit.py +0 -0
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/tests/test_resolver_unit.py +0 -0
- {pico_ioc-1.4.0 → pico_ioc-1.5.0}/tests/test_scanner_unit.py +0 -0
- {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
|
|
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
|
|
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
|
-
- **
|
|
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, @
|
|
45
|
-
IOC->>IOC: register providers and collect
|
|
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
|
-
* `@
|
|
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(),
|
|
221
|
-
"fast_model": lambda: {"mock": True},
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
249
|
-
from pico_ioc.
|
|
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
|
-
|
|
255
|
+
# 1. Define the Interceptor
|
|
252
256
|
class LoggingInterceptor(MethodInterceptor):
|
|
253
|
-
def
|
|
254
|
-
print(f"Calling {
|
|
255
|
-
result =
|
|
256
|
-
print(f"Finished {
|
|
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
|
-
* `
|
|
267
|
-
* `
|
|
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 **
|
|
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
|
-
+
|
|
396
|
+
+invoke(ctx, call_next)
|
|
388
397
|
}
|
|
389
398
|
class ContainerInterceptor {
|
|
390
|
-
+
|
|
391
|
-
+
|
|
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:
|
|
413
|
+
D --> I_BEFORE[ContainerInterceptors: around_create]
|
|
407
414
|
I_BEFORE --> F[Instantiate T]
|
|
408
|
-
F -- exception --> I_EXC[
|
|
415
|
+
F -- exception --> I_EXC[Error bubbles up]
|
|
409
416
|
F -- success --> H[Wrap with MethodInterceptors if needed]
|
|
410
|
-
H --> I_AFTER[
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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) **
|
|
91
|
-
**Decision**:
|
|
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 `
|
|
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
|
-
- `
|
|
96
|
-
- `
|
|
97
|
-
|
|
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
|
-
-
|
|
102
|
-
-
|
|
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**:
|
|
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,
|
|
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`
|