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 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 _scan_and_configure(package_or_name, container: PicoContainer):
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
- dep_key = p.annotation if p.annotation is not inspect._empty else p.name
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
- def init(root_package):
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(name: str, lazy: bool = True):
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
- setattr(wrapper, '_provides_name', name)
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.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
+ [![PyPI](https://img.shields.io/pypi/v/pico-ioc.svg)](https://pypi.org/project/pico-ioc/)
28
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
29
+ ![CI (tox matrix)](https://github.com/dperezcabrera/pico-ioc/actions/workflows/ci.yml/badge.svg)
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
@@ -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,,