pico-ioc 0.2.0__tar.gz → 0.3.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.
- {pico_ioc-0.2.0 → pico_ioc-0.3.0}/PKG-INFO +1 -6
- {pico_ioc-0.2.0 → pico_ioc-0.3.0}/README.md +0 -5
- {pico_ioc-0.2.0 → pico_ioc-0.3.0}/src/pico_ioc/__init__.py +68 -20
- pico_ioc-0.3.0/src/pico_ioc/_version.py +1 -0
- {pico_ioc-0.2.0 → pico_ioc-0.3.0}/src/pico_ioc.egg-info/PKG-INFO +1 -6
- {pico_ioc-0.2.0 → pico_ioc-0.3.0}/tests/test_pico_ioc.py +40 -74
- pico_ioc-0.2.0/src/pico_ioc/_version.py +0 -1
- {pico_ioc-0.2.0 → pico_ioc-0.3.0}/.github/workflows/ci.yml +0 -0
- {pico_ioc-0.2.0 → pico_ioc-0.3.0}/.github/workflows/publish-to-pypi.yml +0 -0
- {pico_ioc-0.2.0 → pico_ioc-0.3.0}/Dockerfile.test +0 -0
- {pico_ioc-0.2.0 → pico_ioc-0.3.0}/Makefile +0 -0
- {pico_ioc-0.2.0 → pico_ioc-0.3.0}/pyproject.toml +0 -0
- {pico_ioc-0.2.0 → pico_ioc-0.3.0}/setup.cfg +0 -0
- {pico_ioc-0.2.0 → pico_ioc-0.3.0}/src/pico_ioc.egg-info/SOURCES.txt +0 -0
- {pico_ioc-0.2.0 → pico_ioc-0.3.0}/src/pico_ioc.egg-info/dependency_links.txt +0 -0
- {pico_ioc-0.2.0 → pico_ioc-0.3.0}/src/pico_ioc.egg-info/top_level.txt +0 -0
- {pico_ioc-0.2.0 → pico_ioc-0.3.0}/tox.ini +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pico-ioc
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -226,8 +226,3 @@ tox -e py311
|
|
|
226
226
|
## 📜 License
|
|
227
227
|
|
|
228
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
|
-
|
|
@@ -203,8 +203,3 @@ tox -e py311
|
|
|
203
203
|
## 📜 License
|
|
204
204
|
|
|
205
205
|
MIT — see [LICENSE](https://opensource.org/licenses/MIT)
|
|
206
|
-
|
|
207
|
-
---
|
|
208
|
-
|
|
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.
|
|
210
|
-
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
from typing import Callable, Any, Iterator, Optional, AsyncIterator
|
|
1
|
+
import functools, inspect, pkgutil, importlib, logging
|
|
2
|
+
from typing import Callable, Any, Optional
|
|
3
|
+
from contextvars import ContextVar
|
|
5
4
|
|
|
6
5
|
try:
|
|
7
6
|
# written at build time by setuptools-scm
|
|
@@ -11,6 +10,15 @@ except Exception: # pragma: no cover
|
|
|
11
10
|
|
|
12
11
|
__all__ = ["__version__"]
|
|
13
12
|
|
|
13
|
+
# ------------------------------------------------------------------------------
|
|
14
|
+
# Re-entrancy guards
|
|
15
|
+
# ------------------------------------------------------------------------------
|
|
16
|
+
# True while init/scan is running. Blocks userland container access during scan.
|
|
17
|
+
_scanning: ContextVar[bool] = ContextVar("pico_scanning", default=False)
|
|
18
|
+
|
|
19
|
+
# True while the container is resolving deps for a component (internal use allowed).
|
|
20
|
+
_resolving: ContextVar[bool] = ContextVar("pico_resolving", default=False)
|
|
21
|
+
|
|
14
22
|
# ==============================================================================
|
|
15
23
|
# --- 1. Container and Chameleon Proxy (Framework-Agnostic) ---
|
|
16
24
|
# ==============================================================================
|
|
@@ -26,6 +34,16 @@ class PicoContainer:
|
|
|
26
34
|
return key in self._providers or key in self._singletons
|
|
27
35
|
|
|
28
36
|
def get(self, key: Any) -> Any:
|
|
37
|
+
# Forbid user code calling container.get() while the scanner is importing modules.
|
|
38
|
+
# Allow only internal calls performed during dependency resolution.
|
|
39
|
+
if _scanning.get() and not _resolving.get():
|
|
40
|
+
raise RuntimeError(
|
|
41
|
+
"pico-ioc: re-entrant container access during scan. "
|
|
42
|
+
"Avoid calling init()/get() at import time (e.g., in a module body). "
|
|
43
|
+
"Move resolution to runtime (e.g., under if __name__ == '__main__':) "
|
|
44
|
+
"or delay it until pico-ioc init completes."
|
|
45
|
+
)
|
|
46
|
+
|
|
29
47
|
if key in self._singletons:
|
|
30
48
|
return self._singletons[key]
|
|
31
49
|
if key in self._providers:
|
|
@@ -143,13 +161,17 @@ def _resolve_param(container: PicoContainer, p: inspect.Parameter) -> Any:
|
|
|
143
161
|
if p.name == 'self' or p.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
144
162
|
raise RuntimeError("Invalid param for resolution")
|
|
145
163
|
|
|
164
|
+
# 1) NAME
|
|
146
165
|
if container.has(p.name):
|
|
147
166
|
return container.get(p.name)
|
|
148
167
|
|
|
149
168
|
ann = p.annotation
|
|
169
|
+
|
|
170
|
+
# 2) TYPE
|
|
150
171
|
if ann is not inspect._empty and container.has(ann):
|
|
151
172
|
return container.get(ann)
|
|
152
173
|
|
|
174
|
+
# 3) TYPE MRO
|
|
153
175
|
if ann is not inspect._empty:
|
|
154
176
|
try:
|
|
155
177
|
for base in getattr(ann, "__mro__", ())[1:]:
|
|
@@ -160,10 +182,11 @@ def _resolve_param(container: PicoContainer, p: inspect.Parameter) -> Any:
|
|
|
160
182
|
except Exception:
|
|
161
183
|
pass
|
|
162
184
|
|
|
185
|
+
# 4) str(NAME)
|
|
163
186
|
if container.has(str(p.name)):
|
|
164
187
|
return container.get(str(p.name))
|
|
165
188
|
|
|
166
|
-
key = p.name if
|
|
189
|
+
key = p.name if ann is inspect._empty else ann
|
|
167
190
|
return container.get(key)
|
|
168
191
|
|
|
169
192
|
|
|
@@ -175,21 +198,22 @@ def _scan_and_configure(
|
|
|
175
198
|
package = importlib.import_module(package_or_name) if isinstance(package_or_name, str) else package_or_name
|
|
176
199
|
logging.info(f"🚀 Scanning in '{package.__name__}'...")
|
|
177
200
|
component_classes, factory_classes = [], []
|
|
201
|
+
|
|
178
202
|
for _, name, _ in pkgutil.walk_packages(package.__path__, package.__name__ + '.'):
|
|
179
|
-
# Skip excluded modules (used by auto_exclude_caller and custom excludes)
|
|
180
203
|
if exclude and exclude(name):
|
|
181
204
|
logging.info(f" ⏭️ Skipping module {name} (excluded)")
|
|
182
205
|
continue
|
|
183
206
|
try:
|
|
184
207
|
module = importlib.import_module(name)
|
|
185
208
|
for _, obj in inspect.getmembers(module, inspect.isclass):
|
|
186
|
-
if
|
|
209
|
+
if getattr(obj, '_is_component', False):
|
|
187
210
|
component_classes.append(obj)
|
|
188
|
-
elif
|
|
211
|
+
elif getattr(obj, '_is_factory_component', False):
|
|
189
212
|
factory_classes.append(obj)
|
|
190
213
|
except Exception as e:
|
|
191
214
|
logging.warning(f" ⚠️ Module {name} not processed: {e}")
|
|
192
215
|
|
|
216
|
+
# Register factories
|
|
193
217
|
for factory_cls in factory_classes:
|
|
194
218
|
try:
|
|
195
219
|
sig = inspect.signature(factory_cls.__init__)
|
|
@@ -200,28 +224,39 @@ def _scan_and_configure(
|
|
|
200
224
|
except Exception as e:
|
|
201
225
|
logging.error(f" ❌ Error in factory {factory_cls.__name__}: {e}", exc_info=True)
|
|
202
226
|
|
|
227
|
+
# Register components
|
|
203
228
|
for component_cls in component_classes:
|
|
204
229
|
key = getattr(component_cls, '_component_key', component_cls)
|
|
230
|
+
|
|
205
231
|
def create_component(cls=component_cls):
|
|
206
232
|
sig = inspect.signature(cls.__init__)
|
|
207
233
|
deps = {}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
234
|
+
tok = _resolving.set(True)
|
|
235
|
+
try:
|
|
236
|
+
for p in sig.parameters.values():
|
|
237
|
+
if p.name == 'self' or p.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
238
|
+
continue
|
|
239
|
+
deps[p.name] = _resolve_param(container, p)
|
|
240
|
+
finally:
|
|
241
|
+
_resolving.reset(tok)
|
|
212
242
|
return cls(**deps)
|
|
243
|
+
|
|
213
244
|
container.bind(key, create_component)
|
|
214
245
|
|
|
215
246
|
_container = None
|
|
216
247
|
|
|
217
248
|
def init(root_package, *, exclude: Optional[Callable[[str], bool]] = None, auto_exclude_caller: bool = True):
|
|
249
|
+
"""
|
|
250
|
+
Initialize the global container and scan the given root package/module.
|
|
251
|
+
While scanning, re-entrant userland access to container.get() is blocked
|
|
252
|
+
to avoid import-time side effects.
|
|
253
|
+
"""
|
|
218
254
|
global _container
|
|
219
255
|
if _container:
|
|
220
256
|
return _container
|
|
221
257
|
|
|
222
258
|
combined_exclude = exclude
|
|
223
259
|
if auto_exclude_caller:
|
|
224
|
-
# módulo que invoca a init()
|
|
225
260
|
try:
|
|
226
261
|
caller_frame = inspect.stack()[1].frame
|
|
227
262
|
caller_module = inspect.getmodule(caller_frame)
|
|
@@ -240,7 +275,13 @@ def init(root_package, *, exclude: Optional[Callable[[str], bool]] = None, auto_
|
|
|
240
275
|
|
|
241
276
|
_container = PicoContainer()
|
|
242
277
|
logging.info("🔌 Initializing 'pico-ioc'...")
|
|
243
|
-
|
|
278
|
+
|
|
279
|
+
tok = _scanning.set(True)
|
|
280
|
+
try:
|
|
281
|
+
_scan_and_configure(root_package, _container, exclude=combined_exclude)
|
|
282
|
+
finally:
|
|
283
|
+
_scanning.reset(tok)
|
|
284
|
+
|
|
244
285
|
logging.info("✅ Container configured and ready.")
|
|
245
286
|
return _container
|
|
246
287
|
|
|
@@ -252,19 +293,26 @@ def factory_component(cls):
|
|
|
252
293
|
return cls
|
|
253
294
|
|
|
254
295
|
def provides(key: Any, *, lazy: bool = True):
|
|
296
|
+
"""
|
|
297
|
+
Declare that a factory method provides a component under 'key'.
|
|
298
|
+
By default, returns a LazyProxy that instantiates upon first real use.
|
|
299
|
+
"""
|
|
255
300
|
def decorator(func):
|
|
256
301
|
@functools.wraps(func)
|
|
257
302
|
def wrapper(*args, **kwargs):
|
|
258
303
|
return LazyProxy(lambda: func(*args, **kwargs)) if lazy else func(*args, **kwargs)
|
|
259
|
-
|
|
260
|
-
setattr(wrapper, '_provides_name', key)
|
|
304
|
+
setattr(wrapper, '_provides_name', key) # legacy-compatible storage
|
|
261
305
|
return wrapper
|
|
262
306
|
return decorator
|
|
263
307
|
|
|
264
308
|
def component(cls=None, *, name: str = None):
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
309
|
+
"""
|
|
310
|
+
Mark a class as a component. Registered by class type by default,
|
|
311
|
+
or by 'name' if provided.
|
|
312
|
+
"""
|
|
313
|
+
def decorator(c):
|
|
314
|
+
setattr(c, '_is_component', True)
|
|
315
|
+
setattr(c, '_component_key', name if name is not None else c)
|
|
316
|
+
return c
|
|
269
317
|
return decorator(cls) if cls else decorator
|
|
270
318
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '0.3.0'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pico-ioc
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -226,8 +226,3 @@ tox -e py311
|
|
|
226
226
|
## 📜 License
|
|
227
227
|
|
|
228
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
|
-
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import pytest
|
|
2
2
|
import sys
|
|
3
|
+
import logging
|
|
3
4
|
import pico_ioc
|
|
4
5
|
|
|
5
6
|
# --- Test Environment Setup Fixture ---
|
|
@@ -24,13 +25,7 @@ def test_project(tmp_path):
|
|
|
24
25
|
package_dir.mkdir()
|
|
25
26
|
(package_dir / "__init__.py").touch()
|
|
26
27
|
|
|
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)
|
|
28
|
+
# Components
|
|
34
29
|
(package_dir / "components.py").write_text(
|
|
35
30
|
"""
|
|
36
31
|
from pico_ioc import component
|
|
@@ -81,10 +76,7 @@ class MissingDep:
|
|
|
81
76
|
"""
|
|
82
77
|
)
|
|
83
78
|
|
|
84
|
-
# Factories
|
|
85
|
-
# - complex_service (lazy via LazyProxy; counter to assert laziness)
|
|
86
|
-
# - fast_model (by NAME)
|
|
87
|
-
# - base_interface (by TYPE: BaseInterface)
|
|
79
|
+
# Factories
|
|
88
80
|
(package_dir / "factories.py").write_text(
|
|
89
81
|
"""
|
|
90
82
|
from pico_ioc import factory_component, provides
|
|
@@ -106,7 +98,7 @@ class ServiceFactory:
|
|
|
106
98
|
@provides(key="fast_model")
|
|
107
99
|
def create_fast_model(self):
|
|
108
100
|
FAST_COUNTER["value"] += 1
|
|
109
|
-
return {"who": "fast"}
|
|
101
|
+
return {"who": "fast"}
|
|
110
102
|
|
|
111
103
|
@provides(key=BaseInterface)
|
|
112
104
|
def create_base_interface(self):
|
|
@@ -115,23 +107,25 @@ class ServiceFactory:
|
|
|
115
107
|
"""
|
|
116
108
|
)
|
|
117
109
|
|
|
118
|
-
#
|
|
119
|
-
# used to test auto-exclude of the caller (to prevent re-entrancy).
|
|
110
|
+
# Module that triggers re-entrant access: init() + get() at import-time
|
|
120
111
|
(project_root / "entry.py").write_text(
|
|
121
112
|
"""
|
|
122
113
|
import pico_ioc
|
|
123
114
|
import test_project
|
|
115
|
+
from test_project.services.components import SimpleService
|
|
124
116
|
|
|
125
|
-
#
|
|
126
|
-
#
|
|
117
|
+
# This runs at import-time when the scanner imports this module.
|
|
118
|
+
# init() returns the (global) container set by the outer scan,
|
|
119
|
+
# then get() is attempted while scanning is still in progress -> guard should raise.
|
|
127
120
|
ioc = pico_ioc.init(test_project)
|
|
121
|
+
ioc.get(SimpleService) # should raise RuntimeError due to re-entrant access during scan
|
|
128
122
|
"""
|
|
129
123
|
)
|
|
130
124
|
|
|
131
|
-
# Yield
|
|
125
|
+
# Yield root package name used by pico_ioc.init()
|
|
132
126
|
yield "test_project"
|
|
133
127
|
|
|
134
|
-
# Teardown
|
|
128
|
+
# Teardown
|
|
135
129
|
sys.path.pop(0)
|
|
136
130
|
pico_ioc._container = None
|
|
137
131
|
mods_to_del = [m for m in list(sys.modules.keys()) if m == "test_project" or m.startswith("test_project.")]
|
|
@@ -153,10 +147,7 @@ def test_simple_component_retrieval(test_project):
|
|
|
153
147
|
|
|
154
148
|
|
|
155
149
|
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
|
-
"""
|
|
150
|
+
"""Type-hinted dependency resolves by TYPE when no NAME-bound provider exists."""
|
|
160
151
|
from test_project.services.components import SimpleService, AnotherService
|
|
161
152
|
|
|
162
153
|
container = pico_ioc.init(test_project)
|
|
@@ -167,9 +158,7 @@ def test_dependency_injection_by_type_hint(test_project):
|
|
|
167
158
|
|
|
168
159
|
|
|
169
160
|
def test_components_are_singletons_by_default(test_project):
|
|
170
|
-
"""
|
|
171
|
-
Providers bound by the scanner are singletons: get() returns the same instance.
|
|
172
|
-
"""
|
|
161
|
+
"""Providers registered by the scanner behave as singletons."""
|
|
173
162
|
from test_project.services.components import SimpleService
|
|
174
163
|
|
|
175
164
|
container = pico_ioc.init(test_project)
|
|
@@ -181,9 +170,7 @@ def test_components_are_singletons_by_default(test_project):
|
|
|
181
170
|
|
|
182
171
|
|
|
183
172
|
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
|
-
"""
|
|
173
|
+
"""Requesting an unknown key raises NameError."""
|
|
187
174
|
container = pico_ioc.init(test_project)
|
|
188
175
|
|
|
189
176
|
class Unregistered: ...
|
|
@@ -192,50 +179,36 @@ def test_get_unregistered_component_raises_error(test_project):
|
|
|
192
179
|
|
|
193
180
|
|
|
194
181
|
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
|
-
"""
|
|
182
|
+
"""Factory-provided component is retrievable by key; proxy behaves like real value."""
|
|
198
183
|
container = pico_ioc.init(test_project)
|
|
199
184
|
svc = container.get("complex_service")
|
|
200
|
-
|
|
201
|
-
# Proxy must behave like the real string for equality
|
|
202
185
|
assert svc == "This is a complex service"
|
|
203
186
|
|
|
204
187
|
|
|
205
188
|
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
|
-
"""
|
|
189
|
+
"""LazyProxy creates real object on first use and remains singleton per key."""
|
|
210
190
|
from test_project.services.factories import CREATION_COUNTER
|
|
211
191
|
|
|
212
192
|
container = pico_ioc.init(test_project)
|
|
213
|
-
|
|
214
193
|
assert CREATION_COUNTER["value"] == 0
|
|
215
194
|
|
|
216
195
|
proxy = container.get("complex_service")
|
|
217
|
-
# Accessing attributes/methods of the proxy should trigger creation exactly once
|
|
218
196
|
assert CREATION_COUNTER["value"] == 0
|
|
219
197
|
up = proxy.upper()
|
|
220
198
|
assert up == "THIS IS A COMPLEX SERVICE"
|
|
221
199
|
assert CREATION_COUNTER["value"] == 1
|
|
222
200
|
|
|
223
|
-
# Re-accessing via the same proxy does not create again
|
|
224
201
|
_ = proxy.lower()
|
|
225
202
|
assert CREATION_COUNTER["value"] == 1
|
|
226
203
|
|
|
227
|
-
# Getting the same key again should return the same singleton instance (no extra creations)
|
|
228
204
|
again = container.get("complex_service")
|
|
229
|
-
assert again is proxy
|
|
205
|
+
assert again is proxy
|
|
230
206
|
_ = again.strip()
|
|
231
207
|
assert CREATION_COUNTER["value"] == 1
|
|
232
208
|
|
|
233
209
|
|
|
234
210
|
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
|
-
"""
|
|
211
|
+
"""Component registered by custom name is retrievable by that name, not by class."""
|
|
239
212
|
from test_project.services.components import CustomNameService
|
|
240
213
|
|
|
241
214
|
container = pico_ioc.init(test_project)
|
|
@@ -247,27 +220,20 @@ def test_component_with_custom_name(test_project):
|
|
|
247
220
|
|
|
248
221
|
|
|
249
222
|
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
|
-
"""
|
|
223
|
+
"""If NAME and TYPE providers exist, NAME must win."""
|
|
254
224
|
from test_project.services.components import NeedsNameVsType
|
|
255
225
|
from test_project.services.factories import FAST_COUNTER, BASE_COUNTER
|
|
256
226
|
|
|
257
227
|
container = pico_ioc.init(test_project)
|
|
258
228
|
comp = container.get(NeedsNameVsType)
|
|
259
229
|
|
|
260
|
-
# "fast_model" name must win → uses the fast provider
|
|
261
230
|
assert comp.model == {"who": "fast"}
|
|
262
231
|
assert FAST_COUNTER["value"] == 1
|
|
263
|
-
# Base provider should NOT be used for this resolution
|
|
264
232
|
assert BASE_COUNTER["value"] == 0
|
|
265
233
|
|
|
266
234
|
|
|
267
235
|
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
|
-
"""
|
|
236
|
+
"""Ctor param without type hint resolves strictly by NAME."""
|
|
271
237
|
from test_project.services.components import NeedsByName
|
|
272
238
|
from test_project.services.factories import FAST_COUNTER
|
|
273
239
|
|
|
@@ -279,25 +245,19 @@ def test_resolution_by_name_only(test_project):
|
|
|
279
245
|
|
|
280
246
|
|
|
281
247
|
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
|
-
"""
|
|
248
|
+
"""If neither NAME nor exact TYPE match, fallback via TYPE MRO."""
|
|
286
249
|
from test_project.services.components import NeedsTypeFallback
|
|
287
250
|
from test_project.services.factories import BASE_COUNTER
|
|
288
251
|
|
|
289
252
|
container = pico_ioc.init(test_project)
|
|
290
253
|
comp = container.get(NeedsTypeFallback)
|
|
291
254
|
|
|
292
|
-
# Resolved via MRO to BaseInterface provider
|
|
293
255
|
assert comp.impl == {"who": "base"}
|
|
294
256
|
assert BASE_COUNTER["value"] == 1
|
|
295
257
|
|
|
296
258
|
|
|
297
259
|
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
|
-
"""
|
|
260
|
+
"""Missing dep across NAME/TYPE/MRO raises NameError."""
|
|
301
261
|
from test_project.services.components import MissingDep
|
|
302
262
|
|
|
303
263
|
container = pico_ioc.init(test_project)
|
|
@@ -305,17 +265,23 @@ def test_missing_dependency_raises_clear_error(test_project):
|
|
|
305
265
|
container.get(MissingDep)
|
|
306
266
|
|
|
307
267
|
|
|
308
|
-
|
|
309
|
-
not hasattr(pico_ioc, "init"),
|
|
310
|
-
reason="init not available"
|
|
311
|
-
)
|
|
312
|
-
def test_auto_exclude_caller_prevents_reentrant_scan(test_project):
|
|
268
|
+
def test_reentrant_access_is_blocked_and_container_still_initializes(test_project, caplog):
|
|
313
269
|
"""
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
270
|
+
Importing a module that calls init() and then container.get() at import-time should:
|
|
271
|
+
- raise a RuntimeError from the guard (caught as a warning by the scanner),
|
|
272
|
+
- NOT prevent the container from finishing initialization,
|
|
273
|
+
- allow normal component retrieval afterwards.
|
|
317
274
|
"""
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
275
|
+
caplog.set_level(logging.INFO)
|
|
276
|
+
|
|
277
|
+
container = pico_ioc.init(test_project)
|
|
278
|
+
|
|
279
|
+
assert any(
|
|
280
|
+
"re-entrant container access during scan" in rec.message
|
|
281
|
+
for rec in caplog.records
|
|
282
|
+
), "Expected a warning about re-entrant access during scan"
|
|
283
|
+
|
|
284
|
+
from test_project.services.components import SimpleService
|
|
285
|
+
svc = container.get(SimpleService)
|
|
286
|
+
assert isinstance(svc, SimpleService)
|
|
321
287
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = '0.2.0'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|