pico-ioc 1.2.0__tar.gz → 1.3.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 (66) hide show
  1. pico_ioc-1.3.0/.llm/ARCHITECTURE.md +387 -0
  2. pico_ioc-1.3.0/.llm/DECISIONS.md +174 -0
  3. pico_ioc-1.3.0/.llm/GUIDE.md +466 -0
  4. pico_ioc-1.3.0/.llm/GUIDE_CQRS.md +110 -0
  5. pico_ioc-1.3.0/.llm/GUIDE_CREATING_PLUGINS_AND_INTERCEPTORS.md +182 -0
  6. pico_ioc-1.3.0/.llm/OVERVIEW.md +143 -0
  7. {pico_ioc-1.2.0 → pico_ioc-1.3.0}/CHANGELOG.md +19 -0
  8. {pico_ioc-1.2.0/src/pico_ioc.egg-info → pico_ioc-1.3.0}/PKG-INFO +59 -16
  9. pico_ioc-1.3.0/README.md +190 -0
  10. pico_ioc-1.3.0/src/pico_ioc/__init__.py +45 -0
  11. pico_ioc-1.3.0/src/pico_ioc/_state.py +40 -0
  12. pico_ioc-1.3.0/src/pico_ioc/_version.py +1 -0
  13. pico_ioc-1.3.0/src/pico_ioc/api.py +240 -0
  14. pico_ioc-1.3.0/src/pico_ioc/builder.py +242 -0
  15. {pico_ioc-1.2.0 → pico_ioc-1.3.0}/src/pico_ioc/container.py +57 -29
  16. {pico_ioc-1.2.0 → pico_ioc-1.3.0}/src/pico_ioc/decorators.py +63 -8
  17. pico_ioc-1.3.0/src/pico_ioc/interceptors.py +50 -0
  18. {pico_ioc-1.2.0 → pico_ioc-1.3.0}/src/pico_ioc/plugins.py +17 -1
  19. pico_ioc-1.3.0/src/pico_ioc/policy.py +332 -0
  20. {pico_ioc-1.2.0 → pico_ioc-1.3.0}/src/pico_ioc/proxy.py +41 -1
  21. {pico_ioc-1.2.0 → pico_ioc-1.3.0}/src/pico_ioc/resolver.py +45 -40
  22. pico_ioc-1.3.0/src/pico_ioc/scanner.py +203 -0
  23. pico_ioc-1.3.0/src/pico_ioc/utils.py +25 -0
  24. {pico_ioc-1.2.0 → pico_ioc-1.3.0/src/pico_ioc.egg-info}/PKG-INFO +59 -16
  25. {pico_ioc-1.2.0 → pico_ioc-1.3.0}/src/pico_ioc.egg-info/SOURCES.txt +11 -15
  26. pico_ioc-1.3.0/tests/test_api.py +145 -0
  27. pico_ioc-1.3.0/tests/test_decorators_and_policy.py +216 -0
  28. pico_ioc-1.3.0/tests/test_fingerprint_public.py +27 -0
  29. pico_ioc-1.3.0/tests/test_interceptors_autoreg.py +214 -0
  30. pico_ioc-1.3.0/tests/test_scope.py +80 -0
  31. pico_ioc-1.2.0/.llm/ARCHITECTURE.md +0 -357
  32. pico_ioc-1.2.0/.llm/DECISIONS.md +0 -150
  33. pico_ioc-1.2.0/.llm/GUIDE.md +0 -504
  34. pico_ioc-1.2.0/.llm/OVERVIEW.md +0 -160
  35. pico_ioc-1.2.0/README.md +0 -147
  36. pico_ioc-1.2.0/src/pico_ioc/__init__.py +0 -32
  37. pico_ioc-1.2.0/src/pico_ioc/_state.py +0 -10
  38. pico_ioc-1.2.0/src/pico_ioc/_version.py +0 -1
  39. pico_ioc-1.2.0/src/pico_ioc/api.py +0 -289
  40. pico_ioc-1.2.0/src/pico_ioc/scanner.py +0 -230
  41. pico_ioc-1.2.0/src/pico_ioc/typing_utils.py +0 -29
  42. pico_ioc-1.2.0/tests/test_api_unit.py +0 -197
  43. pico_ioc-1.2.0/tests/test_container_get_all.py +0 -23
  44. pico_ioc-1.2.0/tests/test_container_unit.py +0 -125
  45. pico_ioc-1.2.0/tests/test_decorators_unit.py +0 -138
  46. pico_ioc-1.2.0/tests/test_pico_ioc.py +0 -280
  47. pico_ioc-1.2.0/tests/test_pico_ioc_additional.py +0 -192
  48. pico_ioc-1.2.0/tests/test_pico_ioc_discovery.py +0 -114
  49. pico_ioc-1.2.0/tests/test_proxy_unit.py +0 -322
  50. pico_ioc-1.2.0/tests/test_public_api.py +0 -220
  51. pico_ioc-1.2.0/tests/test_qualifiers_unit.py +0 -70
  52. pico_ioc-1.2.0/tests/test_resolver_unit.py +0 -121
  53. pico_ioc-1.2.0/tests/test_scanner_unit.py +0 -190
  54. pico_ioc-1.2.0/tests/test_scope_unit.py +0 -232
  55. pico_ioc-1.2.0/tests/test_typing_utils_unit.py +0 -102
  56. {pico_ioc-1.2.0 → pico_ioc-1.3.0}/.coveragerc +0 -0
  57. {pico_ioc-1.2.0 → pico_ioc-1.3.0}/.github/workflows/ci.yml +0 -0
  58. {pico_ioc-1.2.0 → pico_ioc-1.3.0}/.github/workflows/publish-to-pypi.yml +0 -0
  59. {pico_ioc-1.2.0 → pico_ioc-1.3.0}/LICENSE +0 -0
  60. {pico_ioc-1.2.0 → pico_ioc-1.3.0}/MANIFEST.in +0 -0
  61. {pico_ioc-1.2.0 → pico_ioc-1.3.0}/pyproject.toml +0 -0
  62. {pico_ioc-1.2.0 → pico_ioc-1.3.0}/setup.cfg +0 -0
  63. {pico_ioc-1.2.0 → pico_ioc-1.3.0}/src/pico_ioc/public_api.py +0 -0
  64. {pico_ioc-1.2.0 → pico_ioc-1.3.0}/src/pico_ioc.egg-info/dependency_links.txt +0 -0
  65. {pico_ioc-1.2.0 → pico_ioc-1.3.0}/src/pico_ioc.egg-info/top_level.txt +0 -0
  66. {pico_ioc-1.2.0 → pico_ioc-1.3.0}/tox.ini +0 -0
