pico-ioc 0.1.1__py3-none-any.whl → 0.2.0__py3-none-any.whl
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/__init__.py +71 -14
- pico_ioc/_version.py +1 -1
- {pico_ioc-0.1.1.dist-info → pico_ioc-0.2.0.dist-info}/METADATA +58 -50
- pico_ioc-0.2.0.dist-info/RECORD +6 -0
- pico_ioc-0.1.1.dist-info/RECORD +0 -6
- {pico_ioc-0.1.1.dist-info → pico_ioc-0.2.0.dist-info}/WHEEL +0 -0
- {pico_ioc-0.1.1.dist-info → pico_ioc-0.2.0.dist-info}/top_level.txt +0 -0
pico_ioc/__init__.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# src/pico_ioc/__init__.py
|
|
2
2
|
|
|
3
3
|
import functools, inspect, pkgutil, importlib, logging, sys
|
|
4
|
-
from typing import Callable, Any, Iterator, AsyncIterator
|
|
4
|
+
from typing import Callable, Any, Iterator, Optional, AsyncIterator
|
|
5
5
|
|
|
6
6
|
try:
|
|
7
7
|
# written at build time by setuptools-scm
|
|
@@ -16,12 +16,15 @@ __all__ = ["__version__"]
|
|
|
16
16
|
# ==============================================================================
|
|
17
17
|
class PicoContainer:
|
|
18
18
|
def __init__(self):
|
|
19
|
-
self._providers = {}
|
|
20
|
-
self._singletons = {}
|
|
19
|
+
self._providers: dict[Any, Callable[[], Any]] = {}
|
|
20
|
+
self._singletons: dict[Any, Any] = {}
|
|
21
21
|
|
|
22
22
|
def bind(self, key: Any, provider: Callable[[], Any]):
|
|
23
23
|
self._providers[key] = provider
|
|
24
24
|
|
|
25
|
+
def has(self, key: Any) -> bool:
|
|
26
|
+
return key in self._providers or key in self._singletons
|
|
27
|
+
|
|
25
28
|
def get(self, key: Any) -> Any:
|
|
26
29
|
if key in self._singletons:
|
|
27
30
|
return self._singletons[key]
|
|
@@ -136,11 +139,47 @@ class LazyProxy:
|
|
|
136
139
|
# ==============================================================================
|
|
137
140
|
# --- 2. The Scanner and `init` Facade ---
|
|
138
141
|
# ==============================================================================
|
|
139
|
-
def
|
|
142
|
+
def _resolve_param(container: PicoContainer, p: inspect.Parameter) -> Any:
|
|
143
|
+
if p.name == 'self' or p.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
144
|
+
raise RuntimeError("Invalid param for resolution")
|
|
145
|
+
|
|
146
|
+
if container.has(p.name):
|
|
147
|
+
return container.get(p.name)
|
|
148
|
+
|
|
149
|
+
ann = p.annotation
|
|
150
|
+
if ann is not inspect._empty and container.has(ann):
|
|
151
|
+
return container.get(ann)
|
|
152
|
+
|
|
153
|
+
if ann is not inspect._empty:
|
|
154
|
+
try:
|
|
155
|
+
for base in getattr(ann, "__mro__", ())[1:]:
|
|
156
|
+
if base is object:
|
|
157
|
+
break
|
|
158
|
+
if container.has(base):
|
|
159
|
+
return container.get(base)
|
|
160
|
+
except Exception:
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
if container.has(str(p.name)):
|
|
164
|
+
return container.get(str(p.name))
|
|
165
|
+
|
|
166
|
+
key = p.name if not (ann and ann is not inspect._empty) else ann
|
|
167
|
+
return container.get(key)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _scan_and_configure(
|
|
171
|
+
package_or_name,
|
|
172
|
+
container: PicoContainer,
|
|
173
|
+
exclude: Optional[Callable[[str], bool]] = None
|
|
174
|
+
):
|
|
140
175
|
package = importlib.import_module(package_or_name) if isinstance(package_or_name, str) else package_or_name
|
|
141
176
|
logging.info(f"🚀 Scanning in '{package.__name__}'...")
|
|
142
177
|
component_classes, factory_classes = [], []
|
|
143
178
|
for _, name, _ in pkgutil.walk_packages(package.__path__, package.__name__ + '.'):
|
|
179
|
+
# Skip excluded modules (used by auto_exclude_caller and custom excludes)
|
|
180
|
+
if exclude and exclude(name):
|
|
181
|
+
logging.info(f" ⏭️ Skipping module {name} (excluded)")
|
|
182
|
+
continue
|
|
144
183
|
try:
|
|
145
184
|
module = importlib.import_module(name)
|
|
146
185
|
for _, obj in inspect.getmembers(module, inspect.isclass):
|
|
@@ -167,24 +206,41 @@ def _scan_and_configure(package_or_name, container: PicoContainer):
|
|
|
167
206
|
sig = inspect.signature(cls.__init__)
|
|
168
207
|
deps = {}
|
|
169
208
|
for p in sig.parameters.values():
|
|
170
|
-
if p.name == 'self' or p.kind in (
|
|
171
|
-
inspect.Parameter.VAR_POSITIONAL, # *args
|
|
172
|
-
inspect.Parameter.VAR_KEYWORD, # **kwargs
|
|
173
|
-
):
|
|
209
|
+
if p.name == 'self' or p.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
174
210
|
continue
|
|
175
|
-
|
|
176
|
-
deps[p.name] = container.get(dep_key)
|
|
211
|
+
deps[p.name] = _resolve_param(container, p)
|
|
177
212
|
return cls(**deps)
|
|
178
213
|
container.bind(key, create_component)
|
|
179
214
|
|
|
180
215
|
_container = None
|
|
181
|
-
|
|
216
|
+
|
|
217
|
+
def init(root_package, *, exclude: Optional[Callable[[str], bool]] = None, auto_exclude_caller: bool = True):
|
|
182
218
|
global _container
|
|
183
219
|
if _container:
|
|
184
220
|
return _container
|
|
221
|
+
|
|
222
|
+
combined_exclude = exclude
|
|
223
|
+
if auto_exclude_caller:
|
|
224
|
+
# módulo que invoca a init()
|
|
225
|
+
try:
|
|
226
|
+
caller_frame = inspect.stack()[1].frame
|
|
227
|
+
caller_module = inspect.getmodule(caller_frame)
|
|
228
|
+
caller_name = getattr(caller_module, "__name__", None)
|
|
229
|
+
except Exception:
|
|
230
|
+
caller_name = None
|
|
231
|
+
|
|
232
|
+
if caller_name:
|
|
233
|
+
if combined_exclude is None:
|
|
234
|
+
def combined_exclude(mod: str, _caller=caller_name):
|
|
235
|
+
return mod == _caller
|
|
236
|
+
else:
|
|
237
|
+
prev = combined_exclude
|
|
238
|
+
def combined_exclude(mod: str, _caller=caller_name, _prev=prev):
|
|
239
|
+
return mod == _caller or _prev(mod)
|
|
240
|
+
|
|
185
241
|
_container = PicoContainer()
|
|
186
242
|
logging.info("🔌 Initializing 'pico-ioc'...")
|
|
187
|
-
_scan_and_configure(root_package, _container)
|
|
243
|
+
_scan_and_configure(root_package, _container, exclude=combined_exclude)
|
|
188
244
|
logging.info("✅ Container configured and ready.")
|
|
189
245
|
return _container
|
|
190
246
|
|
|
@@ -195,12 +251,13 @@ def factory_component(cls):
|
|
|
195
251
|
setattr(cls, '_is_factory_component', True)
|
|
196
252
|
return cls
|
|
197
253
|
|
|
198
|
-
def provides(
|
|
254
|
+
def provides(key: Any, *, lazy: bool = True):
|
|
199
255
|
def decorator(func):
|
|
200
256
|
@functools.wraps(func)
|
|
201
257
|
def wrapper(*args, **kwargs):
|
|
202
258
|
return LazyProxy(lambda: func(*args, **kwargs)) if lazy else func(*args, **kwargs)
|
|
203
|
-
|
|
259
|
+
# mantenemos compat con _provides_name (por si alguien lo usa)
|
|
260
|
+
setattr(wrapper, '_provides_name', key)
|
|
204
261
|
return wrapper
|
|
205
262
|
return decorator
|
|
206
263
|
|
pico_ioc/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = '0.
|
|
1
|
+
__version__ = '0.2.0'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pico-ioc
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.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
|
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**
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
pico_ioc/__init__.py,sha256=JkCWRwZb_Mv86Q6gI7wJnsFuKWh0A0B7u-uuAfyeF6Y,11781
|
|
2
|
+
pico_ioc/_version.py,sha256=FVHPBGkfhbQDi_z3v0PiKJrXXqXOx0vGW_1VaqNJi7U,22
|
|
3
|
+
pico_ioc-0.2.0.dist-info/METADATA,sha256=CgJTdTIs1-MeabncD7fl9sez6t5a41jdYVea8qUDKOY,6599
|
|
4
|
+
pico_ioc-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
5
|
+
pico_ioc-0.2.0.dist-info/top_level.txt,sha256=_7_RLu616z_dtRw16impXn4Mw8IXe2J4BeX5912m5dQ,9
|
|
6
|
+
pico_ioc-0.2.0.dist-info/RECORD,,
|
pico_ioc-0.1.1.dist-info/RECORD
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
pico_ioc/__init__.py,sha256=SI-ee8rpiNU0f3uTndpTmBmZJ5RZG8MUSGOJXroJfKk,9710
|
|
2
|
-
pico_ioc/_version.py,sha256=ls1camlIoMxEZz9gSkZ1OJo-MXqHWwKPtdPbZJmwp7E,22
|
|
3
|
-
pico_ioc-0.1.1.dist-info/METADATA,sha256=kDn0xAh82ph8bUrzpWv3wvt6PRU_raQ1msWeKAPz9XE,6081
|
|
4
|
-
pico_ioc-0.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
5
|
-
pico_ioc-0.1.1.dist-info/top_level.txt,sha256=_7_RLu616z_dtRw16impXn4Mw8IXe2J4BeX5912m5dQ,9
|
|
6
|
-
pico_ioc-0.1.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|