pico-ioc 0.6.0__tar.gz → 1.1.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 (56) hide show
  1. {pico_ioc-0.6.0 → pico_ioc-1.1.0}/.github/workflows/ci.yml +1 -1
  2. pico_ioc-1.1.0/.llm/ARCHITECTURE.md +306 -0
  3. pico_ioc-1.1.0/.llm/DECISIONS.md +135 -0
  4. pico_ioc-1.1.0/.llm/GUIDE.md +411 -0
  5. pico_ioc-1.1.0/.llm/OVERVIEW.md +133 -0
  6. pico_ioc-1.1.0/CHANGELOG.md +56 -0
  7. pico_ioc-1.1.0/PKG-INFO +166 -0
  8. pico_ioc-1.1.0/README.md +121 -0
  9. {pico_ioc-0.6.0 → pico_ioc-1.1.0}/pyproject.toml +1 -3
  10. {pico_ioc-0.6.0 → pico_ioc-1.1.0}/src/pico_ioc/__init__.py +14 -10
  11. {pico_ioc-0.6.0 → pico_ioc-1.1.0}/src/pico_ioc/_state.py +2 -0
  12. pico_ioc-1.1.0/src/pico_ioc/_version.py +1 -0
  13. pico_ioc-1.1.0/src/pico_ioc/api.py +140 -0
  14. pico_ioc-1.1.0/src/pico_ioc/container.py +158 -0
  15. pico_ioc-1.1.0/src/pico_ioc/decorators.py +76 -0
  16. pico_ioc-1.1.0/src/pico_ioc/public_api.py +76 -0
  17. pico_ioc-1.1.0/src/pico_ioc/resolver.py +110 -0
  18. pico_ioc-1.1.0/src/pico_ioc/scanner.py +238 -0
  19. {pico_ioc-0.6.0 → pico_ioc-1.1.0}/src/pico_ioc/typing_utils.py +9 -4
  20. pico_ioc-1.1.0/src/pico_ioc.egg-info/PKG-INFO +166 -0
  21. {pico_ioc-0.6.0 → pico_ioc-1.1.0}/src/pico_ioc.egg-info/SOURCES.txt +9 -2
  22. {pico_ioc-0.6.0 → pico_ioc-1.1.0}/tests/test_api_unit.py +75 -1
  23. pico_ioc-1.1.0/tests/test_container_get_all.py +23 -0
  24. {pico_ioc-0.6.0 → pico_ioc-1.1.0}/tests/test_pico_ioc_discovery.py +46 -27
  25. {pico_ioc-0.6.0 → pico_ioc-1.1.0}/tests/test_proxy_unit.py +2 -2
  26. pico_ioc-1.1.0/tests/test_public_api.py +220 -0
  27. pico_ioc-1.1.0/tests/test_qualifiers_unit.py +70 -0
  28. pico_ioc-1.1.0/tests/test_resolver_unit.py +121 -0
  29. {pico_ioc-0.6.0 → pico_ioc-1.1.0}/tox.ini +1 -1
  30. pico_ioc-0.6.0/PKG-INFO +0 -290
  31. pico_ioc-0.6.0/README.md +0 -243
  32. pico_ioc-0.6.0/doc4llm/architecture-pico-ioc.md +0 -288
  33. pico_ioc-0.6.0/doc4llm/usage-patterns.md +0 -198
  34. pico_ioc-0.6.0/src/pico_ioc/_version.py +0 -1
  35. pico_ioc-0.6.0/src/pico_ioc/api.py +0 -74
  36. pico_ioc-0.6.0/src/pico_ioc/container.py +0 -43
  37. pico_ioc-0.6.0/src/pico_ioc/decorators.py +0 -33
  38. pico_ioc-0.6.0/src/pico_ioc/resolver.py +0 -58
  39. pico_ioc-0.6.0/src/pico_ioc/scanner.py +0 -105
  40. pico_ioc-0.6.0/src/pico_ioc.egg-info/PKG-INFO +0 -290
  41. pico_ioc-0.6.0/tests/test_resolver_unit.py +0 -162
  42. {pico_ioc-0.6.0 → pico_ioc-1.1.0}/.coveragerc +0 -0
  43. {pico_ioc-0.6.0 → pico_ioc-1.1.0}/.github/workflows/publish-to-pypi.yml +0 -0
  44. {pico_ioc-0.6.0 → pico_ioc-1.1.0}/LICENSE +0 -0
  45. {pico_ioc-0.6.0 → pico_ioc-1.1.0}/MANIFEST.in +0 -0
  46. {pico_ioc-0.6.0 → pico_ioc-1.1.0}/setup.cfg +0 -0
  47. {pico_ioc-0.6.0 → pico_ioc-1.1.0}/src/pico_ioc/plugins.py +0 -0
  48. {pico_ioc-0.6.0 → pico_ioc-1.1.0}/src/pico_ioc/proxy.py +0 -0
  49. {pico_ioc-0.6.0 → pico_ioc-1.1.0}/src/pico_ioc.egg-info/dependency_links.txt +0 -0
  50. {pico_ioc-0.6.0 → pico_ioc-1.1.0}/src/pico_ioc.egg-info/top_level.txt +0 -0
  51. {pico_ioc-0.6.0 → pico_ioc-1.1.0}/tests/test_container_unit.py +0 -0
  52. {pico_ioc-0.6.0 → pico_ioc-1.1.0}/tests/test_decorators_unit.py +0 -0
  53. {pico_ioc-0.6.0 → pico_ioc-1.1.0}/tests/test_pico_ioc.py +0 -0
  54. {pico_ioc-0.6.0 → pico_ioc-1.1.0}/tests/test_pico_ioc_additional.py +0 -0
  55. {pico_ioc-0.6.0 → pico_ioc-1.1.0}/tests/test_scanner_unit.py +0 -0
  56. {pico_ioc-0.6.0 → pico_ioc-1.1.0}/tests/test_typing_utils_unit.py +0 -0