@@ -0,0 +1,387 @@
1
+ # pico-ioc — Architecture
2
+
3
+ > **Scope:** internal model, wiring algorithm, lifecycle, and design trade-offs.
4
+ > **Non-goals:** tutorials/recipes (see `GUIDE_CREATING_PLUGINS_AND_INTERCEPTORS.md`), product pitch.
5
+ >
6
+ > ⚠️ **Requires Python 3.10+** (uses `typing.Annotated` with `include_extras=True`).
7
+
8
+ -----
9
+
10
+ ## 1\) Design goals & non-goals
11
+
12
+ ### Goals
13
+
14
+ - **Tiny, predictable DI** for Python apps (CLIs, Flask/FastAPI, services).
15
+ - **Fail fast** at bootstrap; deterministic resolution.
16
+ - **Ergonomic**: typed constructors; minimal reflection; explicit decorators.
17
+ - **Framework-agnostic**: no hard deps on web frameworks.
18
+ - **Safe by default**: thread/async-friendly; no global mutable singletons.
19
+
20
+ ### Non-goals
21
+
22
+ - Full Spring feature set (complex scopes, bean post-processors).
23
+ - Hot reload or runtime graph mutation beyond explicit overrides.
24
+ - Magical filesystem-wide auto-imports.
25
+
26
+ -----
27
+
28
+ ## 2\) High-level model
29
+
30
+ - **Component** → class marked with `@component`. Instantiated by the container.
31
+ - **Factory component** → class marked with `@factory_component`; owns provider methods via `@provides(key=TypeOrToken)`. Providers return *externals* (e.g., `Flask`, DB clients).
32
+ - **Interceptor** → class or function marked with `@interceptor`. Discovered automatically to apply cross-cutting logic.
33
+ - **Container** → built by `pico_ioc.init(mod_or_list, ...)`; resolve with `container.get(KeyOrType)`.
34
+
35
+ ### Bootstrap sequence
36
+
37
+ ```mermaid
38
+ sequenceDiagram
39
+ participant App as Your package(s)
40
+ participant IOC as pico-ioc Container
41
+ App->>IOC: init(packages, ...)
42
+ IOC->>App: scan decorators (@component, @factory_component, @interceptor)
43
+ IOC->>IOC: register providers and collect interceptor declarations
44
+ IOC->>IOC: build and activate interceptors
45
+ IOC->>IOC: apply policy (e.g., @primary, @on_missing aliases)
46
+ IOC->>IOC: apply overrides (replace providers/constants)
47
+ IOC->>IOC: instantiate eager components
48
+ App->>IOC: get(Service)
49
+ IOC->>IOC: resolve dependencies (with interception)
50
+ IOC-->>App: instance(Service)
51
+ ```
52
+
53
+ -----
54
+
55
+ ## 3\) Discovery & registration
56
+
57
+ 1. **Scan inputs** passed to `init(...)`: module or list of modules/packages.
58
+ 2. **Collect**:
59
+ * `@component` classes → registered by a **key** (defaults to the class type).
60
+ * `@factory_component` classes → introspected for `@provides(key=...)` methods.
61
+ * `@interceptor` classes/functions → collected for activation.
62
+ * `@plugin` classes → if explicitly passed via `init(..., plugins=(...))`.
63
+ 3. **Registry** (frozen after bootstrap):
64
+ * Map **key → provider**. Keys are typically **types**; string tokens are also supported.
65
+
66
+ **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
+
68
+ -----
69
+
70
+ ## 4\) Resolution algorithm (deterministic)
71
+
72
+ When constructing a component `C`:
73
+
74
+ 1. Inspect `__init__(self, ...)`; collect **type-annotated** parameters (excluding `self`).
75
+ 2. For each parameter `p: T`, resolve by this order:
76
+ 1. **By name**: if a provider key matches the parameter name `p` (if `prefer_name_first=True`).
77
+ 2. **Exact type** `T`.
78
+ 3. **MRO walk**: first registered base class of `T`.
79
+ 4. **By name (fallback)**: if a provider key matches the parameter name `p`.
80
+ 3. Instantiate dependencies depth-first; cache singletons.
81
+ 4. Construct `C` with resolved instances.
82
+
83
+ ### Failure modes
84
+
85
+ * **No provider** for a required key → **bootstrap error** (fail fast) with a full dependency chain.
86
+ * **Ambiguous/incompatible** registrations → policy resolves to a single provider or raises an error.
87
+
88
+ ### 4b) Collection resolution
89
+
90
+ If the constructor requests `list[T]` or `list[Annotated[T, Q]]`:
91
+
92
+ * Return **all** compatible providers for `T`.
93
+ * If `Q` (qualifier) is present, filter to matching ones.
94
+ * Registration order is preserved; no implicit sorting.
95
+ * Returns an empty list if no matches.
96
+
97
+ -----
98
+
99
+ ## 5\) Lifecycles & scopes
100
+
101
+ * **Singleton per container**: a provider is instantiated at most once and cached.
102
+ * **Lazy proxies (optional)**: `@component(lazy=True)` or `@provides(lazy=True)` defers instantiation until first use via a `ComponentProxy`. Prefer eager to catch errors early.
103
+
104
+ **Rationale:** Most Python app composition (config, clients, web apps) fits singleton-per-container; it’s simple and fast.
105
+
106
+ -----
107
+
108
+ ## 6\) Factories & providers
109
+
110
+ Use `@factory_component` for **externals** (framework apps, DB clients, engines).
111
+
112
+ ```python
113
+ from pico_ioc import factory_component, provides
114
+ from flask import Flask
115
+
116
+ @factory_component
117
+ class AppFactory:
118
+ @provides(key=Flask)
119
+ def provide_flask(self) -> Flask:
120
+ app = Flask(__name__)
121
+ app.config["JSON_AS_ASCII"] = False
122
+ return app
123
+ ```
124
+
125
+ Guidelines:
126
+
127
+ * Providers should be **pure constructors** (no long-running work).
128
+ * Prefer **typed keys** (e.g., `Flask`) over strings.
129
+
130
+ -----
131
+
132
+ ## 7\) Concurrency model
133
+
134
+ * Container state is **immutable after init**.
135
+ * Caches & resolution are **thread/async safe** (internal isolation; no global singletons).
136
+ * Instances you create **must** be safe for your usage patterns; the container cannot fix non-thread-safe libraries.
137
+
138
+ -----
139
+
140
+ ## 8\) Error handling & diagnostics
141
+
142
+ * **Bootstrap**:
143
+ * Missing providers → explicit `NameError` with full dependency chain details.
144
+ * Duplicate keys → resolved by policy, preferring `@primary`.
145
+ * **Runtime**:
146
+ * Exceptions from providers/constructors bubble up. The resolver path is included in the error for easier debugging.
147
+
148
+ **Tip:** Keep constructors **cheap**; push I/O to explicit start/serve methods.
149
+
150
+ -----
151
+
152
+ ## 9\) Configuration
153
+
154
+ Treat config as a **component**:
155
+
156
+ ```python
157
+ @component
158
+ class Config:
159
+ WORKERS: int = int(os.getenv("WORKERS", "4"))
160
+ DEBUG: bool = os.getenv("DEBUG", "0") == "1"
161
+ ```
162
+
163
+ Inject `Config` where needed; avoid scattered `os.getenv` calls.
164
+
165
+ -----
166
+
167
+ ## 10\) Overrides & composition
168
+
169
+ ### 10.1 Module-ordered overrides
170
+
171
+ The policy engine respects definition order. While not a strict "last-wins", providers marked `@primary` will take precedence over others discovered during the scan.
172
+
173
+ ### 10.2 Direct overrides argument
174
+
175
+ `init()` accepts an `overrides` dictionary for ad-hoc replacement.
176
+
177
+ ```python
178
+ c = init(app, overrides={
179
+ Repo: FakeRepo(), # constant instance
180
+ "fast_model": lambda: {"mock": True}, # provider
181
+ "expensive": (lambda: object(), True), # provider with lazy=True
182
+ })
183
+ ```
184
+
185
+ **Semantics:**
186
+
187
+ * Applied **after scanning and policy** but before eager instantiation → replaced providers never run.
188
+ * Accepted forms:
189
+ * `key: instance`
190
+ * `key: callable`
191
+ * `key: (callable, lazy_bool)`
192
+ * With `reuse=True`, re-calling `init(..., overrides=...)` applies new overrides to the cached container.
193
+
194
+ -----
195
+
196
+ ## 11\) Interceptors (AOP & Lifecycle Hooks)
197
+
198
+ Interceptors are components that apply cross-cutting logic like logging, metrics, or policy enforcement. They are discovered automatically via the `@interceptor` decorator.
199
+
200
+ `pico-ioc` supports two kinds of interceptors:
201
+
202
+ ### Method Interceptors
203
+
204
+ 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
+
206
+ ```python
207
+ from pico_ioc import interceptor
208
+ from pico_ioc.interceptors import MethodInterceptor, Invocation
209
+
210
+ @interceptor(order=-10) # lower order runs first
211
+ class LoggingInterceptor(MethodInterceptor):
212
+ def __call__(self, inv: Invocation, proceed):
213
+ print(f"Calling {inv.method_name}...")
214
+ result = proceed()
215
+ print(f"Finished {inv.method_name}.")
216
+ return result
217
+ ```
218
+
219
+ ### Container Interceptors
220
+
221
+ These implement the `ContainerInterceptor` protocol and hook into the container's internal lifecycle events.
222
+
223
+ **Hook points**:
224
+
225
+ * `on_resolve(key, annotation, qualifiers)`
226
+ * `on_before_create(key)`
227
+ * `on_after_create(key, instance)` → may return a **wrapped/replaced** instance.
228
+ * `on_exception(key, exc)`
229
+
230
+ **Registration:** Interceptors are discovered by the scanner during `init()` or `scope()`. There is no need to pass them manually. Their activation can be controlled with the same `@conditional` decorator and gates (`profiles`, `require_env`) used for other components.
231
+
232
+ -----
233
+
234
+ ## 12\) Profiles & conditional providers
235
+
236
+ Use `@conditional` to **activate providers based on profiles, environment variables, or a predicate function**.
237
+
238
+ ```python
239
+ from pico_ioc import component, conditional
240
+
241
+ class Cache: ...
242
+
243
+ @component
244
+ @conditional(profiles=("prod", "staging"))
245
+ class RedisCache(Cache): ...
246
+
247
+ @component
248
+ @conditional(require_env=("REDIS_URL",))
249
+ class AnotherRedisCache(Cache): ...
250
+
251
+ @component
252
+ @conditional(predicate=lambda: os.path.exists("/tmp/use_mem"))
253
+ class InMemoryCache(Cache): ...
254
+ ```
255
+
256
+ **Rules**:
257
+
258
+ * `profiles=("A","B")` → active if any profile passed to `init()` or `scope()` matches.
259
+ * `require_env=("A","B")` → all environment variables must exist and be non-empty.
260
+ * `predicate=callable` → must return a truthy value to activate.
261
+ * If no active provider satisfies a required type and something depends on it → **bootstrap error** (fail fast).
262
+
263
+ -----
264
+
265
+ ## 13\) Qualifiers & collection injection
266
+
267
+ Attach qualifiers to group/select implementations using `@qualifier`.
268
+
269
+ * Request `list[T]` → injects all registered implementations of `T`.
270
+ * Request `list[Annotated[T, Q]]` → injects only those implementations of `T` tagged with qualifier `Q`.
271
+
272
+ This preserves registration order and returns a stable list.
273
+
274
+ -----
275
+
276
+ ## 14\) Plugins
277
+
278
+ `@plugin` classes implementing the `PicoPlugin` protocol can observe the **container lifecycle**.
279
+
280
+ * `before_scan(package, binder)`
281
+ * `after_scan(package, binder)`
282
+ * `after_bind(container, binder)`
283
+ * `before_eager(container, binder)`
284
+ * `after_ready(container, binder)`
285
+
286
+ Plugins are passed **explicitly** via `init(..., plugins=(MyPlugin(),))`. Prefer **interceptors** for fine-grained wiring events; use **plugins** for coarse lifecycle integration.
287
+
288
+ -----
289
+
290
+ ## 15\) Scoped subgraphs (`scope`)
291
+
292
+ Build a **bounded container** containing only dependencies reachable from selected **roots**.
293
+
294
+ ```python
295
+ from pico_ioc import scope
296
+ from src.runner_service import RunnerService
297
+ from tests.fakes import FakeDocker
298
+ import src
299
+
300
+ c = scope(
301
+ modules=[src],
302
+ roots=[RunnerService],
303
+ overrides={"docker.DockerClient": FakeDocker()},
304
+ strict=True, lazy=True,
305
+ )
306
+ svc = c.get(RunnerService)
307
+ ```
308
+
309
+ ### Tag-based pruning
310
+
311
+ Providers may carry `tags` (via `@component(tags=...)` or `@provides(..., tags=...)`). `scope()` can filter the initial set of providers using `include_tags` and `exclude_tags` before traversing the dependency graph.
312
+
313
+ ### Semantics
314
+
315
+ * **Limited reach**: only providers transitively reachable from `roots` are included in the final graph.
316
+ * **Deterministic precedence**: `overrides > scoped providers > base container providers` (if `strict=False`).
317
+ * **Strict mode**: controls whether missing dependencies raise an error (`True`) or can be resolved from the `base` container (`False`).
318
+ * **Lifecycle**: still **singleton-per-container**; `scope` does **not** add request/session scopes.
319
+ * **Context manager**: `with scope(...):` is supported.
320
+
321
+ **Use cases:** fast unit tests, integration-lite, CLI tools, microbenchmarks.
322
+
323
+ -----
324
+
325
+ ## 16\) Diagnostics & diagrams
326
+
327
+ ### Registry & resolution (class diagram)
328
+
329
+ ```mermaid
330
+ classDiagram
331
+ class PicoContainer {
332
+ +get(key) instance
333
+ +get_all(base_type) sequence
334
+ +add_method_interceptor(it)
335
+ +add_container_interceptor(it)
336
+ - _providers: Map[key, ProviderMeta]
337
+ - _singletons: Map[key, instance]
338
+ }
339
+ class ProviderMeta {
340
+ + factory: Callable
341
+ + lazy: bool
342
+ + tags: tuple
343
+ + qualifiers: tuple
344
+ }
345
+ class MethodInterceptor {
346
+ +__call__(inv, proceed)
347
+ }
348
+ class ContainerInterceptor {
349
+ +on_resolve()
350
+ +on_before_create()
351
+ +on_after_create()
352
+ +on_exception()
353
+ }
354
+ PicoContainer "1" o-- "*" MethodInterceptor
355
+ PicoContainer "1" o-- "*" ContainerInterceptor
356
+ ```
357
+
358
+ ### Resolution flow (activity)
359
+
360
+ ```mermaid
361
+ flowchart TD
362
+ A[get(Type T)] --> B{Cached?}
363
+ B -- yes --> Z[Return cached instance]
364
+ B -- no --> D[Resolve dependencies for T (recurse)]
365
+ D --> I_BEFORE[ContainerInterceptors: on_before_create]
366
+ I_BEFORE --> F[Instantiate T]
367
+ F -- exception --> I_EXC[ContainerInterceptors: on_exception]
368
+ F -- success --> H[Wrap with MethodInterceptors if needed]
369
+ H --> I_AFTER[ContainerInterceptors: on_after_create]
370
+ I_AFTER --> G[Cache instance]
371
+ G --> Z
372
+ ```
373
+
374
+ -----
375
+
376
+ ## 17\) Rationale & trade-offs
377
+
378
+ * **Typed keys first**: better IDE support; fewer foot-guns than strings.
379
+ * **Singleton-per-container**: matches typical Python app composition; simpler mental model.
380
+ * **Explicit decorators**: determinism and debuggability over magical auto-wiring.
381
+ * **Fail fast**: configuration and graph issues surface at startup, not mid-request.
382
+ * **Interceptors over AOP**: precise, opt-in hooks without full-blown aspect weavers.
383
+
384
+ -----
385
+
386
+ **TL;DR**
387
+ `pico-ioc` builds a **deterministic, typed dependency graph** from decorated components, factories, and interceptors. It resolves by **type** (with qualifiers and collections), memoizes **singletons**, supports **overrides**, **plugins**, **conditionals/profiles**, and **scoped subgraphs**—keeping wiring **predictable, testable, and framework-agnostic**.
@@ -0,0 +1,174 @@
1
+ # DECISIONS.md — pico-ioc
2
+
3
+ This document records **technical and architectural decisions** for pico-ioc.
4
+ Each entry includes a rationale and implications. If a decision is later changed, mark it **[REVOKED]** and link to the replacement.
5
+
6
+ ---
7
+
8
+ ## ✅ Current Decisions
9
+
10
+ ### 1) Minimum Python version: **3.10**
11
+ **Decision**: Require Python **3.10+**; drop 3.8/3.9.
12
+ **Rationale**: `typing.Annotated` + `get_type_hints(..., include_extras=True)` simplify internals and enable qualifiers/collections.
13
+ **Implications**: Users on older runtimes must upgrade; CI matrix targets 3.10+.
14
+
15
+ ---
16
+
17
+ ### 2) Resolution order: **param name → exact type → MRO → string token**
18
+ **Decision**: Name-first resolution with deterministic fallback.
19
+ **Rationale**: Ergonomic for common configs by name; still strongly typed.
20
+ **Implications**: Documented behavior; potential behavior change from older pre-1.0 versions.
21
+
22
+ ---
23
+
24
+ ### 3) Lifecycle model: **singleton per container**
25
+ **Decision**: One instance per key per container.
26
+ **Rationale**: Matches typical Python app composition (config/clients/services); simple and fast.
27
+ **Implications**: No request/session scopes at IoC level; use framework facilities if needed. `lazy=True` is available but not default.
28
+
29
+ ---
30
+
31
+ ### 4) Fail-fast bootstrap
32
+ **Decision**: Instantiate eager components after `init()`; surface errors early.
33
+ **Rationale**: Deterministic startup; no hidden runtime surprises.
34
+ **Implications**: Keep constructors cheap; push heavy I/O to explicit start/serve phases.
35
+
36
+ ---
37
+
38
+ ### 5) Keys: **typed keys preferred; string tokens allowed but discouraged**
39
+ **Decision**: Prefer class/type keys (e.g., `Flask`) over string tokens.
40
+ **Rationale**: Better IDE support, fewer collisions, clearer intent.
41
+ **Implications**: String tokens remain for interop, but are a last resort.
42
+
43
+ ---
44
+
45
+ ### 6) Qualifiers & collection injection are **first-class**
46
+ **Decision**: Support `Annotated[T, Q]` and `list[Annotated[T, Q]]` for filtering.
47
+ **Rationale**: Enables side-by-side implementations (primary/fallback) without custom registries.
48
+ **Implications**: Stable registration order is preserved for lists; empty lists are valid.
49
+
50
+ ---
51
+
52
+ ### 7) Plugins are **explicitly registered**
53
+ **Decision**: No magical discovery; pass plugins to `init(..., plugins=(...))`.
54
+ **Rationale**: Predictability and testability.
55
+ **Implications**: Slightly more verbose, but boundaries stay explicit.
56
+
57
+ ---
58
+
59
+ ### 8) Public API helper (`export_public_symbols_decorated`)
60
+ **Decision**: Provide a helper to auto-export decorated symbols in a package’s `__init__.py`.
61
+ **Rationale**: Reduces boilerplate; favors convention over configuration.
62
+ **Implications**: Dynamic export is opt-in; does not auto-register providers by itself.
63
+
64
+ ---
65
+
66
+ ### 9) Overrides in `init(...)`
67
+ **Decision**: `init(..., overrides={...})` replaces bindings at bootstrap.
68
+ **Rationale**: Simple unit testing/mocking without extra modules.
69
+ **Implications**:
70
+ - Applied **before eager instantiation** → replaced providers never run.
71
+ - Accepted forms:
72
+ - `key: instance` (constant)
73
+ - `key: callable` (provider, non-lazy)
74
+ - `key: (callable, lazy_bool)` (provider with explicit laziness)
75
+ - With `reuse=True`, subsequent `init(..., overrides=...)` mutates cached bindings.
76
+
77
+ ---
78
+
79
+ ### 10) Scoped subgraphs with `scope(...)`
80
+ **Decision**: Provide `scope(...)` to build a bounded container limited to the dependency subgraph of given roots.
81
+ **Rationale**: Faster, deterministic unit/integration setups; great for CLIs/benchmarks.
82
+ **Implications**:
83
+ - **Not** a new lifecycle: still singleton-per-container.
84
+ - Supports `include_tags`/`exclude_tags` (tags from `@component(..., tags=...)` / `@provides(..., tags=...)`).
85
+ - `strict=True` fails if deps are outside subgraph.
86
+ - Works as a context manager to ensure clean teardown.
87
+
88
+ ---
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.
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.
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.
100
+ **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.
104
+
105
+ ---
106
+
107
+ ### 12) **Conditional providers** (profiles, env, predicate)
108
+ **Decision**: `@conditional(profiles=(...), require_env=(...), predicate=callable)` activates providers based on environment vars or logic.
109
+ **Rationale**: Profile-driven wiring (`PROFILE=test/prod/ci`), optional integrations (Redis/S3) without code changes.
110
+ **Implications**:
111
+ - If no active provider satisfies a required type → **bootstrap error** (or at resolution if lazy).
112
+ - Multiple candidates may be active (use qualifiers/collections to select).
113
+ - Works seamlessly with `scope(...)`; conditionals apply before traversal.
114
+
115
+ ---
116
+
117
+ ### 13) Deterministic Provider Selection: **Policy-driven, preferring @primary**
118
+ **Decision**: When multiple providers implement the same base type, the selection is not a simple "last-wins". Instead, a policy is applied:
119
+ 1. If one or more candidates are decorated with `@primary`, the first one found will be chosen.
120
+ 2. If no candidates are marked as primary, the selection may fall back to providers decorated with `@on_missing`, or other aliasing logic.
121
+ A true "last-wins" only occurs when binding the *exact same key* multiple times, which overwrites the previous provider entry.
122
+ **Rationale**: This provides more explicit control over which implementation is the default, making the dependency graph more predictable than relying solely on scan order.
123
+ **Implications**: To select a default implementation for an interface, use `@primary`. Module ordering is a less reliable mechanism for this purpose.
124
+
125
+ ---
126
+
127
+ ### 14) Concurrency & safety
128
+ **Decision**: Container is immutable after `init()`; caches are isolated and safe across threads/tasks; no global singletons.
129
+ **Rationale**: Avoid shared mutable state; enable safe parallel use.
130
+ **Implications**: Make your **own** instances thread/async-safe if they’re shared.
131
+
132
+ ---
133
+
134
+ ## ❌ Won’t-Do Decisions
135
+
136
+ ### A) Alternative scopes (request/session)
137
+ **Decision**: No extra lifecycles beyond singleton-per-container.
138
+ **Rationale**: Keep model simple; delegate per-request/session to frameworks.
139
+ **Implications**: Avoids ownership ambiguity and complexity.
140
+
141
+ ---
142
+
143
+ ### B) Asynchronous providers (`async def`)
144
+ **Decision**: Not supported inside the container.
145
+ **Rationale**: Simplicity and determinism; no loop coupling in core.
146
+ **Implications**: If async init is required, handle it inside your component explicitly.
147
+
148
+ ---
149
+
150
+ ### C) Hot reload / dynamic re-scan
151
+ **Decision**: Not supported.
152
+ **Rationale**: Conflicts with fail-fast and immutability; complicates debugging.
153
+ **Implications**: Use framework/dev tools for code reload (e.g., `uvicorn --reload`).
154
+
155
+ ---
156
+
157
+ ## 🗃️ Deprecated / Revoked
158
+
159
+ _No entries currently._
160
+
161
+ ---
162
+
163
+ ## 📜 Changelog of Decisions
164
+
165
+ - **2025-08**: Minimum Python 3.10; name-first resolution; fail-fast clarified; typed keys preferred.
166
+ - **2025-09-08**: Introduced `init(..., overrides)` with defined precedence and laziness semantics.
167
+ - **2025-09-13**: Added `scope(...)` for bounded containers with tag pruning and strict mode.
168
+ - **2025-09-14**: Added **Interceptors API** and **Conditional providers** as first-class features; documented last-wins registration and concurrency stance.
169
+
170
+ ---
171
+
172
+ **Summary**: pico-ioc remains **simple, deterministic, and fail-fast**.
173
+ We favor typed wiring, explicit registration, and small, composable primitives (overrides, scope, interceptors, conditionals) instead of heavyweight AOP or multi-scope lifecycles.
174
+