pico-ioc 0.1.1__tar.gz → 0.2.0__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.
@@ -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**
@@ -1,29 +1,32 @@
1
- # Pico-IoC: A Minimalist IoC Container for Python
1
+
2
+ # 📦 Pico-IoC: A Minimalist IoC Container for Python
2
3
 
3
4
  [![PyPI](https://img.shields.io/pypi/v/pico-ioc.svg)](https://pypi.org/project/pico-ioc/)
4
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
6
  ![CI (tox matrix)](https://github.com/dperezcabrera/pico-ioc/actions/workflows/ci.yml/badge.svg)
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
- * **Zero Dependencies:** Pure Python, no external libraries.
18
- * 🚀 **Decorator-Based API:** Simple decorators like `@component` and `@provides`.
19
- * 🔍 **Automatic Discovery:** Scans your package to auto-register components.
20
- * 🧩 **Lazy Instantiation:** Objects are created on first use.
21
- * 🏭 **Flexible Factories:** Encapsulate complex creation logic.
22
- * 🤝 **Framework-Agnostic:** Works with Flask, FastAPI, CLIs, scripts, etc.
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
- ## More Examples
64
+ ## 🧩 Custom Component Keys
64
65
 
65
- ### 🧩 Custom Component Name
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"): # refer by custom name if you prefer
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
- > Pico-IoC prefers **type annotations** to resolve deps; if missing, it falls back to the **parameter name**.
88
+ ---
87
89
 
88
- ### 💤 Lazy Factories (only build when used)
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(name="heavy_service") # returns a LazyProxy by default
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
- ### 📦 Project-Style Package Scanning
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
- ## API Reference (mini)
159
+ ## 🧠 Dependency Resolution Order
155
160
 
156
- ### `init(root_package_or_module) -> PicoContainer`
161
+ When Pico-IoC instantiates a component, it tries to resolve each parameter in this order:
157
162
 
158
- Initialize the container by scanning a root **package** (str) or **module**. Returns the configured container.
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
- ### `@component(cls=None, *, name: str | None = None)`
168
+ ---
161
169
 
162
- Mark a class as a component. Registered by **class type** by default, or by **name** if provided.
170
+ ## 🛠 API Reference
163
171
 
164
- ### `@factory_component`
172
+ ### `init(root_package_or_module, *, exclude=None, auto_exclude_caller=True) -> PicoContainer`
165
173
 
166
- Mark a class as a factory holder. Methods inside can be `@provides(...)`.
174
+ Scan the given root **package** (str) or **module**.
175
+ By default, excludes the calling module.
167
176
 
168
- ### `@provides(name: str, lazy: bool = True)`
177
+ ### `@component(cls=None, *, name=None)`
169
178
 
170
- Declare that a factory method **produces** a component registered under `name`. By default it’s **lazy** (a proxy that creates on first real use).
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
- ## Testing
184
+ Register a class as a factory of components.
175
185
 
176
- `pico-ioc` ships with `pytest` tests and a `tox` matrix.
186
+ ### `@provides(key=None, *, name=None, lazy=True)`
177
187
 
178
- ```bash
179
- pip install tox
180
- tox -e py311 # run on a specific version
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
- ## Contributing
194
+ ## 🧪 Testing
187
195
 
188
- Issues and PRs are welcome. If you spot a bug or have an idea, open an issue!
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 the [LICENSE](https://opensource.org/licenses/MIT).
205
+ MIT — see [LICENSE](https://opensource.org/licenses/MIT)
195
206
 
196
207
  ---
197
208
 
198
- ## Authors
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,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
 
@@ -0,0 +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,321 @@
1
+ import pytest
2
+ import sys
3
+ import pico_ioc
4
+
5
+ # --- Test Environment Setup Fixture ---
6
+
7
+ @pytest.fixture
8
+ def test_project(tmp_path):
9
+ """
10
+ Creates a fake project in a temporary directory so the pico_ioc scanner
11
+ can find components/factories via import.
12
+ """
13
+ project_root = tmp_path / "test_project"
14
+ project_root.mkdir()
15
+
16
+ # Make the temp root importable
17
+ sys.path.insert(0, str(tmp_path))
18
+
19
+ # Turn 'test_project' into a real package
20
+ (project_root / "__init__.py").touch()
21
+
22
+ # Create the package 'services'
23
+ package_dir = project_root / "services"
24
+ package_dir.mkdir()
25
+ (package_dir / "__init__.py").touch()
26
+
27
+ # Components:
28
+ # - SimpleService (no deps)
29
+ # - AnotherService (depends on SimpleService by type)
30
+ # - CustomNameService (registered by custom name)
31
+ # - NeedsByName (depends by name only)
32
+ # - NeedsNameVsType (name should win over type)
33
+ # - NeedsTypeFallback (fallback to base type via MRO)
34
+ (package_dir / "components.py").write_text(
35
+ """
36
+ from pico_ioc import component
37
+
38
+ class BaseInterface: ...
39
+ class SubInterface(BaseInterface): ...
40
+
41
+ @component
42
+ class SimpleService:
43
+ def get_id(self):
44
+ return id(self)
45
+
46
+ @component
47
+ class AnotherService:
48
+ def __init__(self, simple_service: SimpleService):
49
+ # Will resolve by TYPE because there is no provider bound by the name "simple_service"
50
+ self.child = simple_service
51
+
52
+ @component(name="custom_name_service")
53
+ class CustomNameService:
54
+ pass
55
+
56
+ @component
57
+ class NeedsByName:
58
+ def __init__(self, fast_model):
59
+ # Will resolve by NAME, since there will be a provider bound to "fast_model"
60
+ self.model = fast_model
61
+
62
+ @component
63
+ class NeedsNameVsType:
64
+ def __init__(self, fast_model: BaseInterface):
65
+ # There will be providers for BOTH the name "fast_model" and the base type.
66
+ # NAME must win over TYPE.
67
+ self.model = fast_model
68
+
69
+ @component
70
+ class NeedsTypeFallback:
71
+ def __init__(self, impl: SubInterface):
72
+ # There will NOT be a provider for the name "impl" nor for SubInterface directly,
73
+ # but there will be one for BaseInterface → must fallback via MRO.
74
+ self.impl = impl
75
+
76
+ @component
77
+ class MissingDep:
78
+ def __init__(self, missing):
79
+ # No provider by name nor type: must raise on resolution.
80
+ self.missing = missing
81
+ """
82
+ )
83
+
84
+ # Factories:
85
+ # - complex_service (lazy via LazyProxy; counter to assert laziness)
86
+ # - fast_model (by NAME)
87
+ # - base_interface (by TYPE: BaseInterface)
88
+ (package_dir / "factories.py").write_text(
89
+ """
90
+ from pico_ioc import factory_component, provides
91
+ from .components import BaseInterface
92
+
93
+ # Used to assert lazy instantiation
94
+ CREATION_COUNTER = {"value": 0}
95
+ FAST_COUNTER = {"value": 0}
96
+ BASE_COUNTER = {"value": 0}
97
+
98
+ @factory_component
99
+ class ServiceFactory:
100
+ @provides(key="complex_service")
101
+ def create_complex_service(self):
102
+ # Increment ONLY when the real object is created (not when proxy is returned)
103
+ CREATION_COUNTER["value"] += 1
104
+ return "This is a complex service"
105
+
106
+ @provides(key="fast_model")
107
+ def create_fast_model(self):
108
+ FAST_COUNTER["value"] += 1
109
+ return {"who": "fast"} # any object; dict is convenient for identity checks
110
+
111
+ @provides(key=BaseInterface)
112
+ def create_base_interface(self):
113
+ BASE_COUNTER["value"] += 1
114
+ return {"who": "base"}
115
+ """
116
+ )
117
+
118
+ # Optional module that calls init() at import-time:
119
+ # used to test auto-exclude of the caller (to prevent re-entrancy).
120
+ (project_root / "entry.py").write_text(
121
+ """
122
+ import pico_ioc
123
+ import test_project
124
+
125
+ # If auto-exclude-caller is on AND _scan_and_configure() honors 'exclude',
126
+ # importing this module during scanning should NOT recurse infinitely.
127
+ ioc = pico_ioc.init(test_project)
128
+ """
129
+ )
130
+
131
+ # Yield the root package name used by pico_ioc.init()
132
+ yield "test_project"
133
+
134
+ # Teardown: remove path, reset container, purge modules from cache
135
+ sys.path.pop(0)
136
+ pico_ioc._container = None
137
+ mods_to_del = [m for m in list(sys.modules.keys()) if m == "test_project" or m.startswith("test_project.")]
138
+ for m in mods_to_del:
139
+ sys.modules.pop(m, None)
140
+
141
+
142
+ # --- Test Suite ---
143
+
144
+ def test_simple_component_retrieval(test_project):
145
+ """A plain component registered by class can be retrieved by its class key."""
146
+ from test_project.services.components import SimpleService
147
+
148
+ container = pico_ioc.init(test_project)
149
+ service = container.get(SimpleService)
150
+
151
+ assert service is not None
152
+ assert isinstance(service, SimpleService)
153
+
154
+
155
+ def test_dependency_injection_by_type_hint(test_project):
156
+ """
157
+ When a constructor parameter has a type hint and no provider is bound by name,
158
+ the container should resolve it by TYPE.
159
+ """
160
+ from test_project.services.components import SimpleService, AnotherService
161
+
162
+ container = pico_ioc.init(test_project)
163
+ another = container.get(AnotherService)
164
+
165
+ assert another is not None
166
+ assert isinstance(another.child, SimpleService)
167
+
168
+
169
+ def test_components_are_singletons_by_default(test_project):
170
+ """
171
+ Providers bound by the scanner are singletons: get() returns the same instance.
172
+ """
173
+ from test_project.services.components import SimpleService
174
+
175
+ container = pico_ioc.init(test_project)
176
+ s1 = container.get(SimpleService)
177
+ s2 = container.get(SimpleService)
178
+
179
+ assert s1 is s2
180
+ assert s1.get_id() == s2.get_id()
181
+
182
+
183
+ def test_get_unregistered_component_raises_error(test_project):
184
+ """
185
+ Requesting a key with no provider must raise NameError with a helpful message.
186
+ """
187
+ container = pico_ioc.init(test_project)
188
+
189
+ class Unregistered: ...
190
+ with pytest.raises(NameError, match="No provider found for key"):
191
+ container.get(Unregistered)
192
+
193
+
194
+ def test_factory_provides_component_by_name(test_project):
195
+ """
196
+ A factory method annotated with @provides(key="...") is bound by NAME and is retrievable.
197
+ """
198
+ container = pico_ioc.init(test_project)
199
+ svc = container.get("complex_service")
200
+
201
+ # Proxy must behave like the real string for equality
202
+ assert svc == "This is a complex service"
203
+
204
+
205
+ def test_factory_instantiation_is_lazy_and_singleton(test_project):
206
+ """
207
+ Factory methods with default lazy=True return a LazyProxy. The real object is created on first use.
208
+ Also, container should cache the created instance (singleton per key).
209
+ """
210
+ from test_project.services.factories import CREATION_COUNTER
211
+
212
+ container = pico_ioc.init(test_project)
213
+
214
+ assert CREATION_COUNTER["value"] == 0
215
+
216
+ proxy = container.get("complex_service")
217
+ # Accessing attributes/methods of the proxy should trigger creation exactly once
218
+ assert CREATION_COUNTER["value"] == 0
219
+ up = proxy.upper()
220
+ assert up == "THIS IS A COMPLEX SERVICE"
221
+ assert CREATION_COUNTER["value"] == 1
222
+
223
+ # Re-accessing via the same proxy does not create again
224
+ _ = proxy.lower()
225
+ assert CREATION_COUNTER["value"] == 1
226
+
227
+ # Getting the same key again should return the same singleton instance (no extra creations)
228
+ again = container.get("complex_service")
229
+ assert again is proxy # same object returned by container
230
+ _ = again.strip()
231
+ assert CREATION_COUNTER["value"] == 1
232
+
233
+
234
+ def test_component_with_custom_name(test_project):
235
+ """
236
+ A component registered by custom name is retrievable by that name,
237
+ and NOT by its class.
238
+ """
239
+ from test_project.services.components import CustomNameService
240
+
241
+ container = pico_ioc.init(test_project)
242
+ svc = container.get("custom_name_service")
243
+ assert isinstance(svc, CustomNameService)
244
+
245
+ with pytest.raises(NameError):
246
+ container.get(CustomNameService)
247
+
248
+
249
+ def test_resolution_prefers_name_over_type(test_project):
250
+ """
251
+ If both a NAME-bound provider and a TYPE-bound provider exist, resolution MUST
252
+ prefer the NAME (parameter name) over the TYPE hint.
253
+ """
254
+ from test_project.services.components import NeedsNameVsType
255
+ from test_project.services.factories import FAST_COUNTER, BASE_COUNTER
256
+
257
+ container = pico_ioc.init(test_project)
258
+ comp = container.get(NeedsNameVsType)
259
+
260
+ # "fast_model" name must win → uses the fast provider
261
+ assert comp.model == {"who": "fast"}
262
+ assert FAST_COUNTER["value"] == 1
263
+ # Base provider should NOT be used for this resolution
264
+ assert BASE_COUNTER["value"] == 0
265
+
266
+
267
+ def test_resolution_by_name_only(test_project):
268
+ """
269
+ When a ctor parameter has NO type hint, the container must resolve strictly by NAME.
270
+ """
271
+ from test_project.services.components import NeedsByName
272
+ from test_project.services.factories import FAST_COUNTER
273
+
274
+ container = pico_ioc.init(test_project)
275
+ comp = container.get(NeedsByName)
276
+
277
+ assert comp.model == {"who": "fast"}
278
+ assert FAST_COUNTER["value"] == 1
279
+
280
+
281
+ def test_resolution_fallback_to_type_mro(test_project):
282
+ """
283
+ When there is no provider for the parameter NAME nor the exact TYPE,
284
+ the container must try TYPE's MRO and use the first available provider.
285
+ """
286
+ from test_project.services.components import NeedsTypeFallback
287
+ from test_project.services.factories import BASE_COUNTER
288
+
289
+ container = pico_ioc.init(test_project)
290
+ comp = container.get(NeedsTypeFallback)
291
+
292
+ # Resolved via MRO to BaseInterface provider
293
+ assert comp.impl == {"who": "base"}
294
+ assert BASE_COUNTER["value"] == 1
295
+
296
+
297
+ def test_missing_dependency_raises_clear_error(test_project):
298
+ """
299
+ If no provider exists for NAME nor TYPE nor MRO, resolution must raise NameError.
300
+ """
301
+ from test_project.services.components import MissingDep
302
+
303
+ container = pico_ioc.init(test_project)
304
+ with pytest.raises(NameError, match="No provider found for key"):
305
+ container.get(MissingDep)
306
+
307
+
308
+ @pytest.mark.skipif(
309
+ not hasattr(pico_ioc, "init"),
310
+ reason="init not available"
311
+ )
312
+ def test_auto_exclude_caller_prevents_reentrant_scan(test_project):
313
+ """
314
+ Smoke test: importing a module that calls pico_ioc.init(root) at import-time
315
+ should not cause re-entrant scans if init() auto-excludes the caller AND
316
+ _scan_and_configure honors the 'exclude' predicate.
317
+ """
318
+ # If the library correctly auto-excludes the caller and passes 'exclude'
319
+ # into the scanner (which must skip excluded modules), this import should be safe.
320
+ __import__("test_project.entry")
321
+
@@ -1 +0,0 @@
1
- __version__ = '0.1.1'
@@ -1,178 +0,0 @@
1
- # tests/test_pico_ioc.py
2
-
3
- import pytest
4
- import sys
5
- import pico_ioc
6
-
7
- # --- Test Environment Setup Fixture ---
8
-
9
- @pytest.fixture
10
- def test_project(tmp_path):
11
- """
12
- Creates a fake project structure in a temporary directory
13
- so that the pico_ioc scanner can find the components.
14
- """
15
- project_root = tmp_path / "test_project"
16
- project_root.mkdir()
17
-
18
- # Add the root directory to the Python path so modules can be imported
19
- sys.path.insert(0, str(tmp_path))
20
-
21
- # >>> THE LINE THAT FIXES THE PROBLEM <<<
22
- # Turn 'test_project' into a regular package.
23
- (project_root / "__init__.py").touch()
24
-
25
- # Create the package and modules with test components
26
- package_dir = project_root / "services"
27
- package_dir.mkdir()
28
-
29
- # __init__.py file to turn 'services' into a sub-package
30
- (package_dir / "__init__.py").touch()
31
-
32
- # Module with simple components and components with dependencies
33
- (package_dir / "components.py").write_text("""
34
- from pico_ioc import component
35
-
36
- @component
37
- class SimpleService:
38
- def get_id(self):
39
- return id(self)
40
-
41
- @component
42
- class AnotherService:
43
- def __init__(self, simple_service: SimpleService):
44
- self.child = simple_service
45
-
46
- @component(name="custom_name_service")
47
- class CustomNameService:
48
- pass
49
- """)
50
-
51
- # Module with a component factory
52
- (package_dir / "factories.py").write_text("""
53
- from pico_ioc import factory_component, provides
54
-
55
- # To test that instantiation is lazy
56
- CREATION_COUNTER = {"value": 0}
57
-
58
- @factory_component
59
- class ServiceFactory:
60
- @provides(name="complex_service")
61
- def create_complex_service(self):
62
- CREATION_COUNTER["value"] += 1
63
- return "This is a complex service"
64
- """)
65
-
66
- # Return the root package name for init() to use
67
- yield "test_project"
68
-
69
- sys.path.pop(0)
70
-
71
- # Reset the global pico_ioc container to isolate tests.
72
- pico_ioc._container = None
73
-
74
- # Purge the temporary package from the module cache so each test starts
75
- # with a fresh module state (e.g., CREATION_COUNTER resets to 0).
76
- mods_to_del = [
77
- m for m in list(sys.modules.keys())
78
- if m == "test_project" or m.startswith("test_project.")
79
- ]
80
- for m in mods_to_del:
81
- sys.modules.pop(m, None)
82
-
83
-
84
- # --- Test Suite ---
85
-
86
- def test_simple_component_retrieval(test_project):
87
- """Verifies that a simple component can be registered and retrieved."""
88
- # Import the TEST classes after the fixture has created them
89
- from test_project.services.components import SimpleService
90
-
91
- container = pico_ioc.init(test_project)
92
- service = container.get(SimpleService)
93
-
94
- assert service is not None
95
- assert isinstance(service, SimpleService)
96
-
97
- def test_dependency_injection(test_project):
98
- """Verifies that a dependency is correctly injected into another component."""
99
- from test_project.services.components import SimpleService, AnotherService
100
-
101
- container = pico_ioc.init(test_project)
102
- another_service = container.get(AnotherService)
103
-
104
- assert another_service is not None
105
- assert hasattr(another_service, "child")
106
- assert isinstance(another_service.child, SimpleService)
107
-
108
- def test_components_are_singletons_by_default(test_project):
109
- """Verifies that get() always returns the same instance for a component."""
110
- from test_project.services.components import SimpleService
111
-
112
- container = pico_ioc.init(test_project)
113
- service1 = container.get(SimpleService)
114
- service2 = container.get(SimpleService)
115
-
116
- assert service1 is service2
117
- assert service1.get_id() == service2.get_id()
118
-
119
- def test_get_unregistered_component_raises_error(test_project):
120
- """Verifies that requesting an unregistered component raises a NameError."""
121
- container = pico_ioc.init(test_project)
122
-
123
- class UnregisteredClass:
124
- pass
125
-
126
- with pytest.raises(NameError, match="No provider found for key"):
127
- container.get(UnregisteredClass)
128
-
129
- def test_factory_provides_component(test_project):
130
- """Verifies that a component created by a factory can be retrieved."""
131
- container = pico_ioc.init(test_project)
132
-
133
- service = container.get("complex_service")
134
-
135
- # The object is a proxy, but it should delegate the comparison
136
- assert service == "This is a complex service"
137
-
138
- def test_factory_instantiation_is_lazy(test_project):
139
- """
140
- Verifies that a factory's @provides method is only executed
141
- when the object is first accessed.
142
- """
143
- # Import the counter from the test factory
144
- from test_project.services.factories import CREATION_COUNTER
145
-
146
- container = pico_ioc.init(test_project)
147
-
148
- # Initially, the counter must be 0 because nothing has been created yet
149
- assert CREATION_COUNTER["value"] == 0
150
-
151
- # We get the proxy, but this should NOT trigger the creation
152
- service_proxy = container.get("complex_service")
153
- assert CREATION_COUNTER["value"] == 0
154
-
155
- # Now we access an attribute of the real object (through the proxy)
156
- # This SHOULD trigger the creation
157
- result = service_proxy.upper() # .upper() is called on the real string
158
-
159
- assert CREATION_COUNTER["value"] == 1
160
- assert result == "THIS IS A COMPLEX SERVICE"
161
-
162
- # If we access it again, the counter should not increment
163
- _ = service_proxy.lower()
164
- assert CREATION_COUNTER["value"] == 1
165
-
166
- def test_component_with_custom_name(test_project):
167
- """Verifies that a component with a custom name can be registered and retrieved."""
168
- from test_project.services.components import CustomNameService
169
-
170
- container = pico_ioc.init(test_project)
171
-
172
- # We get the service using its custom name
173
- service = container.get("custom_name_service")
174
- assert isinstance(service, CustomNameService)
175
-
176
- # Verify that requesting it by its class fails, as it was registered by name
177
- with pytest.raises(NameError):
178
- container.get(CustomNameService)
File without changes
File without changes
File without changes
File without changes
File without changes