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 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.1'
1
+ __version__ = '0.2.0'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pico-ioc
3
- Version: 0.1.1
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
- # Pico-IoC: A Minimalist IoC Container for Python
24
+
25
+ # 📦 Pico-IoC: A Minimalist IoC Container for Python
25
26
 
26
27
  [![PyPI](https://img.shields.io/pypi/v/pico-ioc.svg)](https://pypi.org/project/pico-ioc/)
27
28
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
28
29
  ![CI (tox matrix)](https://github.com/dperezcabrera/pico-ioc/actions/workflows/ci.yml/badge.svg)
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
- * **Zero Dependencies:** Pure Python, no external libraries.
41
- * 🚀 **Decorator-Based API:** Simple decorators like `@component` and `@provides`.
42
- * 🔍 **Automatic Discovery:** Scans your package to auto-register components.
43
- * 🧩 **Lazy Instantiation:** Objects are created on first use.
44
- * 🏭 **Flexible Factories:** Encapsulate complex creation logic.
45
- * 🤝 **Framework-Agnostic:** Works with Flask, FastAPI, CLIs, scripts, etc.
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
- ## More Examples
87
+ ## 🧩 Custom Component Keys
87
88
 
88
- ### 🧩 Custom Component Name
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"): # refer by custom name if you prefer
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
- > Pico-IoC prefers **type annotations** to resolve deps; if missing, it falls back to the **parameter name**.
111
+ ---
110
112
 
111
- ### 💤 Lazy Factories (only build when used)
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(name="heavy_service") # returns a LazyProxy by default
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
- ### 📦 Project-Style Package Scanning
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
- ## API Reference (mini)
182
+ ## 🧠 Dependency Resolution Order
178
183
 
179
- ### `init(root_package_or_module) -> PicoContainer`
184
+ When Pico-IoC instantiates a component, it tries to resolve each parameter in this order:
180
185
 
181
- Initialize the container by scanning a root **package** (str) or **module**. Returns the configured container.
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
- ### `@component(cls=None, *, name: str | None = None)`
191
+ ---
184
192
 
185
- Mark a class as a component. Registered by **class type** by default, or by **name** if provided.
193
+ ## 🛠 API Reference
186
194
 
187
- ### `@factory_component`
195
+ ### `init(root_package_or_module, *, exclude=None, auto_exclude_caller=True) -> PicoContainer`
188
196
 
189
- Mark a class as a factory holder. Methods inside can be `@provides(...)`.
197
+ Scan the given root **package** (str) or **module**.
198
+ By default, excludes the calling module.
190
199
 
191
- ### `@provides(name: str, lazy: bool = True)`
200
+ ### `@component(cls=None, *, name=None)`
192
201
 
193
- Declare that a factory method **produces** a component registered under `name`. By default it’s **lazy** (a proxy that creates on first real use).
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
- ## Testing
207
+ Register a class as a factory of components.
198
208
 
199
- `pico-ioc` ships with `pytest` tests and a `tox` matrix.
209
+ ### `@provides(key=None, *, name=None, lazy=True)`
200
210
 
201
- ```bash
202
- pip install tox
203
- tox -e py311 # run on a specific version
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
- ## Contributing
217
+ ## 🧪 Testing
210
218
 
211
- Issues and PRs are welcome. If you spot a bug or have an idea, open an issue!
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 the [LICENSE](https://opensource.org/licenses/MIT).
228
+ MIT — see [LICENSE](https://opensource.org/licenses/MIT)
218
229
 
219
230
  ---
220
231
 
221
- ## Authors
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,,
@@ -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,,