pico-ioc 1.2.0__tar.gz → 1.4.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 (89) hide show
  1. pico_ioc-1.4.0/.llm/ARCHITECTURE.md +428 -0
  2. pico_ioc-1.4.0/.llm/DECISIONS.md +185 -0
  3. pico_ioc-1.4.0/.llm/GUIDE-CONFIGURATION-INJECTION.md +129 -0
  4. pico_ioc-1.4.0/.llm/GUIDE.md +496 -0
  5. pico_ioc-1.4.0/.llm/GUIDE_CQRS.md +110 -0
  6. pico_ioc-1.4.0/.llm/GUIDE_CREATING_PLUGINS_AND_INTERCEPTORS.md +182 -0
  7. pico_ioc-1.4.0/.llm/OVERVIEW.md +167 -0
  8. {pico_ioc-1.2.0 → pico_ioc-1.4.0}/CHANGELOG.md +35 -0
  9. {pico_ioc-1.2.0/src/pico_ioc.egg-info → pico_ioc-1.4.0}/PKG-INFO +65 -16
  10. pico_ioc-1.4.0/README.md +196 -0
  11. pico_ioc-1.4.0/src/pico_ioc/__init__.py +58 -0
  12. pico_ioc-1.4.0/src/pico_ioc/_state.py +75 -0
  13. pico_ioc-1.4.0/src/pico_ioc/_version.py +1 -0
  14. pico_ioc-1.4.0/src/pico_ioc/api.py +221 -0
  15. pico_ioc-1.4.0/src/pico_ioc/builder.py +294 -0
  16. pico_ioc-1.4.0/src/pico_ioc/config.py +332 -0
  17. {pico_ioc-1.2.0 → pico_ioc-1.4.0}/src/pico_ioc/container.py +73 -26
  18. pico_ioc-1.4.0/src/pico_ioc/decorators.py +158 -0
  19. pico_ioc-1.4.0/src/pico_ioc/interceptors.py +56 -0
  20. {pico_ioc-1.2.0 → pico_ioc-1.4.0}/src/pico_ioc/plugins.py +17 -1
  21. pico_ioc-1.4.0/src/pico_ioc/policy.py +245 -0
  22. {pico_ioc-1.2.0 → pico_ioc-1.4.0}/src/pico_ioc/proxy.py +59 -7
  23. {pico_ioc-1.2.0 → pico_ioc-1.4.0}/src/pico_ioc/resolver.py +54 -46
  24. pico_ioc-1.4.0/src/pico_ioc/scanner.py +203 -0
  25. pico_ioc-1.4.0/src/pico_ioc/scope.py +46 -0
  26. pico_ioc-1.4.0/src/pico_ioc/utils.py +25 -0
  27. {pico_ioc-1.2.0 → pico_ioc-1.4.0/src/pico_ioc.egg-info}/PKG-INFO +65 -16
  28. pico_ioc-1.4.0/src/pico_ioc.egg-info/SOURCES.txt +69 -0
  29. pico_ioc-1.4.0/tests/test_api.py +269 -0
  30. pico_ioc-1.4.0/tests/test_conditional_with_predicate.py +42 -0
  31. pico_ioc-1.4.0/tests/test_config_injection.py +176 -0
  32. pico_ioc-1.4.0/tests/test_container.py +204 -0
  33. pico_ioc-1.4.0/tests/test_core_helpers_and_errors.py +86 -0
  34. pico_ioc-1.4.0/tests/test_decorators_and_policy.py +216 -0
  35. pico_ioc-1.4.0/tests/test_defaults_and_overrides.py +71 -0
  36. pico_ioc-1.4.0/tests/test_factory_policy_and_defaults.py +179 -0
  37. pico_ioc-1.4.0/tests/test_fingerprint_public.py +31 -0
  38. pico_ioc-1.4.0/tests/test_interceptors.py +269 -0
  39. pico_ioc-1.4.0/tests/test_no_overrides_needed_with_on_missing.py +30 -0
  40. pico_ioc-1.4.0/tests/test_on_missing_and_primary_mix.py +29 -0
  41. pico_ioc-1.4.0/tests/test_on_missing_blackbox.py +90 -0
  42. pico_ioc-1.4.0/tests/test_on_missing_component.py +50 -0
  43. pico_ioc-1.4.0/tests/test_on_missing_factory.py +29 -0
  44. pico_ioc-1.4.0/tests/test_policy_and_container_helpers.py +106 -0
  45. pico_ioc-1.4.0/tests/test_policy_env_activation.py +33 -0
  46. pico_ioc-1.4.0/tests/test_policy_profile_primary.py +35 -0
  47. pico_ioc-1.4.0/tests/test_proxy_unit.py +188 -0
  48. pico_ioc-1.4.0/tests/test_scanner_providers.py +151 -0
  49. pico_ioc-1.4.0/tests/test_scope.py +80 -0
  50. pico_ioc-1.4.0/tests/test_scope_defaults.py +69 -0
  51. pico_ioc-1.4.0/tests/test_scope_defaults_and_policy.py +38 -0
  52. pico_ioc-1.2.0/.llm/ARCHITECTURE.md +0 -357
  53. pico_ioc-1.2.0/.llm/DECISIONS.md +0 -150
  54. pico_ioc-1.2.0/.llm/GUIDE.md +0 -504
  55. pico_ioc-1.2.0/.llm/OVERVIEW.md +0 -160
  56. pico_ioc-1.2.0/README.md +0 -147
  57. pico_ioc-1.2.0/src/pico_ioc/__init__.py +0 -32
  58. pico_ioc-1.2.0/src/pico_ioc/_state.py +0 -10
  59. pico_ioc-1.2.0/src/pico_ioc/_version.py +0 -1
  60. pico_ioc-1.2.0/src/pico_ioc/api.py +0 -289
  61. pico_ioc-1.2.0/src/pico_ioc/decorators.py +0 -79
  62. pico_ioc-1.2.0/src/pico_ioc/scanner.py +0 -230
  63. pico_ioc-1.2.0/src/pico_ioc/typing_utils.py +0 -29
  64. pico_ioc-1.2.0/src/pico_ioc.egg-info/SOURCES.txt +0 -43
  65. pico_ioc-1.2.0/tests/test_api_unit.py +0 -197
  66. pico_ioc-1.2.0/tests/test_container_get_all.py +0 -23
  67. pico_ioc-1.2.0/tests/test_container_unit.py +0 -125
  68. pico_ioc-1.2.0/tests/test_proxy_unit.py +0 -322
  69. pico_ioc-1.2.0/tests/test_typing_utils_unit.py +0 -102
  70. {pico_ioc-1.2.0 → pico_ioc-1.4.0}/.coveragerc +0 -0
  71. {pico_ioc-1.2.0 → pico_ioc-1.4.0}/.github/workflows/ci.yml +0 -0
  72. {pico_ioc-1.2.0 → pico_ioc-1.4.0}/.github/workflows/publish-to-pypi.yml +0 -0
  73. {pico_ioc-1.2.0 → pico_ioc-1.4.0}/LICENSE +0 -0
  74. {pico_ioc-1.2.0 → pico_ioc-1.4.0}/MANIFEST.in +0 -0
  75. {pico_ioc-1.2.0 → pico_ioc-1.4.0}/pyproject.toml +0 -0
  76. {pico_ioc-1.2.0 → pico_ioc-1.4.0}/setup.cfg +0 -0
  77. {pico_ioc-1.2.0 → pico_ioc-1.4.0}/src/pico_ioc/public_api.py +0 -0
  78. {pico_ioc-1.2.0 → pico_ioc-1.4.0}/src/pico_ioc.egg-info/dependency_links.txt +0 -0
  79. {pico_ioc-1.2.0 → pico_ioc-1.4.0}/src/pico_ioc.egg-info/top_level.txt +0 -0
  80. {pico_ioc-1.2.0 → pico_ioc-1.4.0}/tests/test_decorators_unit.py +0 -0
  81. {pico_ioc-1.2.0 → pico_ioc-1.4.0}/tests/test_pico_ioc.py +0 -0
  82. {pico_ioc-1.2.0 → pico_ioc-1.4.0}/tests/test_pico_ioc_additional.py +0 -0
  83. {pico_ioc-1.2.0 → pico_ioc-1.4.0}/tests/test_pico_ioc_discovery.py +0 -0
  84. {pico_ioc-1.2.0 → pico_ioc-1.4.0}/tests/test_public_api.py +0 -0
  85. {pico_ioc-1.2.0 → pico_ioc-1.4.0}/tests/test_qualifiers_unit.py +0 -0
  86. {pico_ioc-1.2.0 → pico_ioc-1.4.0}/tests/test_resolver_unit.py +0 -0
  87. {pico_ioc-1.2.0 → pico_ioc-1.4.0}/tests/test_scanner_unit.py +0 -0
  88. {pico_ioc-1.2.0 → pico_ioc-1.4.0}/tests/test_scope_unit.py +0 -0
  89. {pico_ioc-1.2.0 → pico_ioc-1.4.0}/tox.ini +0 -0
