pico-ioc 0.1.0__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.2.0.dist-info/METADATA +233 -0
- pico_ioc-0.2.0.dist-info/RECORD +6 -0
- pico_ioc-0.1.0.dist-info/METADATA +0 -22
- pico_ioc-0.1.0.dist-info/RECORD +0 -6
- {pico_ioc-0.1.0.dist-info → pico_ioc-0.2.0.dist-info}/WHEEL +0 -0
- {pico_ioc-0.1.0.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'
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pico-ioc
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A minimalist, zero-dependency Inversion of Control (IoC) container for Python.
|
|
5
|
+
Author-email: David Perez Cabrera <dperezcabrera@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/dperezcabrera/pico-ioc
|
|
7
|
+
Project-URL: Repository, https://github.com/dperezcabrera/pico-ioc
|
|
8
|
+
Project-URL: Issue Tracker, https://github.com/dperezcabrera/pico-ioc/issues
|
|
9
|
+
Keywords: ioc,di,dependency injection,inversion of control,decorator
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# 📦 Pico-IoC: A Minimalist IoC Container for Python
|
|
26
|
+
|
|
27
|
+
[](https://pypi.org/project/pico-ioc/)
|
|
28
|
+
[](https://opensource.org/licenses/MIT)
|
|
29
|
+

|
|
30
|
+
|
|
31
|
+
**Pico-IoC** is a tiny, zero-dependency, decorator-based Inversion of Control (IoC) container for Python.
|
|
32
|
+
It helps you manage dependencies in a clean, intuitive, and *Pythonic* way.
|
|
33
|
+
|
|
34
|
+
The core idea is to let you build loosely coupled, easily testable applications without manually wiring components.
|
|
35
|
+
*Inspired by the IoC philosophy popularized by the Spring Framework.*
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## ✨ Key Features
|
|
40
|
+
|
|
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.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 📦 Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install pico-ioc
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 🚀 Quick Start
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from pico_ioc import component, init
|
|
64
|
+
|
|
65
|
+
@component
|
|
66
|
+
class AppConfig:
|
|
67
|
+
def get_db_url(self):
|
|
68
|
+
return "postgresql://user:pass@host/db"
|
|
69
|
+
|
|
70
|
+
@component
|
|
71
|
+
class DatabaseService:
|
|
72
|
+
def __init__(self, config: AppConfig):
|
|
73
|
+
self._cs = config.get_db_url()
|
|
74
|
+
|
|
75
|
+
def get_data(self):
|
|
76
|
+
return f"Data from {self._cs}"
|
|
77
|
+
|
|
78
|
+
# Initialize the container scanning the current module
|
|
79
|
+
container = init(__name__)
|
|
80
|
+
|
|
81
|
+
db = container.get(DatabaseService)
|
|
82
|
+
print(db.get_data()) # Data from postgresql://user:pass@host/db
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 🧩 Custom Component Keys
|
|
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.
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from pico_ioc import component, init
|
|
94
|
+
|
|
95
|
+
@component(name="config") # still supported for legacy code
|
|
96
|
+
class AppConfig:
|
|
97
|
+
def __init__(self):
|
|
98
|
+
self.db_url = "postgresql://user:pass@localhost/db"
|
|
99
|
+
|
|
100
|
+
@component
|
|
101
|
+
class Repository:
|
|
102
|
+
def __init__(self, config: "config"): # resolve by name
|
|
103
|
+
self._url = config.db_url
|
|
104
|
+
|
|
105
|
+
container = init(__name__)
|
|
106
|
+
repo = container.get(Repository)
|
|
107
|
+
print(repo._url) # postgresql://user:pass@localhost/db
|
|
108
|
+
print(container.get("config").db_url)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## 🏭 Factory Components and `@provides`
|
|
114
|
+
|
|
115
|
+
Factories can provide components under a specific **key**.
|
|
116
|
+
Default is lazy creation (via `LazyProxy`).
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from pico_ioc import factory_component, provides, init
|
|
120
|
+
|
|
121
|
+
CREATION_COUNTER = {"value": 0}
|
|
122
|
+
|
|
123
|
+
@factory_component
|
|
124
|
+
class ServicesFactory:
|
|
125
|
+
@provides(key="heavy_service") # preferred
|
|
126
|
+
def make_heavy(self):
|
|
127
|
+
CREATION_COUNTER["value"] += 1
|
|
128
|
+
return {"payload": "Hello from heavy service"}
|
|
129
|
+
|
|
130
|
+
container = init(__name__)
|
|
131
|
+
svc = container.get("heavy_service")
|
|
132
|
+
print(CREATION_COUNTER["value"]) # 0 (not created yet)
|
|
133
|
+
|
|
134
|
+
print(svc["payload"]) # triggers creation
|
|
135
|
+
print(CREATION_COUNTER["value"]) # 1
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## 📦 Project-Style Scanning
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
project_root/
|
|
144
|
+
└── myapp/
|
|
145
|
+
├── __init__.py
|
|
146
|
+
├── services.py
|
|
147
|
+
└── main.py
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**myapp/services.py**
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from pico_ioc import component
|
|
154
|
+
|
|
155
|
+
@component
|
|
156
|
+
class Config:
|
|
157
|
+
def __init__(self):
|
|
158
|
+
self.base_url = "https://api.example.com"
|
|
159
|
+
|
|
160
|
+
@component
|
|
161
|
+
class ApiClient:
|
|
162
|
+
def __init__(self, config: Config):
|
|
163
|
+
self.base_url = config.base_url
|
|
164
|
+
|
|
165
|
+
def get(self, path: str):
|
|
166
|
+
return f"GET {self.base_url}/{path}"
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**myapp/main.py**
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
import pico_ioc
|
|
173
|
+
from myapp.services import ApiClient
|
|
174
|
+
|
|
175
|
+
container = pico_ioc.init("myapp")
|
|
176
|
+
client = container.get(ApiClient)
|
|
177
|
+
print(client.get("status")) # GET https://api.example.com/status
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## 🧠 Dependency Resolution Order
|
|
183
|
+
|
|
184
|
+
When Pico-IoC instantiates a component, it tries to resolve each parameter in this order:
|
|
185
|
+
|
|
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
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## 🛠 API Reference
|
|
194
|
+
|
|
195
|
+
### `init(root_package_or_module, *, exclude=None, auto_exclude_caller=True) -> PicoContainer`
|
|
196
|
+
|
|
197
|
+
Scan the given root **package** (str) or **module**.
|
|
198
|
+
By default, excludes the calling module.
|
|
199
|
+
|
|
200
|
+
### `@component(cls=None, *, name=None)`
|
|
201
|
+
|
|
202
|
+
Register a class as a component.
|
|
203
|
+
If `name` is given, registers under that string; otherwise under the class type.
|
|
204
|
+
|
|
205
|
+
### `@factory_component`
|
|
206
|
+
|
|
207
|
+
Register a class as a factory of components.
|
|
208
|
+
|
|
209
|
+
### `@provides(key=None, *, name=None, lazy=True)`
|
|
210
|
+
|
|
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.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## 🧪 Testing
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
pip install tox
|
|
221
|
+
tox -e py311
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## 📜 License
|
|
227
|
+
|
|
228
|
+
MIT — see [LICENSE](https://opensource.org/licenses/MIT)
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
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.
|
|
233
|
+
|
|
@@ -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,,
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: pico-ioc
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: A minimalist, zero-dependency Inversion of Control (IoC) container for Python.
|
|
5
|
-
Author-email: David Perez Cabrera <dperezcabrera@gmail.com>
|
|
6
|
-
Project-URL: Homepage, https://github.com/dperezcabrera/pico-ioc
|
|
7
|
-
Project-URL: Repository, https://github.com/dperezcabrera/pico-ioc
|
|
8
|
-
Project-URL: Issue Tracker, https://github.com/dperezcabrera/pico-ioc/issues
|
|
9
|
-
Keywords: ioc,di,dependency injection,inversion of control,decorator
|
|
10
|
-
Classifier: Development Status :: 4 - Beta
|
|
11
|
-
Classifier: Programming Language :: Python :: 3
|
|
12
|
-
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
20
|
-
Classifier: Operating System :: OS Independent
|
|
21
|
-
Requires-Python: >=3.8
|
|
22
|
-
Description-Content-Type: text/markdown
|
pico_ioc-0.1.0.dist-info/RECORD
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
pico_ioc/__init__.py,sha256=SI-ee8rpiNU0f3uTndpTmBmZJ5RZG8MUSGOJXroJfKk,9710
|
|
2
|
-
pico_ioc/_version.py,sha256=IMjkMO3twhQzluVTo8Z6rE7Eg-9U79_LGKMcsWLKBkY,22
|
|
3
|
-
pico_ioc-0.1.0.dist-info/METADATA,sha256=VZHRU852qmS5KyavUYx2DJFjVRurY96bJy_I4qKNOsI,1090
|
|
4
|
-
pico_ioc-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
5
|
-
pico_ioc-0.1.0.dist-info/top_level.txt,sha256=_7_RLu616z_dtRw16impXn4Mw8IXe2J4BeX5912m5dQ,9
|
|
6
|
-
pico_ioc-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|