pico-ioc 0.5.1__tar.gz → 0.6.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-0.5.1 → pico_ioc-0.6.0}/PKG-INFO +17 -11
- {pico_ioc-0.5.1 → pico_ioc-0.6.0}/README.md +16 -10
- pico_ioc-0.6.0/doc4llm/architecture-pico-ioc.md +288 -0
- pico_ioc-0.6.0/doc4llm/usage-patterns.md +198 -0
- pico_ioc-0.6.0/src/pico_ioc/__init__.py +27 -0
- pico_ioc-0.6.0/src/pico_ioc/_state.py +8 -0
- pico_ioc-0.6.0/src/pico_ioc/_version.py +1 -0
- pico_ioc-0.6.0/src/pico_ioc/api.py +74 -0
- pico_ioc-0.6.0/src/pico_ioc/container.py +43 -0
- pico_ioc-0.6.0/src/pico_ioc/decorators.py +33 -0
- pico_ioc-0.6.0/src/pico_ioc/plugins.py +12 -0
- pico_ioc-0.6.0/src/pico_ioc/proxy.py +77 -0
- pico_ioc-0.6.0/src/pico_ioc/resolver.py +58 -0
- pico_ioc-0.6.0/src/pico_ioc/scanner.py +105 -0
- pico_ioc-0.6.0/src/pico_ioc/typing_utils.py +24 -0
- {pico_ioc-0.5.1 → pico_ioc-0.6.0}/src/pico_ioc.egg-info/PKG-INFO +17 -11
- pico_ioc-0.6.0/src/pico_ioc.egg-info/SOURCES.txt +35 -0
- pico_ioc-0.6.0/tests/test_api_unit.py +123 -0
- pico_ioc-0.6.0/tests/test_container_unit.py +125 -0
- pico_ioc-0.6.0/tests/test_decorators_unit.py +138 -0
- {pico_ioc-0.5.1 → pico_ioc-0.6.0}/tests/test_pico_ioc.py +4 -1
- {pico_ioc-0.5.1 → pico_ioc-0.6.0}/tests/test_pico_ioc_additional.py +8 -2
- pico_ioc-0.6.0/tests/test_pico_ioc_discovery.py +95 -0
- pico_ioc-0.6.0/tests/test_proxy_unit.py +322 -0
- pico_ioc-0.6.0/tests/test_resolver_unit.py +162 -0
- pico_ioc-0.6.0/tests/test_scanner_unit.py +190 -0
- pico_ioc-0.6.0/tests/test_typing_utils_unit.py +102 -0
- pico_ioc-0.5.1/src/pico_ioc/__init__.py +0 -408
- pico_ioc-0.5.1/src/pico_ioc/_version.py +0 -1
- pico_ioc-0.5.1/src/pico_ioc.egg-info/SOURCES.txt +0 -16
- {pico_ioc-0.5.1 → pico_ioc-0.6.0}/.coveragerc +0 -0
- {pico_ioc-0.5.1 → pico_ioc-0.6.0}/.github/workflows/ci.yml +0 -0
- {pico_ioc-0.5.1 → pico_ioc-0.6.0}/.github/workflows/publish-to-pypi.yml +0 -0
- {pico_ioc-0.5.1 → pico_ioc-0.6.0}/LICENSE +0 -0
- {pico_ioc-0.5.1 → pico_ioc-0.6.0}/MANIFEST.in +0 -0
- {pico_ioc-0.5.1 → pico_ioc-0.6.0}/pyproject.toml +0 -0
- {pico_ioc-0.5.1 → pico_ioc-0.6.0}/setup.cfg +0 -0
- {pico_ioc-0.5.1 → pico_ioc-0.6.0}/src/pico_ioc.egg-info/dependency_links.txt +0 -0
- {pico_ioc-0.5.1 → pico_ioc-0.6.0}/src/pico_ioc.egg-info/top_level.txt +0 -0
- {pico_ioc-0.5.1 → pico_ioc-0.6.0}/tox.ini +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pico-ioc
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: A minimalist, zero-dependency Inversion of Control (IoC) container for Python.
|
|
5
5
|
Author-email: David Perez Cabrera <dperezcabrera@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -45,12 +45,6 @@ Description-Content-Type: text/markdown
|
|
|
45
45
|
License-File: LICENSE
|
|
46
46
|
Dynamic: license-file
|
|
47
47
|
|
|
48
|
-
Got it ✅
|
|
49
|
-
Here’s your **updated README.md in full English**, keeping all original sections but now including the **name-first resolution** feature and new tests section.
|
|
50
|
-
|
|
51
|
-
---
|
|
52
|
-
|
|
53
|
-
````markdown
|
|
54
48
|
# 📦 Pico-IoC: A Minimalist IoC Container for Python
|
|
55
49
|
|
|
56
50
|
[](https://pypi.org/project/pico-ioc/)
|
|
@@ -162,10 +156,10 @@ print(COUNTER["value"]) # 1
|
|
|
162
156
|
|
|
163
157
|
Starting with **v0.5.0**, Pico-IoC enforces **name-first resolution**:
|
|
164
158
|
|
|
165
|
-
1. **Parameter name** (highest priority)
|
|
166
|
-
2. **Exact type annotation**
|
|
167
|
-
3. **MRO fallback** (walk base classes)
|
|
168
|
-
4. **String(name)**
|
|
159
|
+
1. **Parameter name** (highest priority)
|
|
160
|
+
2. **Exact type annotation**
|
|
161
|
+
3. **MRO fallback** (walk base classes)
|
|
162
|
+
4. **String(name)**
|
|
169
163
|
|
|
170
164
|
This means that if a dependency could match both by name and type, **the name match wins**.
|
|
171
165
|
|
|
@@ -191,6 +185,18 @@ class NameVsTypeFactory:
|
|
|
191
185
|
container = init(__name__)
|
|
192
186
|
assert container.get("choose") == "by-name"
|
|
193
187
|
```
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## 📝 Notes on Annotations (PEP 563)
|
|
191
|
+
|
|
192
|
+
Pico-IoC fully supports **postponed evaluation of annotations**
|
|
193
|
+
(`from __future__ import annotations`, a.k.a. **PEP 563**) in Python 3.8–3.10.
|
|
194
|
+
|
|
195
|
+
* Type hints are evaluated with `typing.get_type_hints` and safely resolved.
|
|
196
|
+
* Missing dependencies always raise a **`NameError`**, never a `TypeError`.
|
|
197
|
+
* Behavior is consistent across Python 3.8+ and Python 3.11+ (where PEP 563 is no longer default).
|
|
198
|
+
|
|
199
|
+
This means you can freely use either direct type hints or string-based annotations in your components and factories, without breaking dependency injection.
|
|
194
200
|
|
|
195
201
|
---
|
|
196
202
|
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
Got it ✅
|
|
2
|
-
Here’s your **updated README.md in full English**, keeping all original sections but now including the **name-first resolution** feature and new tests section.
|
|
3
|
-
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
````markdown
|
|
7
1
|
# 📦 Pico-IoC: A Minimalist IoC Container for Python
|
|
8
2
|
|
|
9
3
|
[](https://pypi.org/project/pico-ioc/)
|
|
@@ -115,10 +109,10 @@ print(COUNTER["value"]) # 1
|
|
|
115
109
|
|
|
116
110
|
Starting with **v0.5.0**, Pico-IoC enforces **name-first resolution**:
|
|
117
111
|
|
|
118
|
-
1. **Parameter name** (highest priority)
|
|
119
|
-
2. **Exact type annotation**
|
|
120
|
-
3. **MRO fallback** (walk base classes)
|
|
121
|
-
4. **String(name)**
|
|
112
|
+
1. **Parameter name** (highest priority)
|
|
113
|
+
2. **Exact type annotation**
|
|
114
|
+
3. **MRO fallback** (walk base classes)
|
|
115
|
+
4. **String(name)**
|
|
122
116
|
|
|
123
117
|
This means that if a dependency could match both by name and type, **the name match wins**.
|
|
124
118
|
|
|
@@ -144,6 +138,18 @@ class NameVsTypeFactory:
|
|
|
144
138
|
container = init(__name__)
|
|
145
139
|
assert container.get("choose") == "by-name"
|
|
146
140
|
```
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## 📝 Notes on Annotations (PEP 563)
|
|
144
|
+
|
|
145
|
+
Pico-IoC fully supports **postponed evaluation of annotations**
|
|
146
|
+
(`from __future__ import annotations`, a.k.a. **PEP 563**) in Python 3.8–3.10.
|
|
147
|
+
|
|
148
|
+
* Type hints are evaluated with `typing.get_type_hints` and safely resolved.
|
|
149
|
+
* Missing dependencies always raise a **`NameError`**, never a `TypeError`.
|
|
150
|
+
* Behavior is consistent across Python 3.8+ and Python 3.11+ (where PEP 563 is no longer default).
|
|
151
|
+
|
|
152
|
+
This means you can freely use either direct type hints or string-based annotations in your components and factories, without breaking dependency injection.
|
|
147
153
|
|
|
148
154
|
---
|
|
149
155
|
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# Pico-IoC Architecture & LLM Guide
|
|
2
|
+
|
|
3
|
+
## What is Pico-IoC?
|
|
4
|
+
|
|
5
|
+
Pico-IoC is a tiny, zero-dependency Inversion of Control (IoC) container for Python.
|
|
6
|
+
It discovers components via decorators, wires dependencies automatically, and instantiates everything eagerly by default (fail-fast).
|
|
7
|
+
You can opt into lazy creation via a lightweight proxy.
|
|
8
|
+
|
|
9
|
+
Target Python: 3.8+
|
|
10
|
+
Core design goals: minimal API, predictable resolution, easy testing, framework-agnostic.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Core Concepts (Glossary)
|
|
15
|
+
|
|
16
|
+
* **Container (`PicoContainer`)**
|
|
17
|
+
Holds bindings (providers) and singletons. Keys may be classes or strings.
|
|
18
|
+
|
|
19
|
+
* **Binder (`Binder`)**
|
|
20
|
+
A thin façade for plugins to `bind/has/get` during scan & lifecycle hooks.
|
|
21
|
+
|
|
22
|
+
* **Decorators**
|
|
23
|
+
* `@component(cls=None, *, name=None, lazy=False)` → register a class.
|
|
24
|
+
- Key: class type by default, or `name` (string) if provided.
|
|
25
|
+
- `lazy=True` returns a `ComponentProxy` until first real use.
|
|
26
|
+
* `@factory_component` → register a factory class whose methods can provide components.
|
|
27
|
+
* `@provides(key, *, lazy=False)` → mark a factory method as a provider for `key` (string or type).
|
|
28
|
+
|
|
29
|
+
* **Factory DI**
|
|
30
|
+
* Constructor of a `@factory_component` receives DI like regular components.
|
|
31
|
+
* Each `@provides` method can also have DI in its parameters (by name/type).
|
|
32
|
+
* Defaulted params are **optional**: if not bound, the method uses its default.
|
|
33
|
+
|
|
34
|
+
* **Resolution Order**
|
|
35
|
+
1. **parameter name**, 2) type annotation, 3) MRO fallback, 4) `str(name)`.
|
|
36
|
+
|
|
37
|
+
* **ComponentProxy**
|
|
38
|
+
A transparent proxy for lazy components that defers creation until first actual interaction.
|
|
39
|
+
Forwards common dunder methods (e.g., `__len__`, `__getitem__`, `__iter__`, `__bool__`, operators, context managers, etc.).
|
|
40
|
+
|
|
41
|
+
* **Re-entrancy Guard**
|
|
42
|
+
Accessing `container.get(...)` during package scanning raises a clear error (prevents re-entrant use).
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Lifecycle
|
|
47
|
+
|
|
48
|
+
1. **init(root, …)**
|
|
49
|
+
* Scans `root` package (string/module).
|
|
50
|
+
* Auto-excludes the calling module by default (`auto_exclude_caller=True`).
|
|
51
|
+
* Calls plugin hooks (`before_scan`, `visit_class`, `after_scan`, `after_bind`, `before_eager`, `after_ready`).
|
|
52
|
+
* **Binding order**: components first, then factories.
|
|
53
|
+
* **Blueprint**: eagerly instantiate all non-lazy bindings. Errors fail startup.
|
|
54
|
+
|
|
55
|
+
2. **get(key)**
|
|
56
|
+
Returns the singleton instance for `key` (creates if needed).
|
|
57
|
+
For lazy bindings, returns a proxy first.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## How Pico-IoC Resolves Dependencies
|
|
62
|
+
|
|
63
|
+
Given a constructor or provider method parameter `p`:
|
|
64
|
+
|
|
65
|
+
* If a binding exists for the **parameter name**, use it.
|
|
66
|
+
* Else if the **type annotation** is bound, use it.
|
|
67
|
+
* Else try **MRO** (walk base classes) and use the first bound base type.
|
|
68
|
+
* Else if `str(name)` is bound, use that.
|
|
69
|
+
* Else: raise `NameError("No provider found for key: …")`.
|
|
70
|
+
|
|
71
|
+
**Optional defaulted params**: If resolution fails and the param has a default (e.g., `hint: T = None`), the resolver **omits the argument** so Python uses the default.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Quick Recipes
|
|
76
|
+
|
|
77
|
+
### 1) Basic components
|
|
78
|
+
```python
|
|
79
|
+
from pico_ioc import component, init
|
|
80
|
+
|
|
81
|
+
@component
|
|
82
|
+
class Config:
|
|
83
|
+
url = "postgresql://…"
|
|
84
|
+
|
|
85
|
+
@component
|
|
86
|
+
class Repo:
|
|
87
|
+
def __init__(self, config: Config): # type-based DI
|
|
88
|
+
self.url = config.url
|
|
89
|
+
|
|
90
|
+
c = init(__name__)
|
|
91
|
+
repo = c.get(Repo)
|
|
92
|
+
````
|
|
93
|
+
|
|
94
|
+
### 2) Inject by name
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from pico_ioc import component
|
|
98
|
+
|
|
99
|
+
@component(name="fast_model")
|
|
100
|
+
class Model:
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
@component
|
|
104
|
+
class NeedsByName:
|
|
105
|
+
def __init__(self, fast_model): # name-based DI (highest priority)
|
|
106
|
+
self.m = fast_model
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 3) Factory with lazy provider and provider-param DI
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from pico_ioc import factory_component, provides, component
|
|
113
|
+
|
|
114
|
+
@component
|
|
115
|
+
class Counter:
|
|
116
|
+
def __init__(self): self.value = 0
|
|
117
|
+
|
|
118
|
+
@factory_component
|
|
119
|
+
class Factory:
|
|
120
|
+
@provides("dataset", lazy=True)
|
|
121
|
+
def make_dataset(self, counter: Counter): # DI into provider method
|
|
122
|
+
return list(range(counter.value, counter.value + 3))
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### 4) Static/class methods as providers
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
@factory_component
|
|
129
|
+
class F:
|
|
130
|
+
@staticmethod
|
|
131
|
+
@provides("static_result", lazy=True)
|
|
132
|
+
def make_static():
|
|
133
|
+
return "ok"
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
@provides("class_result")
|
|
137
|
+
def make_class(cls):
|
|
138
|
+
return "ok"
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Error Messages & What They Mean
|
|
144
|
+
|
|
145
|
+
* **`NameError: No provider found for key: ...`**
|
|
146
|
+
A dependency couldn’t be resolved. Check the key being looked up:
|
|
147
|
+
|
|
148
|
+
* For name-based DI: ensure a binding with that exact name exists (`@component(name=...)` or `@provides("name")`).
|
|
149
|
+
* For type-based DI: ensure that class (or a base in its MRO) is provided.
|
|
150
|
+
|
|
151
|
+
* **`RuntimeError: pico-ioc: re-entrant container access during scan.`**
|
|
152
|
+
Something called `get()` while scanning modules (e.g., at import time).
|
|
153
|
+
Move that call out of module top-level or mark the dependent thing `lazy=True` and defer access.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Plugins & Extensions
|
|
158
|
+
|
|
159
|
+
* Implement any subset of:
|
|
160
|
+
|
|
161
|
+
* `before_scan(package, binder)`
|
|
162
|
+
* `visit_class(module, cls, binder)`
|
|
163
|
+
* `after_scan(package, binder)`
|
|
164
|
+
* `after_bind(container, binder)`
|
|
165
|
+
* `before_eager(container, binder)`
|
|
166
|
+
* `after_ready(container, binder)`
|
|
167
|
+
|
|
168
|
+
* The `Binder` lets you register extra bindings, e.g.:
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
class MarkerPlugin:
|
|
172
|
+
def visit_class(self, module, cls, binder):
|
|
173
|
+
if cls.__name__ == "SpecialService" and not binder.has("marker"):
|
|
174
|
+
binder.bind("marker", lambda: {"ok": True}, lazy=False)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
* Public helpers (also useful for plugins):
|
|
178
|
+
|
|
179
|
+
* `create_instance(cls, container)` – construct with DI & precedence rules.
|
|
180
|
+
* `resolve_param(container, inspect.Parameter)` – resolve a single param.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Internals (Stable Behaviors)
|
|
185
|
+
|
|
186
|
+
* **Singletons**: All components/providers default to singletons (first creation cached).
|
|
187
|
+
* **Binding Order**: Components are bound **before** factories so factory constructors can be injected.
|
|
188
|
+
* **Eager by default**: `lazy=False` (default) is instantiated at the end of `init()`.
|
|
189
|
+
* **Lazy**: `lazy=True` yields a `ComponentProxy`; realization occurs on first real interaction.
|
|
190
|
+
* **Provider DI**: Provider methods can declare DI params; defaults make them optional.
|
|
191
|
+
* **Thread safety**: Scanning and resolution use `ContextVar`, isolating state per thread/async task.
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Troubleshooting Playbook (for an LLM)
|
|
196
|
+
|
|
197
|
+
When a user asks for help:
|
|
198
|
+
|
|
199
|
+
1. **Identify binding key**
|
|
200
|
+
|
|
201
|
+
* If they mention a quoted name, assume **string key**.
|
|
202
|
+
* If they mention a class, assume **type key**.
|
|
203
|
+
|
|
204
|
+
2. **Check resolution path**
|
|
205
|
+
|
|
206
|
+
* Name → Type → MRO → `str(name)`.
|
|
207
|
+
|
|
208
|
+
3. **Suggest fixes**
|
|
209
|
+
|
|
210
|
+
* For missing name: `@component(name="…")` or a `@provides("…")`.
|
|
211
|
+
* For type: ensure class is scanned or provided by a factory.
|
|
212
|
+
* For optional hint (`param: T = None`): remind that defaults are optional.
|
|
213
|
+
|
|
214
|
+
4. **Re-entrancy**
|
|
215
|
+
|
|
216
|
+
* If errors mention “re-entrant during scan”, move `get()` out of import time.
|
|
217
|
+
|
|
218
|
+
5. **Factories**
|
|
219
|
+
|
|
220
|
+
* Factory constructor and provider params both support DI.
|
|
221
|
+
* Lazy provider builds only on first use; eager builds at `init()`.
|
|
222
|
+
|
|
223
|
+
6. **Testing advice**
|
|
224
|
+
|
|
225
|
+
* For missing dep scenarios, mark component/provider `lazy=True` and force realization in the test.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Anti-Patterns to Avoid
|
|
230
|
+
|
|
231
|
+
* Calling `container.get(...)` at module import time in scanned packages.
|
|
232
|
+
* Expecting type-based injection when a **name** binding exists (name wins).
|
|
233
|
+
* Providers that create new instances each call (Pico-IoC expects singletons; use callables if you need factories).
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Example: Minimal App Layout
|
|
238
|
+
|
|
239
|
+
```
|
|
240
|
+
myapp/
|
|
241
|
+
__init__.py
|
|
242
|
+
services/
|
|
243
|
+
__init__.py
|
|
244
|
+
components.py # @component classes
|
|
245
|
+
factories.py # @factory_component classes + @provides methods
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
# bootstrap.py
|
|
250
|
+
import myapp
|
|
251
|
+
from pico_ioc import init
|
|
252
|
+
|
|
253
|
+
container = init(myapp)
|
|
254
|
+
svc = container.get("heavy_service")
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## FAQ (short)
|
|
260
|
+
|
|
261
|
+
* **Can I bind primitives or dicts?** Yes, via `@provides("name")` returning e.g. a dict.
|
|
262
|
+
* **Thread safety?** Yes. Singletons are created once per container. ContextVar ensures safe state isolation.
|
|
263
|
+
* **How do I exclude modules from scanning?** Use `init(root, exclude=callable)`.
|
|
264
|
+
* **Can I scan by module string?** Yes: `init("myapp.services")`.
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Version Notes Relevant to LLM Answers
|
|
269
|
+
|
|
270
|
+
* **v0.6.0**: `ComponentProxy` made fully transparent — supports operators, context managers, and more dunder methods.
|
|
271
|
+
* **v0.5.2**: Name-first resolution; defaulted params in constructors and provider methods are optional.
|
|
272
|
+
* Earlier versions: basic DI without name-first precedence.
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## How to “Think” When Explaining Usage (LLM prompting tips)
|
|
277
|
+
|
|
278
|
+
* Be explicit about the **binding key** (type vs string).
|
|
279
|
+
* Map constructor/provider params to resolution rules.
|
|
280
|
+
* Show tiny, runnable snippets (no external deps).
|
|
281
|
+
* Prefer **lazy** for failure demos; prefer **eager** for production.
|
|
282
|
+
* If user hits `NameError`, propose concrete fixes (name, type, factory).
|
|
283
|
+
* Don’t promise async/background work — stick to code-level fixes.
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
End of document.
|
|
288
|
+
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# Usage Patterns for pico-ioc (LLM Guidance)
|
|
2
|
+
|
|
3
|
+
This document provides examples of typical questions and answers an LLM should handle when assisting developers with **pico-ioc**.
|
|
4
|
+
The goal is to give consistent, concise, and technically accurate responses.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 1. General Questions
|
|
9
|
+
|
|
10
|
+
**Q:** What is pico-ioc?
|
|
11
|
+
**A:** pico-ioc is a lightweight Inversion of Control (IoC) and Dependency Injection (DI) container for Python.
|
|
12
|
+
It uses decorators (`@component`, `@factory_component`, `@provides`) to register dependencies and automatically resolve them at runtime.
|
|
13
|
+
It is inspired by frameworks like Spring (Java) but designed to be minimal, fast, and Pythonic.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 2. How to Start
|
|
18
|
+
|
|
19
|
+
**Q:** How do I initialize pico-ioc in my project?
|
|
20
|
+
**A:**
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
import pico_ioc
|
|
24
|
+
import myapp
|
|
25
|
+
|
|
26
|
+
container = pico_ioc.init(myapp)
|
|
27
|
+
service = container.get(MyService)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
This scans the `myapp` package for components and wires them automatically.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 3. Registering Components
|
|
35
|
+
|
|
36
|
+
**Q:** How do I register a class as a component?
|
|
37
|
+
**A:**
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from pico_ioc import component
|
|
41
|
+
|
|
42
|
+
@component
|
|
43
|
+
class MyService:
|
|
44
|
+
def greet(self):
|
|
45
|
+
return "Hello from MyService!"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## 4. Using Factories
|
|
51
|
+
|
|
52
|
+
**Q:** How do I create instances using a factory?
|
|
53
|
+
**A:**
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from pico_ioc import factory_component, provides
|
|
57
|
+
|
|
58
|
+
class MyService:
|
|
59
|
+
def greet(self):
|
|
60
|
+
return "Hello!"
|
|
61
|
+
|
|
62
|
+
@factory_component
|
|
63
|
+
class MyFactory:
|
|
64
|
+
@provides("my_service")
|
|
65
|
+
def build_service(self) -> MyService:
|
|
66
|
+
return MyService()
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Factories let you encapsulate creation logic. Each `@provides` method binds a key to the produced object.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 5. Providing Interfaces
|
|
74
|
+
|
|
75
|
+
**Q:** How do I bind an interface to an implementation?
|
|
76
|
+
**A:**
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from pico_ioc import provides
|
|
80
|
+
|
|
81
|
+
class Storage:
|
|
82
|
+
...
|
|
83
|
+
|
|
84
|
+
class FileStorage(Storage):
|
|
85
|
+
...
|
|
86
|
+
|
|
87
|
+
@provides(Storage)
|
|
88
|
+
def provide_storage() -> Storage:
|
|
89
|
+
return FileStorage()
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Now any class that requires `Storage` will receive a `FileStorage`.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 6. Resolving Dependencies
|
|
97
|
+
|
|
98
|
+
**Q:** How are constructor parameters injected?
|
|
99
|
+
**A:** pico-ioc inspects type hints in constructors and provides the required dependencies.
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
@component
|
|
103
|
+
class UserService:
|
|
104
|
+
def __init__(self, storage: Storage):
|
|
105
|
+
self.storage = storage
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
When `UserService` is requested, pico-ioc automatically injects a `Storage`.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 7. Thread Safety
|
|
113
|
+
|
|
114
|
+
**Q:** Is pico-ioc thread-safe?
|
|
115
|
+
**A:** Yes. Dependency resolution and scanning states are tracked using `ContextVar`, which isolates these flags per thread and per async task. This ensures safe operation in multithreaded and asynchronous environments without shared-state conflicts.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 8. Lazy vs. Eager Components
|
|
120
|
+
|
|
121
|
+
By default, bindings are **eager** (instantiated immediately).
|
|
122
|
+
If `lazy=True` is passed, pico-ioc returns a `ComponentProxy` instead. The proxy defers creation until the first real use.
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from pico_ioc import component
|
|
126
|
+
|
|
127
|
+
@component(lazy=True)
|
|
128
|
+
class HeavyService:
|
|
129
|
+
def __init__(self):
|
|
130
|
+
print("Expensive init...")
|
|
131
|
+
self.value = 42
|
|
132
|
+
|
|
133
|
+
container = pico_ioc.init(__name__)
|
|
134
|
+
svc = container.get(HeavyService) # not yet created
|
|
135
|
+
print(svc.value) # triggers creation
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Note (since v0.6.0):** `ComponentProxy` fully forwards operators, attribute access, context manager protocols, etc., making lazy components behave transparently like the real object.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## 9. Best Practices
|
|
143
|
+
|
|
144
|
+
* Always use type hints so dependencies can be resolved correctly.
|
|
145
|
+
* Prefer `@component` for classes and `@provides` for interfaces.
|
|
146
|
+
* Use factories (`@factory_component` + `@provides`) for advanced creation logic.
|
|
147
|
+
* Avoid global state; let the container manage lifecycle.
|
|
148
|
+
* Use `init(package)` at the root of your application to wire everything automatically.
|
|
149
|
+
* Consider `lazy=True` for heavy or rarely used services.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## 10. Example Flow
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
import pico_ioc
|
|
157
|
+
from pico_ioc import component
|
|
158
|
+
|
|
159
|
+
@component
|
|
160
|
+
class Repo:
|
|
161
|
+
def fetch(self):
|
|
162
|
+
return "data"
|
|
163
|
+
|
|
164
|
+
@component
|
|
165
|
+
class Service:
|
|
166
|
+
def __init__(self, repo: Repo):
|
|
167
|
+
self.repo = repo
|
|
168
|
+
|
|
169
|
+
def run(self):
|
|
170
|
+
return f"Service using {self.repo.fetch()}"
|
|
171
|
+
|
|
172
|
+
# Bootstrap
|
|
173
|
+
import myapp
|
|
174
|
+
container = pico_ioc.init(myapp)
|
|
175
|
+
svc = container.get(Service)
|
|
176
|
+
print(svc.run())
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Expected output:**
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
Service using data
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## 11. Troubleshooting
|
|
188
|
+
|
|
189
|
+
* **Error:** "Cannot resolve parameter" → Add proper type hints.
|
|
190
|
+
* **Error:** "No provider found" → Ensure the class/function is decorated with `@component` or `@provides`.
|
|
191
|
+
* **Unexpected singleton reuse** → Check if you intended a new instance vs. a shared one.
|
|
192
|
+
* **Lazy service not instantiated** → Remember: `lazy=True` returns a proxy, actual creation happens only on first real usage.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
✅ With these usage patterns, an LLM should be able to explain **how pico-ioc works**, provide **practical examples**, and help users debug typical problems.
|
|
197
|
+
**New in v0.6.0:** transparent `ComponentProxy` support for all Python protocols, making lazy components nearly indistinguishable from eager ones.
|
|
198
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from ._version import __version__
|
|
2
|
+
from .container import PicoContainer, Binder
|
|
3
|
+
from .decorators import component, factory_component, provides
|
|
4
|
+
from .plugins import PicoPlugin
|
|
5
|
+
from .resolver import Resolver
|
|
6
|
+
from .api import init, reset
|
|
7
|
+
from .proxy import ComponentProxy
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
from ._version import __version__
|
|
11
|
+
except Exception:
|
|
12
|
+
__version__ = "0.0.0"
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"__version__",
|
|
16
|
+
"PicoContainer",
|
|
17
|
+
"Binder",
|
|
18
|
+
"PicoPlugin",
|
|
19
|
+
"init",
|
|
20
|
+
"reset",
|
|
21
|
+
"component",
|
|
22
|
+
"factory_component",
|
|
23
|
+
"provides",
|
|
24
|
+
"resolve_param",
|
|
25
|
+
"create_instance",
|
|
26
|
+
]
|
|
27
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '0.6.0'
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Callable, Optional, Tuple
|
|
4
|
+
from .container import PicoContainer, Binder
|
|
5
|
+
from .plugins import PicoPlugin
|
|
6
|
+
from .scanner import scan_and_configure
|
|
7
|
+
from . import _state
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def reset() -> None:
|
|
11
|
+
_state._container = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def init(
|
|
15
|
+
root_package,
|
|
16
|
+
*,
|
|
17
|
+
exclude: Optional[Callable[[str], bool]] = None,
|
|
18
|
+
auto_exclude_caller: bool = True,
|
|
19
|
+
plugins: Tuple[PicoPlugin, ...] = (),
|
|
20
|
+
reuse: bool = True,
|
|
21
|
+
) -> PicoContainer:
|
|
22
|
+
if reuse and _state._container:
|
|
23
|
+
return _state._container
|
|
24
|
+
|
|
25
|
+
combined_exclude = exclude
|
|
26
|
+
if auto_exclude_caller:
|
|
27
|
+
try:
|
|
28
|
+
caller_frame = inspect.stack()[1].frame
|
|
29
|
+
caller_module = inspect.getmodule(caller_frame)
|
|
30
|
+
caller_name = getattr(caller_module, "__name__", None)
|
|
31
|
+
except Exception:
|
|
32
|
+
caller_name = None
|
|
33
|
+
if caller_name:
|
|
34
|
+
if combined_exclude is None:
|
|
35
|
+
def combined_exclude(mod: str, _caller=caller_name):
|
|
36
|
+
return mod == _caller
|
|
37
|
+
else:
|
|
38
|
+
prev = combined_exclude
|
|
39
|
+
def combined_exclude(mod: str, _caller=caller_name, _prev=prev):
|
|
40
|
+
return mod == _caller or _prev(mod)
|
|
41
|
+
|
|
42
|
+
container = PicoContainer()
|
|
43
|
+
binder = Binder(container)
|
|
44
|
+
logging.info("Initializing pico-ioc...")
|
|
45
|
+
|
|
46
|
+
tok = _state._scanning.set(True)
|
|
47
|
+
try:
|
|
48
|
+
scan_and_configure(root_package, container, exclude=combined_exclude, plugins=plugins)
|
|
49
|
+
finally:
|
|
50
|
+
_state._scanning.reset(tok)
|
|
51
|
+
|
|
52
|
+
for pl in plugins:
|
|
53
|
+
try:
|
|
54
|
+
getattr(pl, "after_bind", lambda *a, **k: None)(container, binder)
|
|
55
|
+
except Exception:
|
|
56
|
+
logging.exception("Plugin after_bind failed")
|
|
57
|
+
for pl in plugins:
|
|
58
|
+
try:
|
|
59
|
+
getattr(pl, "before_eager", lambda *a, **k: None)(container, binder)
|
|
60
|
+
except Exception:
|
|
61
|
+
logging.exception("Plugin before_eager failed")
|
|
62
|
+
|
|
63
|
+
container.eager_instantiate_all()
|
|
64
|
+
|
|
65
|
+
for pl in plugins:
|
|
66
|
+
try:
|
|
67
|
+
getattr(pl, "after_ready", lambda *a, **k: None)(container, binder)
|
|
68
|
+
except Exception:
|
|
69
|
+
logging.exception("Plugin after_ready failed")
|
|
70
|
+
|
|
71
|
+
logging.info("Container configured and ready.")
|
|
72
|
+
_state._container = container
|
|
73
|
+
return container
|
|
74
|
+
|