@@ -0,0 +1,428 @@
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
+ - **Config Component** → class marked with `@config_component`. Instantiated and populated from external sources like files or environment variables.
32
+ - **Factory component** → class marked with `@factory_component`; owns provider methods via `@provides(key=TypeOrToken)`. Providers return *externals* (e.g., `Flask`, DB clients).
33
+ - **Interceptor** → class or function marked with `@interceptor`. Discovered automatically to apply cross-cutting logic.
34
+ - **Container** → built by `pico_ioc.init(mod_or_list, ...)`; resolve with `container.get(KeyOrType)`.
35
+
36
+ ### Bootstrap sequence
37
+
38
+ ```mermaid
39
+ sequenceDiagram
40
+ participant App as Your package(s)
41
+ participant IOC as pico-ioc Container
42
+ App->>IOC: init(packages, config, ...)
43
+ IOC->>IOC: Create ConfigRegistry from sources
44
+ IOC->>App: scan decorators (@component, @config_component, @interceptor)
45
+ IOC->>IOC: register providers and collect interceptor declarations
46
+ IOC->>IOC: build and activate interceptors
47
+ IOC->>IOC: apply policy (e.g., @primary, @on_missing aliases)
48
+ IOC->>IOC: apply overrides (replace providers/constants)
49
+ IOC->>IOC: instantiate eager components
50
+ App->>IOC: get(Service)
51
+ IOC->>IOC: resolve dependencies (with interception)
52
+ IOC-->>App: instance(Service)
53
+ ```
54
+
55
+ ---
56
+
57
+ ## 3\) Discovery & registration
58
+
59
+ 1. **Scan inputs** passed to `init(...)`: module or list of modules/packages.
60
+ 2. **Collect**:
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.
63
+ * `@factory_component` classes → introspected for `@provides(key=...)` methods.
64
+ * `@interceptor` classes/functions → collected for activation.
65
+ * `@plugin` classes → if explicitly passed via `init(..., plugins=(...))`.
66
+ 3. **Registry** (frozen after bootstrap):
67
+ * Map **key → provider**. Keys are typically **types**; string tokens are also supported.
68
+
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.
70
+
71
+ ---
72
+
73
+ ## 4\) Resolution algorithm (deterministic)
74
+
75
+ When constructing a component `C`:
76
+
77
+ 1. Inspect `__init__(self, ...)`; collect **type-annotated** parameters (excluding `self`).
78
+ 2. For each parameter `p: T`, resolve by this order:
79
+ 1. **By name**: if a provider key matches the parameter name `p` (if `prefer_name_first=True`).
80
+ 2. **Exact type** `T`.
81
+ 3. **MRO walk**: first registered base class of `T`.
82
+ 4. **By name (fallback)**: if a provider key matches the parameter name `p`.
83
+ 3. Instantiate dependencies depth-first; cache singletons.
84
+ 4. Construct `C` with resolved instances.
85
+
86
+ ### Failure modes
87
+
88
+ * **No provider** for a required key → **bootstrap error** (fail fast) with a full dependency chain.
89
+ * **Ambiguous/incompatible** registrations → policy resolves to a single provider or raises an error.
90
+
91
+ ### 4b) Collection resolution
92
+
93
+ If the constructor requests `list[T]` or `list[Annotated[T, Q]]`:
94
+
95
+ * Return **all** compatible providers for `T`.
96
+ * If `Q` (qualifier) is present, filter to matching ones.
97
+ * Registration order is preserved; no implicit sorting.
98
+ * Returns an empty list if no matches.
99
+
100
+ ---
101
+
102
+ ## 5\) Lifecycles & scopes
103
+
104
+ * **Singleton per container**: a provider is instantiated at most once and cached.
105
+ * **Lazy proxies (optional)**: `@component(lazy=True)` or `@provides(lazy=True)` defers instantiation until first use via a `ComponentProxy`. Prefer eager to catch errors early.
106
+
107
+ **Rationale:** Most Python app composition (config, clients, web apps) fits singleton-per-container; it’s simple and fast.
108
+
109
+ ---
110
+
111
+ ## 6\) Factories & providers
112
+
113
+ Use `@factory_component` for **externals** (framework apps, DB clients, engines).
114
+
115
+ ```python
116
+ from pico_ioc import factory_component, provides
117
+ from flask import Flask
118
+
119
+ @factory_component
120
+ class AppFactory:
121
+ @provides(key=Flask)
122
+ def provide_flask(self) -> Flask:
123
+ app = Flask(__name__)
124
+ app.config["JSON_AS_ASCII"] = False
125
+ return app
126
+ ```
127
+
128
+ Guidelines:
129
+
130
+ * Providers should be **pure constructors** (no long-running work).
131
+ * Prefer **typed keys** (e.g., `Flask`) over strings.
132
+
133
+ ---
134
+
135
+ ## 7\) Concurrency model
136
+
137
+ * Container state is **immutable after init**.
138
+ * Caches & resolution are **thread/async safe** (internal isolation; no global singletons).
139
+ * Instances you create **must** be safe for your usage patterns; the container cannot fix non-thread-safe libraries.
140
+
141
+ ---
142
+
143
+ ## 8\) Error handling & diagnostics
144
+
145
+ * **Bootstrap**:
146
+ * Missing providers → explicit `NameError` with full dependency chain details.
147
+ * Duplicate keys → resolved by policy, preferring `@primary`.
148
+ * **Runtime**:
149
+ * Exceptions from providers/constructors bubble up. The resolver path is included in the error for easier debugging.
150
+
151
+ **Tip:** Keep constructors **cheap**; push I/O to explicit start/serve methods.
152
+
153
+ ---
154
+
155
+ ## 9\) Configuration
156
+
157
+ Configuration is treated as a first-class, type-safe component using a dedicated injection system.
158
+
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.
189
+
190
+ ```python
191
+ from pico_ioc import component
192
+
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
+ ---
207
+
208
+ ## 10\) Overrides & composition
209
+
210
+ ### 10.1 Module-ordered overrides
211
+
212
+ The policy engine respects definition order. While not a strict "last-wins", providers marked `@primary` will take precedence over others discovered during the scan.
213
+
214
+ ### 10.2 Direct overrides argument
215
+
216
+ `init()` accepts an `overrides` dictionary for ad-hoc replacement.
217
+
218
+ ```python
219
+ c = init(app, overrides={
220
+ Repo: FakeRepo(), # constant instance
221
+ "fast_model": lambda: {"mock": True}, # provider
222
+ "expensive": (lambda: object(), True), # provider with lazy=True
223
+ })
224
+ ```
225
+
226
+ **Semantics:**
227
+
228
+ * Applied **after scanning and policy** but before eager instantiation → replaced providers never run.
229
+ * Accepted forms:
230
+ * `key: instance`
231
+ * `key: callable`
232
+ * `key: (callable, lazy_bool)`
233
+ * With `reuse=True`, re-calling `init(..., overrides=...)` applies new overrides to the cached container.
234
+
235
+ ---
236
+
237
+ ## 11\) Interceptors (AOP & Lifecycle Hooks)
238
+
239
+ Interceptors are components that apply cross-cutting logic like logging, metrics, or policy enforcement. They are discovered automatically via the `@interceptor` decorator.
240
+
241
+ `pico-ioc` supports two kinds of interceptors:
242
+
243
+ ### Method Interceptors
244
+
245
+ 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
+
247
+ ```python
248
+ from pico_ioc import interceptor
249
+ from pico_ioc.interceptors import MethodInterceptor, Invocation
250
+
251
+ @interceptor(order=-10) # lower order runs first
252
+ class LoggingInterceptor(MethodInterceptor):
253
+ def __call__(self, inv: Invocation, proceed):
254
+ print(f"Calling {inv.method_name}...")
255
+ result = proceed()
256
+ print(f"Finished {inv.method_name}.")
257
+ return result
258
+ ```
259
+
260
+ ### Container Interceptors
261
+
262
+ These implement the `ContainerInterceptor` protocol and hook into the container's internal lifecycle events.
263
+
264
+ **Hook points**:
265
+
266
+ * `on_resolve(key, annotation, qualifiers)`
267
+ * `on_before_create(key)`
268
+ * `on_after_create(key, instance)` → may return a **wrapped/replaced** instance.
269
+ * `on_exception(key, exc)`
270
+
271
+ **Registration:** Interceptors are discovered by the scanner during `init()` or `scope()`. There is no need to pass them manually. Their activation can be controlled with the same `@conditional` decorator and gates (`profiles`, `require_env`) used for other components.
272
+
273
+ ---
274
+
275
+ ## 12\) Profiles & conditional providers
276
+
277
+ Use `@conditional` to **activate providers based on profiles, environment variables, or a predicate function**.
278
+
279
+ ```python
280
+ from pico_ioc import component, conditional
281
+
282
+ class Cache: ...
283
+
284
+ @component
285
+ @conditional(profiles=("prod", "staging"))
286
+ class RedisCache(Cache): ...
287
+
288
+ @component
289
+ @conditional(require_env=("REDIS_URL",))
290
+ class AnotherRedisCache(Cache): ...
291
+
292
+ @component
293
+ @conditional(predicate=lambda: os.path.exists("/tmp/use_mem"))
294
+ class InMemoryCache(Cache): ...
295
+ ```
296
+
297
+ **Rules**:
298
+
299
+ * `profiles=("A","B")` → active if any profile passed to `init()` or `scope()` matches.
300
+ * `require_env=("A","B")` → all environment variables must exist and be non-empty.
301
+ * `predicate=callable` → must return a truthy value to activate.
302
+ * If no active provider satisfies a required type and something depends on it → **bootstrap error** (fail fast).
303
+
304
+ ---
305
+
306
+ ## 13\) Qualifiers & collection injection
307
+
308
+ Attach qualifiers to group/select implementations using `@qualifier`.
309
+
310
+ * Request `list[T]` → injects all registered implementations of `T`.
311
+ * Request `list[Annotated[T, Q]]` → injects only those implementations of `T` tagged with qualifier `Q`.
312
+
313
+ This preserves registration order and returns a stable list.
314
+
315
+ ---
316
+
317
+ ## 14\) Plugins
318
+
319
+ `@plugin` classes implementing the `PicoPlugin` protocol can observe the **container lifecycle**.
320
+
321
+ * `before_scan(package, binder)`
322
+ * `after_scan(package, binder)`
323
+ * `after_bind(container, binder)`
324
+ * `before_eager(container, binder)`
325
+ * `after_ready(container, binder)`
326
+
327
+ Plugins are passed **explicitly** via `init(..., plugins=(MyPlugin(),))`. Prefer **interceptors** for fine-grained wiring events; use **plugins** for coarse lifecycle integration.
328
+
329
+ ---
330
+
331
+ ## 15\) Scoped subgraphs (`scope`)
332
+
333
+ Build a **bounded container** containing only dependencies reachable from selected **roots**.
334
+
335
+ ```python
336
+ from pico_ioc import scope
337
+ from src.runner_service import RunnerService
338
+ from tests.fakes import FakeDocker
339
+ import src
340
+
341
+ c = scope(
342
+ modules=[src],
343
+ roots=[RunnerService],
344
+ overrides={"docker.DockerClient": FakeDocker()},
345
+ strict=True, lazy=True,
346
+ )
347
+ svc = c.get(RunnerService)
348
+ ```
349
+
350
+ ### Tag-based pruning
351
+
352
+ 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.
353
+
354
+ ### Semantics
355
+
356
+ * **Limited reach**: only providers transitively reachable from `roots` are included in the final graph.
357
+ * **Deterministic precedence**: `overrides > scoped providers > base container providers` (if `strict=False`).
358
+ * **Strict mode**: controls whether missing dependencies raise an error (`True`) or can be resolved from the `base` container (`False`).
359
+ * **Lifecycle**: still **singleton-per-container**; `scope` does **not** add request/session scopes.
360
+ * **Context manager**: `with scope(...):` is supported.
361
+
362
+ **Use cases:** fast unit tests, integration-lite, CLI tools, microbenchmarks.
363
+
364
+ ---
365
+
366
+ ## 16\) Diagnostics & diagrams
367
+
368
+ ### Registry & resolution (class diagram)
369
+
370
+ ```mermaid
371
+ classDiagram
372
+ class PicoContainer {
373
+ +get(key) instance
374
+ +get_all(base_type) sequence
375
+ +add_method_interceptor(it)
376
+ +add_container_interceptor(it)
377
+ - _providers: Map[key, ProviderMeta]
378
+ - _singletons: Map[key, instance]
379
+ }
380
+ class ProviderMeta {
381
+ + factory: Callable
382
+ + lazy: bool
383
+ + tags: tuple
384
+ + qualifiers: tuple
385
+ }
386
+ class MethodInterceptor {
387
+ +__call__(inv, proceed)
388
+ }
389
+ class ContainerInterceptor {
390
+ +on_resolve()
391
+ +on_before_create()
392
+ +on_after_create()
393
+ +on_exception()
394
+ }
395
+ PicoContainer "1" o-- "*" MethodInterceptor
396
+ PicoContainer "1" o-- "*" ContainerInterceptor
397
+ ```
398
+
399
+ ### Resolution flow (activity)
400
+
401
+ ```mermaid
402
+ flowchart TD
403
+ A[get(Type T)] --> B{Cached?}
404
+ B -- yes --> Z[Return cached instance]
405
+ B -- no --> D[Resolve dependencies for T (recurse)]
406
+ D --> I_BEFORE[ContainerInterceptors: on_before_create]
407
+ I_BEFORE --> F[Instantiate T]
408
+ F -- exception --> I_EXC[ContainerInterceptors: on_exception]
409
+ F -- success --> H[Wrap with MethodInterceptors if needed]
410
+ H --> I_AFTER[ContainerInterceptors: on_after_create]
411
+ I_AFTER --> G[Cache instance]
412
+ G --> Z
413
+ ```
414
+
415
+ ---
416
+
417
+ ## 17\) Rationale & trade-offs
418
+
419
+ * **Typed keys first**: better IDE support; fewer foot-guns than strings.
420
+ * **Singleton-per-container**: matches typical Python app composition; simpler mental model.
421
+ * **Explicit decorators**: determinism and debuggability over magical auto-wiring.
422
+ * **Fail fast**: configuration and graph issues surface at startup, not mid-request.
423
+ * **Interceptors over AOP**: precise, opt-in hooks without full-blown aspect weavers.
424
+
425
+ ---
426
+
427
+ **TL;DR**
428
+ `pico-ioc` builds a **deterministic, typed dependency graph** from decorated components, factories, and interceptors. It resolves by **type** (with qualifiers and collections), memoizes **singletons**, supports **type-safe configuration injection**, **overrides**, **plugins**, **conditionals/profiles**, and **scoped subgraphs**—keeping wiring **predictable, testable, and framework-agnostic**.
@@ -0,0 +1,185 @@
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
+ ### 15) Configuration Injection
135
+ **Decision**: Provide `@config_component` for strongly typed configuration classes, populated from ordered `ConfigSource`s (`EnvSource`, `FileSource`, etc.).
136
+ **Rationale**: Type-safe configuration with minimal boilerplate, supporting both automatic autowiring by field name and manual overrides (`Env`, `File`, `Path`, `Value`).
137
+ **Implications**:
138
+ - Precedence is explicit: `overrides` > sources (in order) > field defaults.
139
+ - Missing required fields (no default and not resolvable) raise `NameError`.
140
+ - Supported formats: env vars, JSON, INI, dotenv, YAML (if available).
141
+ - Encourages using `dataclass(frozen=True)` for immutable, validated settings.
142
+
143
+ ---
144
+
145
+ ## ❌ Won’t-Do Decisions
146
+
147
+ ### A) Alternative scopes (request/session)
148
+ **Decision**: No extra lifecycles beyond singleton-per-container.
149
+ **Rationale**: Keep model simple; delegate per-request/session to frameworks.
150
+ **Implications**: Avoids ownership ambiguity and complexity.
151
+
152
+ ---
153
+
154
+ ### B) Asynchronous providers (`async def`)
155
+ **Decision**: Not supported inside the container.
156
+ **Rationale**: Simplicity and determinism; no loop coupling in core.
157
+ **Implications**: If async init is required, handle it inside your component explicitly.
158
+
159
+ ---
160
+
161
+ ### C) Hot reload / dynamic re-scan
162
+ **Decision**: Not supported.
163
+ **Rationale**: Conflicts with fail-fast and immutability; complicates debugging.
164
+ **Implications**: Use framework/dev tools for code reload (e.g., `uvicorn --reload`).
165
+
166
+ ---
167
+
168
+ ## 🗃️ Deprecated / Revoked
169
+
170
+ _No entries currently._
171
+
172
+ ---
173
+
174
+ ## 📜 Changelog of Decisions
175
+
176
+ - **2025-08**: Minimum Python 3.10; name-first resolution; fail-fast clarified; typed keys preferred.
177
+ - **2025-09-08**: Introduced `init(..., overrides)` with defined precedence and laziness semantics.
178
+ - **2025-09-13**: Added `scope(...)` for bounded containers with tag pruning and strict mode.
179
+ - **2025-09-14**: Added **Interceptors API** and **Conditional providers** as first-class features; documented last-wins registration and concurrency stance.
180
+
181
+ ---
182
+
183
+ **Summary**: pico-ioc remains **simple, deterministic, and fail-fast**.
184
+ We favor typed wiring, explicit registration, and small, composable primitives (overrides, scope, interceptors, conditionals) instead of heavyweight AOP or multi-scope lifecycles.
185
+