anydi 0.67.2__py3-none-any.whl → 0.69.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.
- anydi/_cli.py +80 -0
- anydi/_container.py +697 -263
- anydi/_context.py +14 -14
- anydi/_decorators.py +115 -8
- anydi/_graph.py +217 -0
- anydi/_injector.py +12 -10
- anydi/_marker.py +11 -13
- anydi/_provider.py +46 -8
- anydi/_resolver.py +205 -159
- anydi/_scanner.py +46 -7
- anydi/ext/fastapi.py +1 -1
- anydi/ext/faststream.py +1 -1
- anydi/ext/pydantic_settings.py +3 -3
- anydi/ext/pytest_plugin.py +125 -380
- anydi/ext/typer.py +4 -4
- {anydi-0.67.2.dist-info → anydi-0.69.0.dist-info}/METADATA +1 -1
- anydi-0.69.0.dist-info/RECORD +29 -0
- {anydi-0.67.2.dist-info → anydi-0.69.0.dist-info}/entry_points.txt +3 -0
- anydi-0.67.2.dist-info/RECORD +0 -27
- {anydi-0.67.2.dist-info → anydi-0.69.0.dist-info}/WHEEL +0 -0
anydi/ext/pytest_plugin.py
CHANGED
|
@@ -3,34 +3,24 @@ from __future__ import annotations
|
|
|
3
3
|
import importlib.util
|
|
4
4
|
import inspect
|
|
5
5
|
import logging
|
|
6
|
-
|
|
7
|
-
from
|
|
6
|
+
import warnings
|
|
7
|
+
from collections.abc import Generator
|
|
8
|
+
from typing import TYPE_CHECKING, Annotated, Any, cast, get_args, get_origin
|
|
8
9
|
|
|
9
10
|
import pytest
|
|
10
11
|
from anyio.pytest_plugin import extract_backend_and_options, get_runner
|
|
11
12
|
from typing_extensions import get_annotations
|
|
12
13
|
|
|
13
14
|
from anydi import Container, import_container
|
|
15
|
+
from anydi._marker import is_marker
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from _pytest.fixtures import SubRequest
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
_INJECTED_FIXTURES: dict[str, dict[str, Any]] = {}
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
def pytest_addoption(parser: pytest.Parser) -> None:
|
|
22
|
-
parser.addini(
|
|
23
|
-
"anydi_autoinject",
|
|
24
|
-
help="Automatically inject dependencies into all test functions",
|
|
25
|
-
type="bool",
|
|
26
|
-
default=False,
|
|
27
|
-
)
|
|
28
|
-
parser.addini(
|
|
29
|
-
"anydi_inject_all",
|
|
30
|
-
help="Deprecated: use 'anydi_autoinject' instead",
|
|
31
|
-
type="bool",
|
|
32
|
-
default=False,
|
|
33
|
-
)
|
|
34
24
|
parser.addini(
|
|
35
25
|
"anydi_container",
|
|
36
26
|
help=(
|
|
@@ -41,144 +31,30 @@ def pytest_addoption(parser: pytest.Parser) -> None:
|
|
|
41
31
|
default=None,
|
|
42
32
|
)
|
|
43
33
|
parser.addini(
|
|
44
|
-
"
|
|
45
|
-
help=
|
|
46
|
-
"Enable dependency injection into fixtures marked with @pytest.mark.inject"
|
|
47
|
-
),
|
|
34
|
+
"anydi_autoinject",
|
|
35
|
+
help="Automatically inject dependencies into all test functions",
|
|
48
36
|
type="bool",
|
|
49
|
-
default=
|
|
37
|
+
default=True,
|
|
50
38
|
)
|
|
51
39
|
|
|
52
40
|
|
|
53
41
|
def pytest_configure(config: pytest.Config) -> None:
|
|
54
42
|
config.addinivalue_line(
|
|
55
43
|
"markers",
|
|
56
|
-
"inject: mark test as needing dependency injection",
|
|
44
|
+
"inject: mark test as needing dependency injection (deprecated)",
|
|
57
45
|
)
|
|
58
46
|
|
|
59
|
-
# Enable fixture injection if configured
|
|
60
|
-
inject_fixtures_enabled = cast(bool, config.getini("anydi_fixture_inject_enabled"))
|
|
61
|
-
if inject_fixtures_enabled:
|
|
62
|
-
autoinject = cast(bool, config.getini("anydi_autoinject"))
|
|
63
|
-
inject_all = cast(bool, config.getini("anydi_inject_all"))
|
|
64
|
-
_patch_pytest_fixtures(autoinject=autoinject or inject_all)
|
|
65
|
-
logger.debug(
|
|
66
|
-
"Fixture injection enabled via anydi_fixture_inject_enabled config"
|
|
67
|
-
)
|
|
68
|
-
|
|
69
47
|
|
|
70
48
|
@pytest.hookimpl(hookwrapper=True)
|
|
71
|
-
def pytest_fixture_setup(
|
|
72
|
-
fixturedef: pytest.FixtureDef[Any],
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
"""Inject dependencies into fixtures marked with @pytest.mark.inject."""
|
|
76
|
-
# Check if this fixture has injection metadata
|
|
77
|
-
fixture_name = fixturedef.argname
|
|
78
|
-
if fixture_name not in _INJECTED_FIXTURES:
|
|
79
|
-
yield
|
|
80
|
-
return
|
|
81
|
-
|
|
82
|
-
# Get the metadata
|
|
83
|
-
fixture_info = _INJECTED_FIXTURES[fixture_name]
|
|
84
|
-
original_func = fixture_info["func"]
|
|
85
|
-
parameters: list[tuple[str, Any]] = fixture_info["parameters"]
|
|
86
|
-
|
|
87
|
-
# Get the container
|
|
88
|
-
try:
|
|
89
|
-
container = cast(Container, request.getfixturevalue("container"))
|
|
90
|
-
except pytest.FixtureLookupError:
|
|
91
|
-
yield
|
|
92
|
-
return
|
|
93
|
-
|
|
94
|
-
resolvable_params = _select_resolvable_parameters(container, parameters)
|
|
95
|
-
|
|
96
|
-
if not resolvable_params:
|
|
97
|
-
yield
|
|
98
|
-
return
|
|
99
|
-
|
|
100
|
-
target_name = f"fixture '{fixture_name}'"
|
|
101
|
-
|
|
102
|
-
def _prepare_sync_call_kwargs(kwargs: dict[str, Any]) -> dict[str, Any]:
|
|
103
|
-
combined_kwargs = dict(kwargs)
|
|
104
|
-
combined_kwargs.update(
|
|
105
|
-
_resolve_dependencies_sync(container, resolvable_params, target=target_name)
|
|
106
|
-
)
|
|
107
|
-
return combined_kwargs
|
|
108
|
-
|
|
109
|
-
async def _prepare_async_call_kwargs(kwargs: dict[str, Any]) -> dict[str, Any]:
|
|
110
|
-
combined_kwargs = dict(kwargs)
|
|
111
|
-
combined_kwargs.update(
|
|
112
|
-
await _resolve_dependencies_async(
|
|
113
|
-
container, resolvable_params, target=target_name
|
|
114
|
-
)
|
|
115
|
-
)
|
|
116
|
-
return combined_kwargs
|
|
117
|
-
|
|
118
|
-
def _ensure_anyio_backend() -> tuple[str, dict[str, Any]]:
|
|
119
|
-
try:
|
|
120
|
-
backend = request.getfixturevalue("anyio_backend")
|
|
121
|
-
except pytest.FixtureLookupError as exc: # pragma: no cover - defensive
|
|
122
|
-
msg = (
|
|
123
|
-
"To run async fixtures with AnyDI, please configure the `anyio` pytest "
|
|
124
|
-
"plugin (provide the `anyio_backend` fixture)."
|
|
125
|
-
)
|
|
126
|
-
pytest.fail(msg, pytrace=False)
|
|
127
|
-
raise RuntimeError from exc # Unreachable but satisfies type checkers
|
|
128
|
-
|
|
129
|
-
return extract_backend_and_options(backend)
|
|
130
|
-
|
|
131
|
-
# Replace the fixture function with one that mirrors the original's type and
|
|
132
|
-
# injects dependencies before delegating to the user-defined function.
|
|
133
|
-
original_fixture_func = fixturedef.func
|
|
134
|
-
|
|
135
|
-
if inspect.isasyncgenfunction(original_func):
|
|
136
|
-
|
|
137
|
-
def asyncgen_wrapper(*args: Any, **kwargs: Any) -> Iterator[Any]:
|
|
138
|
-
backend_name, backend_options = _ensure_anyio_backend()
|
|
139
|
-
|
|
140
|
-
async def _fixture() -> Any:
|
|
141
|
-
call_kwargs = await _prepare_async_call_kwargs(kwargs)
|
|
142
|
-
async for value in original_func(**call_kwargs):
|
|
143
|
-
yield value
|
|
144
|
-
|
|
145
|
-
with get_runner(backend_name, backend_options) as runner:
|
|
146
|
-
yield from runner.run_asyncgen_fixture(_fixture, {}) # type: ignore
|
|
147
|
-
|
|
148
|
-
fixturedef.func = asyncgen_wrapper # type: ignore[misc]
|
|
149
|
-
elif inspect.iscoroutinefunction(original_func):
|
|
150
|
-
|
|
151
|
-
def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
152
|
-
backend_name, backend_options = _ensure_anyio_backend()
|
|
153
|
-
|
|
154
|
-
async def _fixture() -> Any:
|
|
155
|
-
call_kwargs = await _prepare_async_call_kwargs(kwargs)
|
|
156
|
-
return await original_func(**call_kwargs)
|
|
157
|
-
|
|
158
|
-
with get_runner(backend_name, backend_options) as runner:
|
|
159
|
-
return runner.run_fixture(_fixture, {})
|
|
160
|
-
|
|
161
|
-
fixturedef.func = async_wrapper # type: ignore[misc]
|
|
162
|
-
elif inspect.isgeneratorfunction(original_func):
|
|
163
|
-
|
|
164
|
-
def generator_wrapper(*args: Any, **kwargs: Any) -> Iterator[Any]:
|
|
165
|
-
call_kwargs = _prepare_sync_call_kwargs(kwargs)
|
|
166
|
-
yield from original_func(**call_kwargs)
|
|
167
|
-
|
|
168
|
-
fixturedef.func = generator_wrapper # type: ignore[misc]
|
|
169
|
-
else:
|
|
170
|
-
|
|
171
|
-
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
172
|
-
call_kwargs = _prepare_sync_call_kwargs(kwargs)
|
|
173
|
-
return original_func(**call_kwargs)
|
|
174
|
-
|
|
175
|
-
fixturedef.func = sync_wrapper # type: ignore[misc]
|
|
176
|
-
|
|
177
|
-
# Let pytest execute the modified fixture
|
|
49
|
+
def pytest_fixture_setup(
|
|
50
|
+
fixturedef: pytest.FixtureDef[Any], request: SubRequest
|
|
51
|
+
) -> Generator[None]:
|
|
52
|
+
"""Automatically enable test mode on the container fixture."""
|
|
178
53
|
yield
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
54
|
+
if fixturedef.argname == "container" and fixturedef.cached_result is not None:
|
|
55
|
+
container = fixturedef.cached_result[0]
|
|
56
|
+
if isinstance(container, Container):
|
|
57
|
+
container.enable_test_mode()
|
|
182
58
|
|
|
183
59
|
|
|
184
60
|
@pytest.fixture(scope="session")
|
|
@@ -187,118 +63,133 @@ def container(request: pytest.FixtureRequest) -> Container:
|
|
|
187
63
|
return _find_container(request)
|
|
188
64
|
|
|
189
65
|
|
|
190
|
-
@pytest.fixture
|
|
191
|
-
def _anydi_should_inject(request: pytest.FixtureRequest) -> bool:
|
|
192
|
-
marker = request.node.get_closest_marker("inject")
|
|
193
|
-
|
|
194
|
-
# Check new config option first
|
|
195
|
-
autoinject = cast(bool, request.config.getini("anydi_autoinject"))
|
|
196
|
-
|
|
197
|
-
# Check deprecated option for backward compatibility
|
|
198
|
-
inject_all = cast(bool, request.config.getini("anydi_inject_all"))
|
|
199
|
-
if inject_all:
|
|
200
|
-
logger.warning(
|
|
201
|
-
"Configuration option 'anydi_inject_all' is deprecated. "
|
|
202
|
-
"Please use 'anydi_autoinject' instead."
|
|
203
|
-
)
|
|
204
|
-
|
|
205
|
-
return marker is not None or autoinject or inject_all
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
@pytest.fixture
|
|
209
|
-
def _anydi_injected_parameter_iterator(
|
|
210
|
-
request: pytest.FixtureRequest,
|
|
211
|
-
) -> Callable[[], Iterator[tuple[str, Any]]]:
|
|
212
|
-
fixturenames = set(request.node._fixtureinfo.initialnames) - set(
|
|
213
|
-
request.node._fixtureinfo.name2fixturedefs.keys()
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
def _iterator() -> Iterator[tuple[str, Any]]:
|
|
217
|
-
for name, annotation in _iter_injectable_parameters(request.function):
|
|
218
|
-
if name not in fixturenames:
|
|
219
|
-
continue
|
|
220
|
-
yield name, annotation
|
|
221
|
-
|
|
222
|
-
return _iterator
|
|
223
|
-
|
|
224
|
-
|
|
225
66
|
@pytest.fixture(autouse=True)
|
|
226
|
-
def _anydi_inject(
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
_anydi_injected_parameter_iterator: Callable[[], Iterator[tuple[str, Any]]],
|
|
230
|
-
) -> None:
|
|
231
|
-
"""Inject dependencies into the test function."""
|
|
232
|
-
|
|
233
|
-
if inspect.iscoroutinefunction(request.function) or not _anydi_should_inject:
|
|
67
|
+
def _anydi_inject(request: pytest.FixtureRequest) -> None:
|
|
68
|
+
"""Inject dependencies into sync test functions."""
|
|
69
|
+
if inspect.iscoroutinefunction(request.function):
|
|
234
70
|
return
|
|
235
71
|
|
|
236
|
-
parameters =
|
|
72
|
+
parameters, uses_deprecated = _get_injectable_params(request)
|
|
237
73
|
if not parameters:
|
|
238
74
|
return
|
|
239
75
|
|
|
240
76
|
container = cast(Container, request.getfixturevalue("container"))
|
|
241
|
-
resolvable = _select_resolvable_parameters(container, parameters)
|
|
242
|
-
if not resolvable:
|
|
243
|
-
return
|
|
244
77
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
78
|
+
warned = False
|
|
79
|
+
for name, dependency_type in parameters:
|
|
80
|
+
if not container.has_provider_for(dependency_type):
|
|
81
|
+
continue
|
|
82
|
+
if uses_deprecated and not warned:
|
|
83
|
+
_warn_deprecated_marker(request.node.name)
|
|
84
|
+
warned = True
|
|
85
|
+
try:
|
|
86
|
+
request.node.funcargs[name] = container.resolve(dependency_type)
|
|
87
|
+
except Exception: # pragma: no cover
|
|
88
|
+
logger.warning("Failed to resolve '%s' for %s", name, request.node.nodeid)
|
|
250
89
|
|
|
251
90
|
|
|
252
91
|
@pytest.fixture(autouse=True)
|
|
253
|
-
def _anydi_ainject(
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
)
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
or not _anydi_should_inject
|
|
263
|
-
):
|
|
92
|
+
def _anydi_ainject(request: pytest.FixtureRequest) -> None:
|
|
93
|
+
"""Inject dependencies into async test functions."""
|
|
94
|
+
if not inspect.iscoroutinefunction(
|
|
95
|
+
request.function
|
|
96
|
+
) and not inspect.isasyncgenfunction(request.function):
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
parameters, uses_deprecated = _get_injectable_params(request)
|
|
100
|
+
if not parameters:
|
|
264
101
|
return
|
|
265
102
|
|
|
266
|
-
# Skip if the anyio backend is not available
|
|
267
103
|
if "anyio_backend" not in request.fixturenames:
|
|
268
|
-
|
|
104
|
+
pytest.fail(
|
|
269
105
|
"To run async test functions with `anyio`, "
|
|
270
106
|
"please configure the `anyio` pytest plugin.\n"
|
|
271
|
-
"See: https://anyio.readthedocs.io/en/stable/testing.html"
|
|
107
|
+
"See: https://anyio.readthedocs.io/en/stable/testing.html",
|
|
108
|
+
pytrace=False,
|
|
272
109
|
)
|
|
273
|
-
pytest.fail(msg, pytrace=False)
|
|
274
|
-
|
|
275
|
-
parameters = list(_anydi_injected_parameter_iterator())
|
|
276
|
-
if not parameters:
|
|
277
|
-
return
|
|
278
110
|
|
|
279
111
|
container = cast(Container, request.getfixturevalue("container"))
|
|
280
|
-
resolvable = _select_resolvable_parameters(container, parameters)
|
|
281
|
-
if not resolvable:
|
|
282
|
-
return
|
|
283
112
|
|
|
284
|
-
async def
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
113
|
+
async def _resolve() -> None:
|
|
114
|
+
warned = False
|
|
115
|
+
for name, dependency_type in parameters:
|
|
116
|
+
if not container.has_provider_for(dependency_type):
|
|
117
|
+
continue
|
|
118
|
+
if uses_deprecated and not warned:
|
|
119
|
+
_warn_deprecated_marker(request.node.name)
|
|
120
|
+
warned = True
|
|
121
|
+
try:
|
|
122
|
+
request.node.funcargs[name] = await container.aresolve(dependency_type)
|
|
123
|
+
except Exception: # pragma: no cover
|
|
124
|
+
logger.warning(
|
|
125
|
+
"Failed to resolve '%s' for %s", name, request.node.nodeid
|
|
126
|
+
)
|
|
290
127
|
|
|
291
128
|
anyio_backend = request.getfixturevalue("anyio_backend")
|
|
292
129
|
backend_name, backend_options = extract_backend_and_options(anyio_backend)
|
|
293
130
|
|
|
294
131
|
with get_runner(backend_name, backend_options) as runner:
|
|
295
|
-
runner.run_fixture(
|
|
132
|
+
runner.run_fixture(_resolve, {})
|
|
296
133
|
|
|
297
134
|
|
|
298
|
-
def
|
|
299
|
-
|
|
135
|
+
def _get_injectable_params(
|
|
136
|
+
request: pytest.FixtureRequest,
|
|
137
|
+
) -> tuple[list[tuple[str, Any]], bool]:
|
|
138
|
+
"""Get injectable parameters for a test function.
|
|
139
|
+
|
|
140
|
+
Returns (parameters, uses_deprecated_marker) tuple.
|
|
141
|
+
"""
|
|
142
|
+
fixture_names = set(request.node._fixtureinfo.initialnames) - set(
|
|
143
|
+
request.node._fixtureinfo.name2fixturedefs.keys()
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
marker = request.node.get_closest_marker("inject")
|
|
147
|
+
autoinject = cast(bool, request.config.getini("anydi_autoinject"))
|
|
148
|
+
|
|
149
|
+
has_any_explicit = False
|
|
150
|
+
explicit_params: list[tuple[str, Any]] = []
|
|
151
|
+
all_params: list[tuple[str, Any]] = []
|
|
152
|
+
|
|
153
|
+
annotations = get_annotations(request.function, eval_str=True)
|
|
154
|
+
|
|
155
|
+
for name, annotation in annotations.items():
|
|
156
|
+
if name in ("request", "return"):
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
dependency_type, is_explicit = _extract_dependency_type(annotation)
|
|
160
|
+
|
|
161
|
+
if is_explicit:
|
|
162
|
+
has_any_explicit = True
|
|
163
|
+
explicit_params.append((name, dependency_type))
|
|
164
|
+
elif name in fixture_names:
|
|
165
|
+
all_params.append((name, dependency_type))
|
|
166
|
+
|
|
167
|
+
# Priority: explicit markers > deprecated @pytest.mark.inject > autoinject
|
|
168
|
+
if has_any_explicit:
|
|
169
|
+
return explicit_params, False
|
|
170
|
+
if marker is not None:
|
|
171
|
+
return all_params, True
|
|
172
|
+
if autoinject:
|
|
173
|
+
return all_params, False
|
|
174
|
+
return [], False
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _extract_dependency_type(annotation: Any) -> tuple[Any, bool]:
|
|
178
|
+
"""Extract the actual type and whether it has an explicit injection marker.
|
|
179
|
+
|
|
180
|
+
Handles Provide[T] and Annotated[T, Inject()].
|
|
181
|
+
Returns (unwrapped_type, is_explicit).
|
|
182
|
+
"""
|
|
183
|
+
if get_origin(annotation) is Annotated:
|
|
184
|
+
args = get_args(annotation)
|
|
185
|
+
for arg in args[1:]:
|
|
186
|
+
if is_marker(arg):
|
|
187
|
+
return args[0], True
|
|
188
|
+
return annotation, False
|
|
300
189
|
|
|
301
|
-
|
|
190
|
+
|
|
191
|
+
def _find_container(request: pytest.FixtureRequest) -> Container:
|
|
192
|
+
"""Find container from config or auto-detection."""
|
|
302
193
|
container_path = cast(str | None, request.config.getini("anydi_container"))
|
|
303
194
|
if container_path:
|
|
304
195
|
try:
|
|
@@ -309,12 +200,10 @@ def _find_container(request: pytest.FixtureRequest) -> Container:
|
|
|
309
200
|
f"'anydi_container={container_path}': {exc}"
|
|
310
201
|
) from exc
|
|
311
202
|
|
|
312
|
-
# Detect pytest-django + anydi_django availability
|
|
313
203
|
pluginmanager = request.config.pluginmanager
|
|
314
204
|
if pluginmanager.hasplugin("django") and importlib.util.find_spec("anydi_django"):
|
|
315
205
|
return import_container("anydi_django.container")
|
|
316
206
|
|
|
317
|
-
# Neither fixture nor config found
|
|
318
207
|
raise pytest.FixtureLookupError(
|
|
319
208
|
None,
|
|
320
209
|
request,
|
|
@@ -324,154 +213,10 @@ def _find_container(request: pytest.FixtureRequest) -> Container:
|
|
|
324
213
|
)
|
|
325
214
|
|
|
326
215
|
|
|
327
|
-
def
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
def should_process(func: Callable[..., Any]) -> bool:
|
|
335
|
-
has_inject_marker = False
|
|
336
|
-
if hasattr(func, "pytestmark"):
|
|
337
|
-
markers = getattr(func, "pytestmark", [])
|
|
338
|
-
if not isinstance(markers, list):
|
|
339
|
-
markers = [markers]
|
|
340
|
-
|
|
341
|
-
has_inject_marker = any(
|
|
342
|
-
marker.name == "inject"
|
|
343
|
-
for marker in markers
|
|
344
|
-
if hasattr(marker, "name")
|
|
345
|
-
)
|
|
346
|
-
|
|
347
|
-
return autoinject or has_inject_marker
|
|
348
|
-
|
|
349
|
-
def register_fixture(func: Callable[..., Any]) -> Callable[..., Any] | None:
|
|
350
|
-
if not should_process(func):
|
|
351
|
-
return None
|
|
352
|
-
|
|
353
|
-
parameters = list(_iter_injectable_parameters(func))
|
|
354
|
-
if not parameters:
|
|
355
|
-
return None
|
|
356
|
-
|
|
357
|
-
sig = inspect.signature(func, eval_str=True)
|
|
358
|
-
has_request_param = "request" in sig.parameters
|
|
359
|
-
|
|
360
|
-
if has_request_param:
|
|
361
|
-
|
|
362
|
-
def wrapper_with_request(request: Any) -> Any:
|
|
363
|
-
return func
|
|
364
|
-
|
|
365
|
-
wrapper_func = wrapper_with_request
|
|
366
|
-
else:
|
|
367
|
-
|
|
368
|
-
def wrapper_no_request() -> Any:
|
|
369
|
-
return func
|
|
370
|
-
|
|
371
|
-
wrapper_func = wrapper_no_request
|
|
372
|
-
|
|
373
|
-
wrapper_func.__name__ = func.__name__
|
|
374
|
-
wrapper_func.__annotations__ = {}
|
|
375
|
-
|
|
376
|
-
fixture_name = func.__name__
|
|
377
|
-
_INJECTED_FIXTURES[fixture_name] = {
|
|
378
|
-
"func": func,
|
|
379
|
-
"parameters": parameters,
|
|
380
|
-
}
|
|
381
|
-
logger.debug(
|
|
382
|
-
"Registered injectable fixture '%s' with params: %s",
|
|
383
|
-
fixture_name,
|
|
384
|
-
[name for name, _ in parameters],
|
|
385
|
-
)
|
|
386
|
-
|
|
387
|
-
return wrapper_func
|
|
388
|
-
|
|
389
|
-
# Handle both @pytest.fixture and @pytest.fixture() usage
|
|
390
|
-
if len(args) == 1 and callable(args[0]) and not kwargs:
|
|
391
|
-
func = args[0]
|
|
392
|
-
wrapper_func = register_fixture(func)
|
|
393
|
-
if wrapper_func:
|
|
394
|
-
return original_fixture_decorator(wrapper_func)
|
|
395
|
-
|
|
396
|
-
return original_fixture_decorator(func)
|
|
397
|
-
else:
|
|
398
|
-
|
|
399
|
-
def decorator(func: Callable[..., Any]) -> Any:
|
|
400
|
-
wrapper_func = register_fixture(func)
|
|
401
|
-
if wrapper_func:
|
|
402
|
-
return original_fixture_decorator(*args, **kwargs)(wrapper_func)
|
|
403
|
-
|
|
404
|
-
return original_fixture_decorator(*args, **kwargs)(func)
|
|
405
|
-
|
|
406
|
-
return decorator
|
|
407
|
-
|
|
408
|
-
# Replace pytest.fixture
|
|
409
|
-
pytest.fixture = patched_fixture # type: ignore[assignment]
|
|
410
|
-
# Also patch _pytest.fixtures.fixture
|
|
411
|
-
import _pytest.fixtures
|
|
412
|
-
|
|
413
|
-
_pytest.fixtures.fixture = patched_fixture # type: ignore[assignment]
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
def _iter_injectable_parameters(
|
|
417
|
-
func: Callable[..., Any], *, skip: tuple[str, ...] = ("request",)
|
|
418
|
-
) -> Iterator[tuple[str, Any]]:
|
|
419
|
-
annotations = get_annotations(func, eval_str=True)
|
|
420
|
-
skip_names = set(skip)
|
|
421
|
-
for name, annotation in annotations.items():
|
|
422
|
-
if name in skip_names or name == "return":
|
|
423
|
-
continue
|
|
424
|
-
yield name, annotation
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
def _select_resolvable_parameters(
|
|
428
|
-
container: Container,
|
|
429
|
-
parameters: Iterator[tuple[str, Any]] | list[tuple[str, Any]],
|
|
430
|
-
) -> list[tuple[str, Any]]:
|
|
431
|
-
return [
|
|
432
|
-
(name, annotation)
|
|
433
|
-
for name, annotation in parameters
|
|
434
|
-
if container.has_provider_for(annotation)
|
|
435
|
-
]
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
def _resolve_dependencies_sync(
|
|
439
|
-
container: Container,
|
|
440
|
-
parameters: list[tuple[str, Any]],
|
|
441
|
-
*,
|
|
442
|
-
target: str,
|
|
443
|
-
) -> dict[str, Any]:
|
|
444
|
-
resolved: dict[str, Any] = {}
|
|
445
|
-
for param_name, annotation in parameters:
|
|
446
|
-
try:
|
|
447
|
-
resolved[param_name] = container.resolve(annotation)
|
|
448
|
-
logger.debug("Resolved %s=%s for %s", param_name, annotation, target)
|
|
449
|
-
except Exception as exc: # pragma: no cover - defensive logging
|
|
450
|
-
logger.warning(
|
|
451
|
-
"Failed to resolve dependency for '%s' on %s.",
|
|
452
|
-
param_name,
|
|
453
|
-
target,
|
|
454
|
-
exc_info=exc,
|
|
455
|
-
)
|
|
456
|
-
return resolved
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
async def _resolve_dependencies_async(
|
|
460
|
-
container: Container,
|
|
461
|
-
parameters: list[tuple[str, Any]],
|
|
462
|
-
*,
|
|
463
|
-
target: str,
|
|
464
|
-
) -> dict[str, Any]:
|
|
465
|
-
resolved: dict[str, Any] = {}
|
|
466
|
-
for param_name, annotation in parameters:
|
|
467
|
-
try:
|
|
468
|
-
resolved[param_name] = await container.aresolve(annotation)
|
|
469
|
-
logger.debug("Resolved %s=%s for async %s", param_name, annotation, target)
|
|
470
|
-
except Exception as exc: # pragma: no cover - defensive logging
|
|
471
|
-
logger.warning(
|
|
472
|
-
"Failed to resolve async dependency for '%s' on %s.",
|
|
473
|
-
param_name,
|
|
474
|
-
target,
|
|
475
|
-
exc_info=exc,
|
|
476
|
-
)
|
|
477
|
-
return resolved
|
|
216
|
+
def _warn_deprecated_marker(test_name: str) -> None:
|
|
217
|
+
warnings.warn(
|
|
218
|
+
f"Using @pytest.mark.inject on test '{test_name}' is deprecated. "
|
|
219
|
+
"Use Provide[T] instead.",
|
|
220
|
+
DeprecationWarning,
|
|
221
|
+
stacklevel=4,
|
|
222
|
+
)
|
anydi/ext/typer.py
CHANGED
|
@@ -91,17 +91,17 @@ def _process_callback(callback: Callable[..., Any], container: Container) -> Any
|
|
|
91
91
|
|
|
92
92
|
# Validate parameters and collect which ones need injection
|
|
93
93
|
for parameter in sig.parameters.values():
|
|
94
|
-
|
|
94
|
+
dependency_type, should_inject, _ = container.validate_injected_parameter(
|
|
95
95
|
parameter, call=callback
|
|
96
96
|
)
|
|
97
97
|
processed_parameter = container._injector.unwrap_parameter(parameter)
|
|
98
98
|
if should_inject:
|
|
99
99
|
injected_param_names.add(parameter.name)
|
|
100
100
|
try:
|
|
101
|
-
scopes.add(container.providers[
|
|
101
|
+
scopes.add(container.providers[dependency_type].scope)
|
|
102
102
|
except KeyError:
|
|
103
|
-
if inspect.isclass(
|
|
104
|
-
scopes.add(
|
|
103
|
+
if inspect.isclass(dependency_type) and is_provided(dependency_type):
|
|
104
|
+
scopes.add(dependency_type.__provided__["scope"])
|
|
105
105
|
else:
|
|
106
106
|
non_injected_params.append(processed_parameter)
|
|
107
107
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
anydi/__init__.py,sha256=KFX8OthKXwBuYDPCV61t-044DpJ88tAOzIxeUWRC5OA,633
|
|
2
|
+
anydi/_async_lock.py,sha256=3dwZr0KthXFYha0XKMyXf8jMmGb1lYoNC0O5w29V9ic,1104
|
|
3
|
+
anydi/_cli.py,sha256=0BhNvWPyuIGzUkDELIBm_nsEMWk7MtLi3oTvgXj5oko,2072
|
|
4
|
+
anydi/_container.py,sha256=lxUkRYU35qSiwTZYXfydElD28n2imzS81qRAAIhiHZU,46677
|
|
5
|
+
anydi/_context.py,sha256=ZQWxtBXWkrMsCk_L7K_A7-e09v5Mv9HApPH3LZ6ZF9k,3648
|
|
6
|
+
anydi/_decorators.py,sha256=7FvgmBDruv1w_iVqwloPzZwQQgyzQCklM6r9f6UVn-E,5728
|
|
7
|
+
anydi/_graph.py,sha256=X7AYsTMcGZtnIUJD5CVc1t8-t54fSI8CY_xh6ijODWk,7945
|
|
8
|
+
anydi/_injector.py,sha256=RvnPEYOgkg-WOIW1ItvVsoAZaSC9wmCnWQrfXad_86A,4507
|
|
9
|
+
anydi/_marker.py,sha256=yXSPbIVU-X-jMSawtCHWFMKke5VpWMiBRZlEH8PlUqE,3373
|
|
10
|
+
anydi/_module.py,sha256=2kN5uEXLd2Dsc58gz5IWK43wJewr_QgIVGSO3iWp798,2609
|
|
11
|
+
anydi/_provider.py,sha256=5pMXyiGwBo2j6OHaoOktLQfiX2rkCf5aYXFlFi65JlQ,2898
|
|
12
|
+
anydi/_resolver.py,sha256=CTYCjYkUXX35o2VMwvVwXyF8Kqp7h2tXituCNq6oydM,32746
|
|
13
|
+
anydi/_scanner.py,sha256=rCPbpW_5OV9MwYQdwTfceV_MlwSMPfpTTKuxdcMmV98,5804
|
|
14
|
+
anydi/_types.py,sha256=lsShY_-_CM2EFajeknAYXvLl-rHfopBT8udnK5_BtS4,1161
|
|
15
|
+
anydi/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
anydi/ext/django/__init__.py,sha256=Ve8lncLU9dPY_Vjt4zihPgsSxwAtFHACn0XvBM5JG8k,367
|
|
17
|
+
anydi/ext/fastapi.py,sha256=NowHc-z_Sw069YDv9vP98Mehum5vHsIIcqvDkRmicmc,2964
|
|
18
|
+
anydi/ext/faststream.py,sha256=_3FZ8vlgl1GVizVv_6ippvwSWu_Zor4qm6HaiAp24o4,2744
|
|
19
|
+
anydi/ext/pydantic_settings.py,sha256=y6uz0MiLtqPvRO186bIdRZVQfezLvRUfT3KvvB5fCWk,1524
|
|
20
|
+
anydi/ext/pytest_plugin.py,sha256=rLZ6NZUYklSg1rYx0FPdSvVmEY-faZTYqEY5qUWqZXk,7336
|
|
21
|
+
anydi/ext/starlette/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
+
anydi/ext/starlette/middleware.py,sha256=n_JJ7BcG2Mg2M5HwM_SBboxZ-mnnD6WWJn4khq7Bgbs,1860
|
|
23
|
+
anydi/ext/typer.py,sha256=c7HapXQfKhnLJQcHNncJAGd8jZ3crX5it6-MRCJjyPM,6268
|
|
24
|
+
anydi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
|
+
anydi/testing.py,sha256=cHg3mMScZbEep9smRqSNQ81BZMQOkyugHe8TvKdPnEg,1347
|
|
26
|
+
anydi-0.69.0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
27
|
+
anydi-0.69.0.dist-info/entry_points.txt,sha256=oDl_yEX12KlWcDzsZBTg85GG1Jl1rpiYOG4C7EJvebs,87
|
|
28
|
+
anydi-0.69.0.dist-info/METADATA,sha256=h8I4S5WOovGNeSInAJUV_9ASebz1FClByn1Ds4DcNRg,8061
|
|
29
|
+
anydi-0.69.0.dist-info/RECORD,,
|