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.
- {pico_ioc-1.3.0 → pico_ioc-1.5.0}/.llm/ARCHITECTURE.md +114 -64
- {pico_ioc-1.3.0 → pico_ioc-1.5.0}/.llm/DECISIONS.md +37 -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-CONFIGURATION-INJECTION.md +129 -0
- pico_ioc-1.5.0/.llm/GUIDE-CREATING-PLUGINS-AND-INTERCEPTORS.md +224 -0
- {pico_ioc-1.3.0 → pico_ioc-1.5.0}/.llm/GUIDE.md +96 -40
- pico_ioc-1.5.0/.llm/OVERVIEW.md +167 -0
- {pico_ioc-1.3.0 → pico_ioc-1.5.0}/CHANGELOG.md +49 -4
- {pico_ioc-1.3.0 → pico_ioc-1.5.0}/PKG-INFO +15 -1
- {pico_ioc-1.3.0 → pico_ioc-1.5.0}/README.md +14 -0
- {pico_ioc-1.3.0 → pico_ioc-1.5.0}/src/pico_ioc/__init__.py +28 -5
- pico_ioc-1.5.0/src/pico_ioc/_state.py +75 -0
- pico_ioc-1.5.0/src/pico_ioc/_version.py +1 -0
- {pico_ioc-1.3.0 → pico_ioc-1.5.0}/src/pico_ioc/api.py +38 -56
- {pico_ioc-1.3.0 → pico_ioc-1.5.0}/src/pico_ioc/builder.py +68 -100
- pico_ioc-1.5.0/src/pico_ioc/config.py +332 -0
- {pico_ioc-1.3.0 → pico_ioc-1.5.0}/src/pico_ioc/container.py +26 -44
- {pico_ioc-1.3.0 → pico_ioc-1.5.0}/src/pico_ioc/decorators.py +15 -29
- 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.5.0/src/pico_ioc/policy.py +245 -0
- {pico_ioc-1.3.0 → pico_ioc-1.5.0}/src/pico_ioc/proxy.py +22 -24
- {pico_ioc-1.3.0 → pico_ioc-1.5.0}/src/pico_ioc/resolver.py +12 -40
- {pico_ioc-1.3.0 → pico_ioc-1.5.0}/src/pico_ioc/scanner.py +42 -67
- pico_ioc-1.5.0/src/pico_ioc/scope.py +41 -0
- {pico_ioc-1.3.0 → pico_ioc-1.5.0}/src/pico_ioc.egg-info/PKG-INFO +15 -1
- pico_ioc-1.5.0/src/pico_ioc.egg-info/SOURCES.txt +65 -0
- pico_ioc-1.5.0/tests/conftest.py +22 -0
- pico_ioc-1.5.0/tests/helpers.py +46 -0
- {pico_ioc-1.3.0 → pico_ioc-1.5.0}/tests/test_api.py +37 -47
- pico_ioc-1.5.0/tests/test_config_injection.py +176 -0
- pico_ioc-1.5.0/tests/test_container.py +181 -0
- pico_ioc-1.5.0/tests/test_core_helpers_and_errors.py +86 -0
- 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.5.0/tests/test_defaults.py +69 -0
- pico_ioc-1.5.0/tests/test_factory_policy_and_defaults.py +179 -0
- {pico_ioc-1.3.0 → pico_ioc-1.5.0}/tests/test_fingerprint_public.py +5 -7
- 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.5.0/tests/test_no_overrides_needed_with_on_missing.py +30 -0
- pico_ioc-1.5.0/tests/test_policy_and_container_helpers.py +106 -0
- pico_ioc-1.5.0/tests/test_policy_env_activation.py +33 -0
- pico_ioc-1.5.0/tests/test_policy_profile_primary.py +35 -0
- pico_ioc-1.5.0/tests/test_proxy_unit.py +188 -0
- pico_ioc-1.5.0/tests/test_public_api.py +220 -0
- pico_ioc-1.5.0/tests/test_qualifiers_unit.py +70 -0
- pico_ioc-1.5.0/tests/test_resolver_unit.py +121 -0
- pico_ioc-1.5.0/tests/test_scanner_providers.py +146 -0
- pico_ioc-1.5.0/tests/test_scanner_unit.py +190 -0
- pico_ioc-1.5.0/tests/test_scope.py +326 -0
- pico_ioc-1.3.0/.llm/GUIDE_CREATING_PLUGINS_AND_INTERCEPTORS.md +0 -182
- pico_ioc-1.3.0/.llm/OVERVIEW.md +0 -143
- pico_ioc-1.3.0/src/pico_ioc/_state.py +0 -40
- pico_ioc-1.3.0/src/pico_ioc/_version.py +0 -1
- pico_ioc-1.3.0/src/pico_ioc/interceptors.py +0 -50
- pico_ioc-1.3.0/src/pico_ioc/policy.py +0 -332
- pico_ioc-1.3.0/src/pico_ioc.egg-info/SOURCES.txt +0 -39
- pico_ioc-1.3.0/tests/test_decorators_and_policy.py +0 -216
- pico_ioc-1.3.0/tests/test_interceptors_autoreg.py +0 -214
- pico_ioc-1.3.0/tests/test_scope.py +0 -80
- {pico_ioc-1.3.0 → pico_ioc-1.5.0}/.coveragerc +0 -0
- {pico_ioc-1.3.0 → pico_ioc-1.5.0}/.github/workflows/ci.yml +0 -0
- {pico_ioc-1.3.0 → pico_ioc-1.5.0}/.github/workflows/publish-to-pypi.yml +0 -0
- /pico_ioc-1.3.0/.llm/GUIDE_CQRS.md → /pico_ioc-1.5.0/.llm/GUIDE-CQRS.md +0 -0
- {pico_ioc-1.3.0 → pico_ioc-1.5.0}/LICENSE +0 -0
- {pico_ioc-1.3.0 → pico_ioc-1.5.0}/MANIFEST.in +0 -0
- {pico_ioc-1.3.0 → pico_ioc-1.5.0}/pyproject.toml +0 -0
- {pico_ioc-1.3.0 → pico_ioc-1.5.0}/setup.cfg +0 -0
- {pico_ioc-1.3.0 → pico_ioc-1.5.0}/src/pico_ioc/plugins.py +0 -0
- {pico_ioc-1.3.0 → pico_ioc-1.5.0}/src/pico_ioc/public_api.py +0 -0
- {pico_ioc-1.3.0 → pico_ioc-1.5.0}/src/pico_ioc/utils.py +0 -0
- {pico_ioc-1.3.0 → pico_ioc-1.5.0}/src/pico_ioc.egg-info/dependency_links.txt +0 -0
- {pico_ioc-1.3.0 → pico_ioc-1.5.0}/src/pico_ioc.egg-info/top_level.txt +0 -0
- {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
|
|
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
|
|
28
|
+
## 2) High-level model
|
|
29
29
|
|
|
30
30
|
- **Component** → class marked with `@component`. Instantiated by the container.
|
|
31
|
+
- **Config Component** → class marked with `@config_component`. Instantiated and populated from external sources like files or environment variables.
|
|
31
32
|
- **Factory component** → class marked with `@factory_component`; owns provider methods via `@provides(key=TypeOrToken)`. Providers return *externals* (e.g., `Flask`, DB clients).
|
|
32
|
-
- **
|
|
33
|
+
- **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->>
|
|
43
|
-
IOC->>
|
|
44
|
-
IOC->>IOC:
|
|
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
|
-
* `@
|
|
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
|
-
|
|
157
|
+
Configuration is treated as a first-class, type-safe component using a dedicated injection system.
|
|
155
158
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
159
|
+
1. **Define a Config Class**: Create a class (preferably a `dataclass`) and mark it with `@config_component`. An optional `prefix` can be used for environment variables.
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
from pico_ioc import config_component
|
|
163
|
+
from dataclasses import dataclass
|
|
164
|
+
|
|
165
|
+
@config_component(prefix="APP_")
|
|
166
|
+
@dataclass(frozen=True)
|
|
167
|
+
class Settings:
|
|
168
|
+
db_url: str
|
|
169
|
+
timeout: int = 30
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
2. **Provide Sources**: At bootstrap, pass an ordered tuple of `ConfigSource` objects to the `config` parameter of `init()`. The order defines precedence (first source wins).
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
from pico_ioc import init
|
|
176
|
+
from pico_ioc.config import EnvSource, FileSource
|
|
177
|
+
|
|
178
|
+
container = init(
|
|
179
|
+
"my_app",
|
|
180
|
+
config=(
|
|
181
|
+
EnvSource(prefix="APP_"), # Highest priority
|
|
182
|
+
FileSource("config.prod.yml", optional=True),
|
|
183
|
+
FileSource("config.yml"), # Lowest priority
|
|
184
|
+
),
|
|
185
|
+
)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
3. **Inject and Use**: Inject the config class into other components just like any other dependency.
|
|
162
189
|
|
|
163
|
-
|
|
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(),
|
|
180
|
-
"fast_model": lambda: {"mock": True},
|
|
181
|
-
"expensive": (lambda: object(), True),
|
|
220
|
+
Repo: FakeRepo(), # constant instance
|
|
221
|
+
"fast_model": lambda: {"mock": True}, # provider
|
|
222
|
+
"expensive": (lambda: object(), True), # provider with lazy=True
|
|
182
223
|
})
|
|
183
224
|
```
|
|
184
225
|
|
|
@@ -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(
|
|
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
|
|
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
|
-
`
|
|
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
|
|
208
|
-
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
|
|
209
254
|
|
|
210
|
-
|
|
255
|
+
# 1. Define the Interceptor
|
|
211
256
|
class LoggingInterceptor(MethodInterceptor):
|
|
212
|
-
def
|
|
213
|
-
print(f"Calling {
|
|
214
|
-
result =
|
|
215
|
-
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}.")
|
|
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
|
-
* `
|
|
226
|
-
* `
|
|
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 **
|
|
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
|
-
+
|
|
396
|
+
+invoke(ctx, call_next)
|
|
347
397
|
}
|
|
348
398
|
class ContainerInterceptor {
|
|
349
|
-
+
|
|
350
|
-
+
|
|
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:
|
|
413
|
+
D --> I_BEFORE[ContainerInterceptors: around_create]
|
|
366
414
|
I_BEFORE --> F[Instantiate T]
|
|
367
|
-
F -- exception --> I_EXC[
|
|
415
|
+
F -- exception --> I_EXC[Error bubbles up]
|
|
368
416
|
F -- success --> H[Wrap with MethodInterceptors if needed]
|
|
369
|
-
H --> I_AFTER[
|
|
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
|
|
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
|
|
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
|
|
|
@@ -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**:
|
|
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,
|
|
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.
|