pico-ioc 0.1.1__tar.gz → 0.2.1__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.1.1 → pico_ioc-0.2.1}/PKG-INFO +58 -50
- {pico_ioc-0.1.1 → pico_ioc-0.2.1}/README.md +57 -49
- {pico_ioc-0.1.1 → pico_ioc-0.2.1}/src/pico_ioc/__init__.py +130 -25
- pico_ioc-0.2.1/src/pico_ioc/_version.py +1 -0
- {pico_ioc-0.1.1 → pico_ioc-0.2.1}/src/pico_ioc.egg-info/PKG-INFO +58 -50
- pico_ioc-0.2.1/tests/test_pico_ioc.py +287 -0
- pico_ioc-0.1.1/src/pico_ioc/_version.py +0 -1
- pico_ioc-0.1.1/tests/test_pico_ioc.py +0 -178
- {pico_ioc-0.1.1 → pico_ioc-0.2.1}/.github/workflows/ci.yml +0 -0
- {pico_ioc-0.1.1 → pico_ioc-0.2.1}/.github/workflows/publish-to-pypi.yml +0 -0
- {pico_ioc-0.1.1 → pico_ioc-0.2.1}/Dockerfile.test +0 -0
- {pico_ioc-0.1.1 → pico_ioc-0.2.1}/Makefile +0 -0
- {pico_ioc-0.1.1 → pico_ioc-0.2.1}/pyproject.toml +0 -0
- {pico_ioc-0.1.1 → pico_ioc-0.2.1}/setup.cfg +0 -0
- {pico_ioc-0.1.1 → pico_ioc-0.2.1}/src/pico_ioc.egg-info/SOURCES.txt +0 -0
- {pico_ioc-0.1.1 → pico_ioc-0.2.1}/src/pico_ioc.egg-info/dependency_links.txt +0 -0
- {pico_ioc-0.1.1 → pico_ioc-0.2.1}/src/pico_ioc.egg-info/top_level.txt +0 -0
- {pico_ioc-0.1.1 → pico_ioc-0.2.1}/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.2.1
|
|
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
|
Project-URL: Homepage, https://github.com/dperezcabrera/pico-ioc
|
|
@@ -21,32 +21,35 @@ Classifier: Operating System :: OS Independent
|
|
|
21
21
|
Requires-Python: >=3.8
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
|
|
25
|
+
# 📦 Pico-IoC: A Minimalist IoC Container for Python
|
|
25
26
|
|
|
26
27
|
[](https://pypi.org/project/pico-ioc/)
|
|
27
28
|
[](https://opensource.org/licenses/MIT)
|
|
28
29
|

|
|
29
30
|
|
|
30
|
-
**Pico-IoC** is a tiny, zero-dependency, decorator-based Inversion of Control (IoC) container for Python.
|
|
31
|
+
**Pico-IoC** is a tiny, zero-dependency, decorator-based Inversion of Control (IoC) container for Python.
|
|
31
32
|
It helps you manage dependencies in a clean, intuitive, and *Pythonic* way.
|
|
32
33
|
|
|
33
|
-
The core idea is to let you build loosely coupled, easily testable applications without manually wiring components.
|
|
34
|
+
The core idea is to let you build loosely coupled, easily testable applications without manually wiring components.
|
|
34
35
|
*Inspired by the IoC philosophy popularized by the Spring Framework.*
|
|
35
36
|
|
|
36
37
|
---
|
|
37
38
|
|
|
38
|
-
## Key Features
|
|
39
|
+
## ✨ Key Features
|
|
39
40
|
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
41
|
+
* **Zero Dependencies:** Pure Python, no external libraries.
|
|
42
|
+
* **Decorator-Based API:** Simple decorators like `@component` and `@provides`.
|
|
43
|
+
* **Automatic Discovery:** Scans your package to auto-register components.
|
|
44
|
+
* **Lazy Instantiation:** Objects are created on first use.
|
|
45
|
+
* **Flexible Factories:** Encapsulate complex creation logic.
|
|
46
|
+
* **Framework-Agnostic:** Works with Flask, FastAPI, CLIs, scripts, etc.
|
|
47
|
+
* **Smart Dependency Resolution:** Resolves by **parameter name**, then **type annotation**, then **MRO fallback**.
|
|
48
|
+
* **Auto-Exclude Caller:** `init()` automatically skips the calling module to avoid double-initialization during scans.
|
|
46
49
|
|
|
47
50
|
---
|
|
48
51
|
|
|
49
|
-
## Installation
|
|
52
|
+
## 📦 Installation
|
|
50
53
|
|
|
51
54
|
```bash
|
|
52
55
|
pip install pico-ioc
|
|
@@ -54,9 +57,7 @@ pip install pico-ioc
|
|
|
54
57
|
|
|
55
58
|
---
|
|
56
59
|
|
|
57
|
-
## Quick Start
|
|
58
|
-
|
|
59
|
-
Getting started is simple. Decorate your classes and let Pico-IoC wire them up.
|
|
60
|
+
## 🚀 Quick Start
|
|
60
61
|
|
|
61
62
|
```python
|
|
62
63
|
from pico_ioc import component, init
|
|
@@ -83,21 +84,22 @@ print(db.get_data()) # Data from postgresql://user:pass@host/db
|
|
|
83
84
|
|
|
84
85
|
---
|
|
85
86
|
|
|
86
|
-
##
|
|
87
|
+
## 🧩 Custom Component Keys
|
|
87
88
|
|
|
88
|
-
|
|
89
|
+
You can register a component with a **custom key** (string, class, enum…).
|
|
90
|
+
`key=` is the preferred syntax. For backwards compatibility, `name=` still works.
|
|
89
91
|
|
|
90
92
|
```python
|
|
91
93
|
from pico_ioc import component, init
|
|
92
94
|
|
|
93
|
-
@component(name="config")
|
|
95
|
+
@component(name="config") # still supported for legacy code
|
|
94
96
|
class AppConfig:
|
|
95
97
|
def __init__(self):
|
|
96
98
|
self.db_url = "postgresql://user:pass@localhost/db"
|
|
97
99
|
|
|
98
100
|
@component
|
|
99
101
|
class Repository:
|
|
100
|
-
def __init__(self, config: "config"): #
|
|
102
|
+
def __init__(self, config: "config"): # resolve by name
|
|
101
103
|
self._url = config.db_url
|
|
102
104
|
|
|
103
105
|
container = init(__name__)
|
|
@@ -106,9 +108,12 @@ print(repo._url) # postgresql://user:pass@localhost/db
|
|
|
106
108
|
print(container.get("config").db_url)
|
|
107
109
|
```
|
|
108
110
|
|
|
109
|
-
|
|
111
|
+
---
|
|
110
112
|
|
|
111
|
-
|
|
113
|
+
## 🏭 Factory Components and `@provides`
|
|
114
|
+
|
|
115
|
+
Factories can provide components under a specific **key**.
|
|
116
|
+
Default is lazy creation (via `LazyProxy`).
|
|
112
117
|
|
|
113
118
|
```python
|
|
114
119
|
from pico_ioc import factory_component, provides, init
|
|
@@ -117,7 +122,7 @@ CREATION_COUNTER = {"value": 0}
|
|
|
117
122
|
|
|
118
123
|
@factory_component
|
|
119
124
|
class ServicesFactory:
|
|
120
|
-
@provides(
|
|
125
|
+
@provides(key="heavy_service") # preferred
|
|
121
126
|
def make_heavy(self):
|
|
122
127
|
CREATION_COUNTER["value"] += 1
|
|
123
128
|
return {"payload": "Hello from heavy service"}
|
|
@@ -130,7 +135,9 @@ print(svc["payload"]) # triggers creation
|
|
|
130
135
|
print(CREATION_COUNTER["value"]) # 1
|
|
131
136
|
```
|
|
132
137
|
|
|
133
|
-
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## 📦 Project-Style Scanning
|
|
134
141
|
|
|
135
142
|
```
|
|
136
143
|
project_root/
|
|
@@ -165,61 +172,62 @@ class ApiClient:
|
|
|
165
172
|
import pico_ioc
|
|
166
173
|
from myapp.services import ApiClient
|
|
167
174
|
|
|
168
|
-
# Scan the whole 'myapp' package
|
|
169
175
|
container = pico_ioc.init("myapp")
|
|
170
|
-
|
|
171
176
|
client = container.get(ApiClient)
|
|
172
177
|
print(client.get("status")) # GET https://api.example.com/status
|
|
173
178
|
```
|
|
174
179
|
|
|
175
180
|
---
|
|
176
181
|
|
|
177
|
-
##
|
|
182
|
+
## 🧠 Dependency Resolution Order
|
|
178
183
|
|
|
179
|
-
|
|
184
|
+
When Pico-IoC instantiates a component, it tries to resolve each parameter in this order:
|
|
180
185
|
|
|
181
|
-
|
|
186
|
+
1. **Exact parameter name** (string key in container)
|
|
187
|
+
2. **Exact type annotation** (class key in container)
|
|
188
|
+
3. **MRO fallback** (walk base classes until match)
|
|
189
|
+
4. **String version** of the parameter name
|
|
182
190
|
|
|
183
|
-
|
|
191
|
+
---
|
|
184
192
|
|
|
185
|
-
|
|
193
|
+
## 🛠 API Reference
|
|
186
194
|
|
|
187
|
-
###
|
|
195
|
+
### `init(root_package_or_module, *, exclude=None, auto_exclude_caller=True) -> PicoContainer`
|
|
188
196
|
|
|
189
|
-
|
|
197
|
+
Scan the given root **package** (str) or **module**.
|
|
198
|
+
By default, excludes the calling module.
|
|
190
199
|
|
|
191
|
-
### `@
|
|
200
|
+
### `@component(cls=None, *, name=None)`
|
|
192
201
|
|
|
193
|
-
|
|
202
|
+
Register a class as a component.
|
|
203
|
+
If `name` is given, registers under that string; otherwise under the class type.
|
|
194
204
|
|
|
195
|
-
|
|
205
|
+
### `@factory_component`
|
|
196
206
|
|
|
197
|
-
|
|
207
|
+
Register a class as a factory of components.
|
|
198
208
|
|
|
199
|
-
|
|
209
|
+
### `@provides(key=None, *, name=None, lazy=True)`
|
|
200
210
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
tox # run all configured envs
|
|
205
|
-
```
|
|
211
|
+
Declare that a factory method provides a component under `key`.
|
|
212
|
+
`name` is accepted for backwards compatibility.
|
|
213
|
+
If `lazy=True`, returns a `LazyProxy` that instantiates on first real use.
|
|
206
214
|
|
|
207
215
|
---
|
|
208
216
|
|
|
209
|
-
##
|
|
217
|
+
## 🧪 Testing
|
|
210
218
|
|
|
211
|
-
|
|
219
|
+
```bash
|
|
220
|
+
pip install tox
|
|
221
|
+
tox -e py311
|
|
222
|
+
```
|
|
212
223
|
|
|
213
224
|
---
|
|
214
225
|
|
|
215
|
-
## License
|
|
226
|
+
## 📜 License
|
|
216
227
|
|
|
217
|
-
MIT — see
|
|
228
|
+
MIT — see [LICENSE](https://opensource.org/licenses/MIT)
|
|
218
229
|
|
|
219
230
|
---
|
|
220
231
|
|
|
221
|
-
|
|
232
|
+
¿Quieres que también te prepare **un ejemplo completo en el README** con `fast_model` y `BaseChatModel` para que quede documentado el nuevo orden de resolución? Así quedaría clarísimo para cualquiera que lo use.
|
|
222
233
|
|
|
223
|
-
* **David Perez Cabrera**
|
|
224
|
-
* **Gemini 2.5-Pro**
|
|
225
|
-
* **GPT-5**
|
|
@@ -1,29 +1,32 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
|
+
# 📦 Pico-IoC: A Minimalist IoC Container for Python
|
|
2
3
|
|
|
3
4
|
[](https://pypi.org/project/pico-ioc/)
|
|
4
5
|
[](https://opensource.org/licenses/MIT)
|
|
5
6
|

|
|
6
7
|
|
|
7
|
-
**Pico-IoC** is a tiny, zero-dependency, decorator-based Inversion of Control (IoC) container for Python.
|
|
8
|
+
**Pico-IoC** is a tiny, zero-dependency, decorator-based Inversion of Control (IoC) container for Python.
|
|
8
9
|
It helps you manage dependencies in a clean, intuitive, and *Pythonic* way.
|
|
9
10
|
|
|
10
|
-
The core idea is to let you build loosely coupled, easily testable applications without manually wiring components.
|
|
11
|
+
The core idea is to let you build loosely coupled, easily testable applications without manually wiring components.
|
|
11
12
|
*Inspired by the IoC philosophy popularized by the Spring Framework.*
|
|
12
13
|
|
|
13
14
|
---
|
|
14
15
|
|
|
15
|
-
## Key Features
|
|
16
|
+
## ✨ Key Features
|
|
16
17
|
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
18
|
+
* **Zero Dependencies:** Pure Python, no external libraries.
|
|
19
|
+
* **Decorator-Based API:** Simple decorators like `@component` and `@provides`.
|
|
20
|
+
* **Automatic Discovery:** Scans your package to auto-register components.
|
|
21
|
+
* **Lazy Instantiation:** Objects are created on first use.
|
|
22
|
+
* **Flexible Factories:** Encapsulate complex creation logic.
|
|
23
|
+
* **Framework-Agnostic:** Works with Flask, FastAPI, CLIs, scripts, etc.
|
|
24
|
+
* **Smart Dependency Resolution:** Resolves by **parameter name**, then **type annotation**, then **MRO fallback**.
|
|
25
|
+
* **Auto-Exclude Caller:** `init()` automatically skips the calling module to avoid double-initialization during scans.
|
|
23
26
|
|
|
24
27
|
---
|
|
25
28
|
|
|
26
|
-
## Installation
|
|
29
|
+
## 📦 Installation
|
|
27
30
|
|
|
28
31
|
```bash
|
|
29
32
|
pip install pico-ioc
|
|
@@ -31,9 +34,7 @@ pip install pico-ioc
|
|
|
31
34
|
|
|
32
35
|
---
|
|
33
36
|
|
|
34
|
-
## Quick Start
|
|
35
|
-
|
|
36
|
-
Getting started is simple. Decorate your classes and let Pico-IoC wire them up.
|
|
37
|
+
## 🚀 Quick Start
|
|
37
38
|
|
|
38
39
|
```python
|
|
39
40
|
from pico_ioc import component, init
|
|
@@ -60,21 +61,22 @@ print(db.get_data()) # Data from postgresql://user:pass@host/db
|
|
|
60
61
|
|
|
61
62
|
---
|
|
62
63
|
|
|
63
|
-
##
|
|
64
|
+
## 🧩 Custom Component Keys
|
|
64
65
|
|
|
65
|
-
|
|
66
|
+
You can register a component with a **custom key** (string, class, enum…).
|
|
67
|
+
`key=` is the preferred syntax. For backwards compatibility, `name=` still works.
|
|
66
68
|
|
|
67
69
|
```python
|
|
68
70
|
from pico_ioc import component, init
|
|
69
71
|
|
|
70
|
-
@component(name="config")
|
|
72
|
+
@component(name="config") # still supported for legacy code
|
|
71
73
|
class AppConfig:
|
|
72
74
|
def __init__(self):
|
|
73
75
|
self.db_url = "postgresql://user:pass@localhost/db"
|
|
74
76
|
|
|
75
77
|
@component
|
|
76
78
|
class Repository:
|
|
77
|
-
def __init__(self, config: "config"): #
|
|
79
|
+
def __init__(self, config: "config"): # resolve by name
|
|
78
80
|
self._url = config.db_url
|
|
79
81
|
|
|
80
82
|
container = init(__name__)
|
|
@@ -83,9 +85,12 @@ print(repo._url) # postgresql://user:pass@localhost/db
|
|
|
83
85
|
print(container.get("config").db_url)
|
|
84
86
|
```
|
|
85
87
|
|
|
86
|
-
|
|
88
|
+
---
|
|
87
89
|
|
|
88
|
-
|
|
90
|
+
## 🏭 Factory Components and `@provides`
|
|
91
|
+
|
|
92
|
+
Factories can provide components under a specific **key**.
|
|
93
|
+
Default is lazy creation (via `LazyProxy`).
|
|
89
94
|
|
|
90
95
|
```python
|
|
91
96
|
from pico_ioc import factory_component, provides, init
|
|
@@ -94,7 +99,7 @@ CREATION_COUNTER = {"value": 0}
|
|
|
94
99
|
|
|
95
100
|
@factory_component
|
|
96
101
|
class ServicesFactory:
|
|
97
|
-
@provides(
|
|
102
|
+
@provides(key="heavy_service") # preferred
|
|
98
103
|
def make_heavy(self):
|
|
99
104
|
CREATION_COUNTER["value"] += 1
|
|
100
105
|
return {"payload": "Hello from heavy service"}
|
|
@@ -107,7 +112,9 @@ print(svc["payload"]) # triggers creation
|
|
|
107
112
|
print(CREATION_COUNTER["value"]) # 1
|
|
108
113
|
```
|
|
109
114
|
|
|
110
|
-
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 📦 Project-Style Scanning
|
|
111
118
|
|
|
112
119
|
```
|
|
113
120
|
project_root/
|
|
@@ -142,61 +149,62 @@ class ApiClient:
|
|
|
142
149
|
import pico_ioc
|
|
143
150
|
from myapp.services import ApiClient
|
|
144
151
|
|
|
145
|
-
# Scan the whole 'myapp' package
|
|
146
152
|
container = pico_ioc.init("myapp")
|
|
147
|
-
|
|
148
153
|
client = container.get(ApiClient)
|
|
149
154
|
print(client.get("status")) # GET https://api.example.com/status
|
|
150
155
|
```
|
|
151
156
|
|
|
152
157
|
---
|
|
153
158
|
|
|
154
|
-
##
|
|
159
|
+
## 🧠 Dependency Resolution Order
|
|
155
160
|
|
|
156
|
-
|
|
161
|
+
When Pico-IoC instantiates a component, it tries to resolve each parameter in this order:
|
|
157
162
|
|
|
158
|
-
|
|
163
|
+
1. **Exact parameter name** (string key in container)
|
|
164
|
+
2. **Exact type annotation** (class key in container)
|
|
165
|
+
3. **MRO fallback** (walk base classes until match)
|
|
166
|
+
4. **String version** of the parameter name
|
|
159
167
|
|
|
160
|
-
|
|
168
|
+
---
|
|
161
169
|
|
|
162
|
-
|
|
170
|
+
## 🛠 API Reference
|
|
163
171
|
|
|
164
|
-
###
|
|
172
|
+
### `init(root_package_or_module, *, exclude=None, auto_exclude_caller=True) -> PicoContainer`
|
|
165
173
|
|
|
166
|
-
|
|
174
|
+
Scan the given root **package** (str) or **module**.
|
|
175
|
+
By default, excludes the calling module.
|
|
167
176
|
|
|
168
|
-
### `@
|
|
177
|
+
### `@component(cls=None, *, name=None)`
|
|
169
178
|
|
|
170
|
-
|
|
179
|
+
Register a class as a component.
|
|
180
|
+
If `name` is given, registers under that string; otherwise under the class type.
|
|
171
181
|
|
|
172
|
-
|
|
182
|
+
### `@factory_component`
|
|
173
183
|
|
|
174
|
-
|
|
184
|
+
Register a class as a factory of components.
|
|
175
185
|
|
|
176
|
-
|
|
186
|
+
### `@provides(key=None, *, name=None, lazy=True)`
|
|
177
187
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
tox # run all configured envs
|
|
182
|
-
```
|
|
188
|
+
Declare that a factory method provides a component under `key`.
|
|
189
|
+
`name` is accepted for backwards compatibility.
|
|
190
|
+
If `lazy=True`, returns a `LazyProxy` that instantiates on first real use.
|
|
183
191
|
|
|
184
192
|
---
|
|
185
193
|
|
|
186
|
-
##
|
|
194
|
+
## 🧪 Testing
|
|
187
195
|
|
|
188
|
-
|
|
196
|
+
```bash
|
|
197
|
+
pip install tox
|
|
198
|
+
tox -e py311
|
|
199
|
+
```
|
|
189
200
|
|
|
190
201
|
---
|
|
191
202
|
|
|
192
|
-
## License
|
|
203
|
+
## 📜 License
|
|
193
204
|
|
|
194
|
-
MIT — see
|
|
205
|
+
MIT — see [LICENSE](https://opensource.org/licenses/MIT)
|
|
195
206
|
|
|
196
207
|
---
|
|
197
208
|
|
|
198
|
-
|
|
209
|
+
¿Quieres que también te prepare **un ejemplo completo en el README** con `fast_model` y `BaseChatModel` para que quede documentado el nuevo orden de resolución? Así quedaría clarísimo para cualquiera que lo use.
|
|
199
210
|
|
|
200
|
-
* **David Perez Cabrera**
|
|
201
|
-
* **Gemini 2.5-Pro**
|
|
202
|
-
* **GPT-5**
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
from typing import Callable, Any, Iterator, AsyncIterator
|
|
1
|
+
import functools, inspect, pkgutil, importlib, logging
|
|
2
|
+
from typing import Callable, Any, Optional
|
|
3
|
+
from contextvars import ContextVar
|
|
5
4
|
|
|
6
5
|
try:
|
|
7
6
|
# written at build time by setuptools-scm
|
|
@@ -11,18 +10,40 @@ except Exception: # pragma: no cover
|
|
|
11
10
|
|
|
12
11
|
__all__ = ["__version__"]
|
|
13
12
|
|
|
13
|
+
# ------------------------------------------------------------------------------
|
|
14
|
+
# Re-entrancy guards
|
|
15
|
+
# ------------------------------------------------------------------------------
|
|
16
|
+
# True while init/scan is running. Blocks userland container access during scan.
|
|
17
|
+
_scanning: ContextVar[bool] = ContextVar("pico_scanning", default=False)
|
|
18
|
+
|
|
19
|
+
# True while the container is resolving deps for a component (internal use allowed).
|
|
20
|
+
_resolving: ContextVar[bool] = ContextVar("pico_resolving", default=False)
|
|
21
|
+
|
|
14
22
|
# ==============================================================================
|
|
15
23
|
# --- 1. Container and Chameleon Proxy (Framework-Agnostic) ---
|
|
16
24
|
# ==============================================================================
|
|
17
25
|
class PicoContainer:
|
|
18
26
|
def __init__(self):
|
|
19
|
-
self._providers = {}
|
|
20
|
-
self._singletons = {}
|
|
27
|
+
self._providers: dict[Any, Callable[[], Any]] = {}
|
|
28
|
+
self._singletons: dict[Any, Any] = {}
|
|
21
29
|
|
|
22
30
|
def bind(self, key: Any, provider: Callable[[], Any]):
|
|
23
31
|
self._providers[key] = provider
|
|
24
32
|
|
|
33
|
+
def has(self, key: Any) -> bool:
|
|
34
|
+
return key in self._providers or key in self._singletons
|
|
35
|
+
|
|
25
36
|
def get(self, key: Any) -> Any:
|
|
37
|
+
# Forbid user code calling container.get() while the scanner is importing modules.
|
|
38
|
+
# Allow only internal calls performed during dependency resolution.
|
|
39
|
+
if _scanning.get() and not _resolving.get():
|
|
40
|
+
raise RuntimeError(
|
|
41
|
+
"pico-ioc: re-entrant container access during scan. "
|
|
42
|
+
"Avoid calling init()/get() at import time (e.g., in a module body). "
|
|
43
|
+
"Move resolution to runtime (e.g., under if __name__ == '__main__':) "
|
|
44
|
+
"or delay it until pico-ioc init completes."
|
|
45
|
+
)
|
|
46
|
+
|
|
26
47
|
if key in self._singletons:
|
|
27
48
|
return self._singletons[key]
|
|
28
49
|
if key in self._providers:
|
|
@@ -136,21 +157,63 @@ class LazyProxy:
|
|
|
136
157
|
# ==============================================================================
|
|
137
158
|
# --- 2. The Scanner and `init` Facade ---
|
|
138
159
|
# ==============================================================================
|
|
139
|
-
def
|
|
160
|
+
def _resolve_param(container: PicoContainer, p: inspect.Parameter) -> Any:
|
|
161
|
+
if p.name == 'self' or p.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
162
|
+
raise RuntimeError("Invalid param for resolution")
|
|
163
|
+
|
|
164
|
+
# 1) NAME
|
|
165
|
+
if container.has(p.name):
|
|
166
|
+
return container.get(p.name)
|
|
167
|
+
|
|
168
|
+
ann = p.annotation
|
|
169
|
+
|
|
170
|
+
# 2) TYPE
|
|
171
|
+
if ann is not inspect._empty and container.has(ann):
|
|
172
|
+
return container.get(ann)
|
|
173
|
+
|
|
174
|
+
# 3) TYPE MRO
|
|
175
|
+
if ann is not inspect._empty:
|
|
176
|
+
try:
|
|
177
|
+
for base in getattr(ann, "__mro__", ())[1:]:
|
|
178
|
+
if base is object:
|
|
179
|
+
break
|
|
180
|
+
if container.has(base):
|
|
181
|
+
return container.get(base)
|
|
182
|
+
except Exception:
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
# 4) str(NAME)
|
|
186
|
+
if container.has(str(p.name)):
|
|
187
|
+
return container.get(str(p.name))
|
|
188
|
+
|
|
189
|
+
key = p.name if ann is inspect._empty else ann
|
|
190
|
+
return container.get(key)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _scan_and_configure(
|
|
194
|
+
package_or_name,
|
|
195
|
+
container: PicoContainer,
|
|
196
|
+
exclude: Optional[Callable[[str], bool]] = None
|
|
197
|
+
):
|
|
140
198
|
package = importlib.import_module(package_or_name) if isinstance(package_or_name, str) else package_or_name
|
|
141
199
|
logging.info(f"🚀 Scanning in '{package.__name__}'...")
|
|
142
200
|
component_classes, factory_classes = [], []
|
|
201
|
+
|
|
143
202
|
for _, name, _ in pkgutil.walk_packages(package.__path__, package.__name__ + '.'):
|
|
203
|
+
if exclude and exclude(name):
|
|
204
|
+
logging.info(f" ⏭️ Skipping module {name} (excluded)")
|
|
205
|
+
continue
|
|
144
206
|
try:
|
|
145
207
|
module = importlib.import_module(name)
|
|
146
208
|
for _, obj in inspect.getmembers(module, inspect.isclass):
|
|
147
|
-
if
|
|
209
|
+
if getattr(obj, '_is_component', False):
|
|
148
210
|
component_classes.append(obj)
|
|
149
|
-
elif
|
|
211
|
+
elif getattr(obj, '_is_factory_component', False):
|
|
150
212
|
factory_classes.append(obj)
|
|
151
213
|
except Exception as e:
|
|
152
214
|
logging.warning(f" ⚠️ Module {name} not processed: {e}")
|
|
153
215
|
|
|
216
|
+
# Register factories
|
|
154
217
|
for factory_cls in factory_classes:
|
|
155
218
|
try:
|
|
156
219
|
sig = inspect.signature(factory_cls.__init__)
|
|
@@ -161,30 +224,64 @@ def _scan_and_configure(package_or_name, container: PicoContainer):
|
|
|
161
224
|
except Exception as e:
|
|
162
225
|
logging.error(f" ❌ Error in factory {factory_cls.__name__}: {e}", exc_info=True)
|
|
163
226
|
|
|
227
|
+
# Register components
|
|
164
228
|
for component_cls in component_classes:
|
|
165
229
|
key = getattr(component_cls, '_component_key', component_cls)
|
|
230
|
+
|
|
166
231
|
def create_component(cls=component_cls):
|
|
167
232
|
sig = inspect.signature(cls.__init__)
|
|
168
233
|
deps = {}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
inspect.Parameter.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
234
|
+
tok = _resolving.set(True)
|
|
235
|
+
try:
|
|
236
|
+
for p in sig.parameters.values():
|
|
237
|
+
if p.name == 'self' or p.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
238
|
+
continue
|
|
239
|
+
deps[p.name] = _resolve_param(container, p)
|
|
240
|
+
finally:
|
|
241
|
+
_resolving.reset(tok)
|
|
177
242
|
return cls(**deps)
|
|
243
|
+
|
|
178
244
|
container.bind(key, create_component)
|
|
179
245
|
|
|
180
246
|
_container = None
|
|
181
|
-
|
|
247
|
+
|
|
248
|
+
def init(root_package, *, exclude: Optional[Callable[[str], bool]] = None, auto_exclude_caller: bool = True):
|
|
249
|
+
"""
|
|
250
|
+
Initialize the global container and scan the given root package/module.
|
|
251
|
+
While scanning, re-entrant userland access to container.get() is blocked
|
|
252
|
+
to avoid import-time side effects.
|
|
253
|
+
"""
|
|
182
254
|
global _container
|
|
183
255
|
if _container:
|
|
184
256
|
return _container
|
|
257
|
+
|
|
258
|
+
combined_exclude = exclude
|
|
259
|
+
if auto_exclude_caller:
|
|
260
|
+
try:
|
|
261
|
+
caller_frame = inspect.stack()[1].frame
|
|
262
|
+
caller_module = inspect.getmodule(caller_frame)
|
|
263
|
+
caller_name = getattr(caller_module, "__name__", None)
|
|
264
|
+
except Exception:
|
|
265
|
+
caller_name = None
|
|
266
|
+
|
|
267
|
+
if caller_name:
|
|
268
|
+
if combined_exclude is None:
|
|
269
|
+
def combined_exclude(mod: str, _caller=caller_name):
|
|
270
|
+
return mod == _caller
|
|
271
|
+
else:
|
|
272
|
+
prev = combined_exclude
|
|
273
|
+
def combined_exclude(mod: str, _caller=caller_name, _prev=prev):
|
|
274
|
+
return mod == _caller or _prev(mod)
|
|
275
|
+
|
|
185
276
|
_container = PicoContainer()
|
|
186
277
|
logging.info("🔌 Initializing 'pico-ioc'...")
|
|
187
|
-
|
|
278
|
+
|
|
279
|
+
tok = _scanning.set(True)
|
|
280
|
+
try:
|
|
281
|
+
_scan_and_configure(root_package, _container, exclude=combined_exclude)
|
|
282
|
+
finally:
|
|
283
|
+
_scanning.reset(tok)
|
|
284
|
+
|
|
188
285
|
logging.info("✅ Container configured and ready.")
|
|
189
286
|
return _container
|
|
190
287
|
|
|
@@ -195,19 +292,27 @@ def factory_component(cls):
|
|
|
195
292
|
setattr(cls, '_is_factory_component', True)
|
|
196
293
|
return cls
|
|
197
294
|
|
|
198
|
-
def provides(
|
|
295
|
+
def provides(key: Any, *, lazy: bool = True):
|
|
296
|
+
"""
|
|
297
|
+
Declare that a factory method provides a component under 'key'.
|
|
298
|
+
By default, returns a LazyProxy that instantiates upon first real use.
|
|
299
|
+
"""
|
|
199
300
|
def decorator(func):
|
|
200
301
|
@functools.wraps(func)
|
|
201
302
|
def wrapper(*args, **kwargs):
|
|
202
303
|
return LazyProxy(lambda: func(*args, **kwargs)) if lazy else func(*args, **kwargs)
|
|
203
|
-
setattr(wrapper, '_provides_name',
|
|
304
|
+
setattr(wrapper, '_provides_name', key) # legacy-compatible storage
|
|
204
305
|
return wrapper
|
|
205
306
|
return decorator
|
|
206
307
|
|
|
207
308
|
def component(cls=None, *, name: str = None):
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
309
|
+
"""
|
|
310
|
+
Mark a class as a component. Registered by class type by default,
|
|
311
|
+
or by 'name' if provided.
|
|
312
|
+
"""
|
|
313
|
+
def decorator(c):
|
|
314
|
+
setattr(c, '_is_component', True)
|
|
315
|
+
setattr(c, '_component_key', name if name is not None else c)
|
|
316
|
+
return c
|
|
212
317
|
return decorator(cls) if cls else decorator
|
|
213
318
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '0.2.1'
|