@@ -13,7 +13,7 @@ jobs:
13
13
  strategy:
14
14
  fail-fast: false
15
15
  matrix:
16
- python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12", "3.13" ]
16
+ python-version: [ "3.10", "3.11", "3.12", "3.13" ]
17
17
 
18
18
  steps:
19
19
  - name: Checkout
@@ -0,0 +1,306 @@
1
+ # pico-ioc — Architecture
2
+
3
+ > Scope: internal model, wiring algorithm, lifecycle, and design trade-offs.
4
+ > Non-goals: user tutorials or recipes (see `GUIDE.md`), product pitch (see `OVERVIEW.md`).
5
+
6
+ > ⚠️ **Requires Python 3.10+** (uses `typing.Annotated` and `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**: annotate constructors, avoid runtime reflection tricks.
17
+ * **Framework-agnostic**: no hard dependency on Flask/FastAPI/etc.
18
+ * **Safe by default**: thread/async friendly, no global mutable singletons.
19
+
20
+ **Non-goals**
21
+
22
+ * Full Spring-like feature set (AOP, complex scopes, bean post-processors).
23
+ * Runtime reconfiguration or hot-swap of graphs.
24
+ * Magical auto-imports across the filesystem.
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` that owns one or more **providers** declared via `@provides(key=TypeOrToken)`. Providers return *externals* (e.g., `Flask`, clients, engines).
32
+ * **Container**: built by `pico_ioc.init(module_or_list)`; resolves dependencies with `container.get(KeyOrType)`.
33
+
34
+ ### Bootstrap sequence (simplified)
35
+
36
+ ```mermaid
37
+ sequenceDiagram
38
+ participant App as Your package(s)
39
+ participant IOC as pico-ioc Container
40
+ App->>IOC: init([app, overrides?])
41
+ IOC->>App: scan decorators (@component, @factory_component)
42
+ IOC->>IOC: register providers (key -> factory)
43
+ IOC->>IOC: validate graph (constructor types, duplicates)
44
+ App->>IOC: get(Service)
45
+ IOC->>IOC: resolve deps (Repo, Config, ...)
46
+ IOC-->>App: instance(Service)
47
+ ````
48
+
49
+ ---
50
+
51
+ ## 3) Discovery & registration
52
+
53
+ 1. **Scan inputs** passed to `init(...)`: a module or list of modules/packages.
54
+ 2. **Collect**:
55
+
56
+ * `@component` classes → registered by **type** (the class itself).
57
+ * `@factory_component` classes → introspected for `@provides(key=...)` methods.
58
+ * `@plugin` classes → collected if explicitly passed to `init(…, plugins=(…))`.
59
+ 3. **Registry** (immutable after bootstrap):
60
+
61
+ * `key -> provider` map. Keys are usually **types**. String tokens are supported but discouraged.
62
+
63
+ > **Invariant:** A key has **at most one** active provider; the **last registration wins**.
64
+ > This enables test overrides by ordering `init([app, test_overrides])`.
65
+
66
+ ---
67
+
68
+ ## 4) Resolution algorithm (deterministic)
69
+
70
+ When constructing a component `C`:
71
+
72
+ 1. Inspect `__init__(self, ...)` and collect **type-annotated** parameters (excluding `self`).
73
+ 2. For each parameter `p: T` resolve **by key** using this order:
74
+
75
+ 1. **Exact type** `T`
76
+ 2. **MRO walk**: first registered base class or protocol
77
+ 3. **String key** (only if `@provides(key="token")` was used)
78
+ 3. Instantiate dependencies depth-first; memoize singletons.
79
+ 4. Construct `C` with resolved instances.
80
+
81
+ **Failure modes**
82
+
83
+ * No provider for a required key → **bootstrap error** (fail fast).
84
+ * Ambiguous/incompatible registrations → **bootstrap error** with a precise hint.
85
+
86
+ ### 4b) Collection resolution
87
+
88
+ If a constructor requests `list[T]` or `list[Annotated[T, Q]]`:
89
+
90
+ * The container resolves **all** registered providers compatible with `T`.
91
+ * If qualifiers are present (`Q`), only matching components are returned.
92
+ * Order is stable by discovery/registration; no implicit sorting.
93
+ * Empty list if no matches.
94
+
95
+ ---
96
+
97
+ ## 5) Lifecycles & scopes
98
+
99
+ * **Singleton per container** (default): each key is created once and cached.
100
+ * **Lazy proxies (optional)**: `@component(lazy=True)` defers instantiation until first use. Use sparingly; prefer eager to surface errors early.
101
+
102
+ > **Why singleton:** It matches typical app composition (config, DB clients, HTTP apps) and keeps DI simple and fast.
103
+
104
+ ---
105
+
106
+ ## 6) Factories & providers
107
+
108
+ Use `@factory_component` to integrate **externals**:
109
+
110
+ ```python
111
+ from pico_ioc import factory_component, provides
112
+ from flask import Flask
113
+
114
+ @factory_component
115
+ class AppFactory:
116
+ @provides(key=Flask)
117
+ def provide_flask(self) -> Flask:
118
+ app = Flask(__name__)
119
+ app.config["JSON_AS_ASCII"] = False
120
+ return app
121
+ ```
122
+
123
+ * **Providers** should be **pure constructors** (no long-running work).
124
+ * Prefer **typed keys** (e.g., `Flask`) over string tokens.
125
+
126
+ ---
127
+
128
+ ## 7) Concurrency model
129
+
130
+ * Container state is **immutable after init**.
131
+ * Resolution cache is **thread/async safe** via internal isolation (no global singletons).
132
+ * Instances you create **must** be thread-safe if shared (container can’t fix non-safe libs).
133
+
134
+ ---
135
+
136
+ ## 8) Error handling & diagnostics
137
+
138
+ * **Bootstrap phase**:
139
+
140
+ * Missing providers → raise with the exact parameter/type.
141
+ * Duplicate keys → last wins; log/trace retains registration order for debugging.
142
+ * Type annotation mismatch → explicit error naming the offending `__init__` param.
143
+ * **Runtime**:
144
+
145
+ * Exceptions bubble from provider/constructor; container stack traces include the dependency chain (key path).
146
+
147
+ Tip: Keep constructors **cheap** (I/O at the edges, e.g., in start/serve methods).
148
+
149
+ ---
150
+
151
+ ## 9) Configuration
152
+
153
+ * Treat config as a **component**:
154
+
155
+ ```python
156
+ @component
157
+ class Config:
158
+ WORKERS: int = int(os.getenv("WORKERS", "4"))
159
+ DEBUG: bool = os.getenv("DEBUG", "0") == "1"
160
+ ```
161
+
162
+ * Inject `Config` wherever needed; do not read `os.getenv` scattered across the codebase.
163
+
164
+ ---
165
+
166
+ ## 10) Extensibility & overrides
167
+
168
+ ### Test overrides (module-based)
169
+
170
+ Register a factory after the app module:
171
+
172
+ ```python
173
+ @factory_component
174
+ class TestOverrides:
175
+ @provides(key=Repo) # same key as production
176
+ def provide_repo(self) -> Repo:
177
+ return FakeRepo()
178
+ ````
179
+
180
+ Then:
181
+
182
+ ```python
183
+ c = init([app, test_overrides]) # overrides win by order
184
+ ```
185
+
186
+ ### Direct overrides argument
187
+
188
+ `init()` also accepts an `overrides` dict for ad-hoc replacement:
189
+
190
+ ```python
191
+ c = init(app, overrides={
192
+ Repo: FakeRepo(), # constant instance
193
+ "fast_model": lambda: {"mock": True}, # provider
194
+ "expensive": (lambda: object(), True), # provider with lazy=True
195
+ })
196
+ ```
197
+
198
+ * **Applied before eager instantiation** → replaced providers never run.
199
+ * **Formats supported**:
200
+
201
+ * `key: instance` → constant
202
+ * `key: callable` → non-lazy provider
203
+ * `key: (callable, lazy_bool)` → provider with explicit laziness
204
+ * With `reuse=True`, calling `init(..., overrides=...)` again mutates the cached container bindings.
205
+
206
+ ### Multi-env composition
207
+
208
+ Define `app.prod`, `app.dev`, `app.test` packages that import different provider sets, then call `init([app.prod])`, etc.
209
+
210
+ ### Plugins
211
+
212
+ Classes decorated with `@plugin` and implementing `PicoPlugin` can observe lifecycle events:
213
+
214
+ * `before_scan(package, binder)`
215
+ * `after_ready(container, binder)`
216
+
217
+ They are passed explicitly to `init(..., plugins=(MyPlugin(),))`.
218
+
219
+ ---
220
+
221
+ ## 11) Performance notes
222
+
223
+ * **O(1)** provider lookup by key; no reflection at call sites after bootstrap.
224
+ * Eager construction surfaces failures early and warms caches.
225
+ * Keep providers idempotent and cheap; push heavy I/O to runtime methods.
226
+
227
+ ---
228
+
229
+ ## 12) Security & boundaries
230
+
231
+ * The container **does not** open sockets/files or spawn threads by itself.
232
+ * Side effects belong to **your components**. Keep providers deterministic.
233
+ * Avoid storing secrets in code; pass secrets via your own config component.
234
+
235
+ ---
236
+
237
+ ## 13) Reference diagrams
238
+
239
+ ### Registry & resolution (class diagram)
240
+
241
+ ```mermaid
242
+ classDiagram
243
+ class Container {
244
+ +get(key) instance
245
+ -registry: Map[key, Provider]
246
+ -cache: Map[key, instance]
247
+ }
248
+ class Provider {
249
+ +create() instance
250
+ +key
251
+ }
252
+ class ComponentProvider
253
+ class FactoryProvider
254
+ Container "1" --> "*" Provider
255
+ Provider <|-- ComponentProvider
256
+ Provider <|-- FactoryProvider
257
+ ```
258
+
259
+ ### Resolution flow (activity)
260
+
261
+ ```mermaid
262
+ flowchart TD
263
+ A[Request instance for Type T] --> B{Cached?}
264
+ B -- yes --> Z[Return cached]
265
+ B -- no --> C[Find provider for T: type / MRO / token]
266
+ C -- none --> E[Raise bootstrap error]
267
+ C -- found --> D[Resolve constructor args recursively]
268
+ D --> F[Instantiate]
269
+ F --> G[Cache instance]
270
+ G --> Z[Return instance]
271
+ ```
272
+
273
+ ---
274
+
275
+ ## 14) Rationale & trade-offs
276
+
277
+ * **Typed keys first** → better ergonomics, editor support, less foot-guns than strings.
278
+ * **Singleton scope** → keeps mental model simple; Python apps rarely need per-request scoping at the container level (push that to frameworks).
279
+ * **No global state** → explicit container ownership clarifies boundaries and testability.
280
+ * **Fail fast** → configuration/graph problems are caught at start, not mid-request.
281
+
282
+ ---
283
+
284
+ ## 15) Appendix: FAQs (architecture-level)
285
+
286
+ * *Why not metaclass magic to auto-wire everything?*
287
+ Determinism and debuggability. Explicit decorators and typed constructors win.
288
+
289
+ * *Can I lazy-load everything?*
290
+ You can, but you shouldn’t. Lazy only where it truly reduces cost without hiding errors.
291
+
292
+ * *How do I integrate with Flask/FastAPI?*
293
+ Provide the app/client via a factory (`@provides(key=Flask)`), then `container.get(Flask)` in your bootstrap. See `GUIDE.md`.
294
+
295
+ ### Public API helper
296
+
297
+ `export_public_symbols_decorated` builds `__getattr__`/`__dir__` for a package’s `__init__.py`,
298
+ auto-exporting all `@component`, `@factory_component`, and `@plugin` classes, plus any `__all__`.
299
+
300
+ ---
301
+
302
+ **TL;DR**
303
+ `pico-ioc` builds a **deterministic dependency graph** at startup from decorated components, factories, and plugins.
304
+ It resolves by **type**, supports **collection injection with qualifiers**, memoizes singletons, and fails fast — so your app wiring stays **predictable, testable, and framework-agnostic**.
305
+
306
+
@@ -0,0 +1,135 @@
1
+ # DECISIONS.md — pico-ioc
2
+
3
+ This document records **technical and architectural decisions** made for pico-ioc.
4
+ Each entry has a rationale and implications. If a decision is revoked, it should be marked as **[REVOKED]** with a link to the replacement.
5
+
6
+ ---
7
+
8
+ ## ✅ Current Decisions
9
+
10
+ ### 1. Minimum Python version: **3.10**
11
+ - **Decision**: Drop support for Python 3.8 and 3.9. Require Python **3.10+**.
12
+ - **Rationale**:
13
+ * Uses `typing.Annotated` and `typing.get_type_hints(..., include_extras=True)`.
14
+ * Simplifies code paths and avoids backports/conditionals.
15
+ * Encourages modern typing practices.
16
+ - **Implications**:
17
+ * Users on older runtimes must upgrade.
18
+ * CI/CD matrix only tests Python 3.10+.
19
+
20
+ ---
21
+
22
+ ### 2. Resolution order: **name-first**
23
+ - **Decision**: Parameter name has precedence over type annotation.
24
+ - **Order**: **param name → exact type → MRO fallback → string token**.
25
+ - **Rationale**:
26
+ * Matches ergonomic use-cases (config by name).
27
+ * Keeps deterministic behavior and avoids ambiguity.
28
+ - **Implications**:
29
+ * Breaking change from earlier versions (<0.5.0).
30
+ * Documented in README and GUIDE.
31
+
32
+ ---
33
+
34
+ ### 3. Lifecycle: **singleton per container**
35
+ - **Decision**: Every provider produces exactly one instance per container.
36
+ - **Rationale**:
37
+ * Matches common Python app architecture (config, DB clients, service classes).
38
+ * Simple mental model, cheap lookups.
39
+ - **Implications**:
40
+ * No per-request or session scopes at the IoC level (delegate that to frameworks).
41
+ * Lazy proxies supported via `lazy=True`.
42
+
43
+ ---
44
+
45
+ ### 4. Fail-fast bootstrap
46
+ - **Decision**: All eager components are instantiated immediately after `init()`.
47
+ - **Rationale**:
48
+ * Surfaces missing dependencies early.
49
+ * Avoids hidden runtime errors deep into request handling.
50
+ - **Implications**:
51
+ * Slower startup if many components are eager.
52
+ * Recommended to keep constructors cheap.
53
+
54
+ ---
55
+
56
+ ### 5. Plugins: **explicit registration**
57
+ - **Decision**: Plugins must be passed explicitly to `init(..., plugins=(...))`.
58
+ - **Rationale**:
59
+ * Keeps scanning predictable and avoids magical discovery.
60
+ * Encourages explicit boundaries in app wiring.
61
+ - **Implications**:
62
+ * More verbose in user code, but safer and testable.
63
+ * `@plugin` decorator only marks, does not auto-register.
64
+
65
+ ---
66
+
67
+ ### 6. Public API helper (`export_public_symbols_decorated`)
68
+ - **Decision**: Auto-export all decorated classes/components/plugins via `__getattr__`/`__dir__`.
69
+ - **Rationale**:
70
+ * Reduces `__init__.py` boilerplate.
71
+ * Encourages convention-over-configuration.
72
+ - **Implications**:
73
+ * Explicit imports may be replaced by dynamic export.
74
+ * Errors in scanning are suppressed (non-fatal).
75
+
76
+ ---
77
+
78
+ ### 7. Overrides in `init()`
79
+ - **Decision**: `init(..., overrides={...})` allows binding custom providers/instances at bootstrap time.
80
+ - **Rationale**:
81
+ * Simplifies unit testing and ad-hoc mocking.
82
+ * Avoids creating dedicated override modules when only a few dependencies need to be replaced.
83
+ * Keeps semantics explicit: last binding wins.
84
+ - **Implications**:
85
+ * Overrides are applied **before eager instantiation** — ensuring replaced providers never run.
86
+ * Accepted formats:
87
+ - `key: instance` → constant binding
88
+ - `key: provider_callable` → non-lazy binding
89
+ - `key: (provider_callable, lazy_bool)` → binding with explicit laziness
90
+ * If `reuse=True`, calling `init(..., overrides=...)` again applies overrides on the cached container.
91
+
92
+ ---
93
+
94
+ ## ❌ Won’t-Do Decisions
95
+
96
+ ### 1. Alternative scopes (request/session)
97
+ - **Decision**: no additional scopes beyond *singleton per container* will be implemented.
98
+ - **Rationale**:
99
+ * The simplicity of the current model (one instance per container) is a core value of pico-ioc.
100
+ * Web frameworks (Flask, FastAPI, etc.) already manage request/session lifecycles.
101
+ * Adding scopes would introduce unnecessary complexity and ambiguity about object ownership.
102
+
103
+ ---
104
+
105
+ ### 2. Asynchronous providers
106
+ - **Decision**: no support for `async def` components or asynchronous resolution inside the container.
107
+ - **Rationale**:
108
+ * Keeping the library **100% synchronous** preserves the current API (`container.get(...)` is always immediate).
109
+ * Async support would require event-loop integration, `await` semantics, and multiple runtime strategies → breaking simplicity.
110
+ * If a dependency needs async initialization, it should be handled inside the component itself, not by the container.
111
+
112
+ ---
113
+
114
+ ### 3. Hot reload / dynamic re-scan
115
+ - **Decision**: no hot reload or dynamic re-scan of modules will be supported.
116
+ - **Rationale**:
117
+ * Contradicts the **fail-fast** philosophy (surface errors at startup).
118
+ * Breaks the **determinism** guarantee (container is immutable after `init()`).
119
+ * Makes debugging harder: old instances may linger, state may become inconsistent, resources may leak.
120
+ * Development-time hot reload is already handled by frameworks (`uvicorn --reload`, etc.).
121
+
122
+ ---
123
+
124
+ 📌 **Summary:** pico-ioc stays **simple, deterministic, and fail-fast**.
125
+ Features that add complexity (alternative scopes, async providers, hot reload) are intentionally excluded.
126
+
127
+ ---
128
+
129
+ ## 📜 Changelog of Decisions
130
+
131
+ - **2025-08**: Dropped Python 3.8/3.9 support, minimum 3.10.
132
+ - **2025-08**: Clarified resolution order as *name-first*.
133
+ - **2025-08**: Documented lifecycle, plugins, and fail-fast policy.
134
+ - **2025-09**: Added `init(..., overrides)` feature for test/mocking convenience.
135
+