anydi 0.56.0__py3-none-any.whl → 0.58.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/__init__.py +4 -2
- anydi/_container.py +181 -157
- anydi/_injector.py +132 -0
- anydi/_resolver.py +51 -24
- anydi/_scanner.py +52 -44
- anydi/_types.py +49 -8
- anydi/ext/fastapi.py +31 -33
- anydi/ext/faststream.py +25 -31
- anydi/ext/pydantic_settings.py +2 -1
- anydi/ext/pytest_plugin.py +380 -50
- anydi/ext/starlette/middleware.py +1 -1
- {anydi-0.56.0.dist-info → anydi-0.58.0.dist-info}/METADATA +32 -13
- anydi-0.58.0.dist-info/RECORD +25 -0
- anydi-0.56.0.dist-info/RECORD +0 -24
- {anydi-0.56.0.dist-info → anydi-0.58.0.dist-info}/WHEEL +0 -0
- {anydi-0.56.0.dist-info → anydi-0.58.0.dist-info}/entry_points.txt +0 -0
anydi/ext/pytest_plugin.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import importlib.util
|
|
3
4
|
import inspect
|
|
4
5
|
import logging
|
|
5
6
|
from collections.abc import Callable, Iterator
|
|
@@ -9,47 +10,199 @@ import pytest
|
|
|
9
10
|
from anyio.pytest_plugin import extract_backend_and_options, get_runner
|
|
10
11
|
from typing_extensions import get_annotations
|
|
11
12
|
|
|
12
|
-
from anydi import Container
|
|
13
|
+
from anydi import Container, import_container
|
|
13
14
|
|
|
14
15
|
logger = logging.getLogger(__name__)
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
config.addinivalue_line(
|
|
19
|
-
"markers",
|
|
20
|
-
"inject: mark test as needing dependency injection",
|
|
21
|
-
)
|
|
17
|
+
# Storage for fixtures with inject markers
|
|
18
|
+
_INJECTED_FIXTURES: dict[str, dict[str, Any]] = {}
|
|
22
19
|
|
|
23
20
|
|
|
24
21
|
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
|
+
)
|
|
25
28
|
parser.addini(
|
|
26
29
|
"anydi_inject_all",
|
|
27
|
-
help="
|
|
30
|
+
help="Deprecated: use 'anydi_autoinject' instead",
|
|
31
|
+
type="bool",
|
|
32
|
+
default=False,
|
|
33
|
+
)
|
|
34
|
+
parser.addini(
|
|
35
|
+
"anydi_container",
|
|
36
|
+
help=(
|
|
37
|
+
"Path to container instance or factory "
|
|
38
|
+
"(e.g., 'myapp.container:container' or 'myapp.container.container')"
|
|
39
|
+
),
|
|
40
|
+
type="string",
|
|
41
|
+
default=None,
|
|
42
|
+
)
|
|
43
|
+
parser.addini(
|
|
44
|
+
"anydi_fixture_inject_enabled",
|
|
45
|
+
help=(
|
|
46
|
+
"Enable dependency injection into fixtures marked with @pytest.mark.inject"
|
|
47
|
+
),
|
|
28
48
|
type="bool",
|
|
29
49
|
default=False,
|
|
30
50
|
)
|
|
31
51
|
|
|
32
52
|
|
|
33
|
-
|
|
53
|
+
def pytest_configure(config: pytest.Config) -> None:
|
|
54
|
+
config.addinivalue_line(
|
|
55
|
+
"markers",
|
|
56
|
+
"inject: mark test as needing dependency injection",
|
|
57
|
+
)
|
|
34
58
|
|
|
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
|
+
)
|
|
35
68
|
|
|
36
|
-
|
|
37
|
-
|
|
69
|
+
|
|
70
|
+
@pytest.hookimpl(hookwrapper=True)
|
|
71
|
+
def pytest_fixture_setup( # noqa: C901
|
|
72
|
+
fixturedef: pytest.FixtureDef[Any],
|
|
73
|
+
request: pytest.FixtureRequest,
|
|
74
|
+
) -> Iterator[None]:
|
|
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
|
|
38
88
|
try:
|
|
39
|
-
|
|
40
|
-
except pytest.FixtureLookupError
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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)
|
|
44
106
|
)
|
|
45
|
-
|
|
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
|
|
178
|
+
yield
|
|
179
|
+
|
|
180
|
+
# Restore the original function
|
|
181
|
+
fixturedef.func = original_fixture_func # type: ignore[misc]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@pytest.fixture(scope="session")
|
|
185
|
+
def container(request: pytest.FixtureRequest) -> Container:
|
|
186
|
+
"""Container fixture."""
|
|
187
|
+
return _find_container(request)
|
|
46
188
|
|
|
47
189
|
|
|
48
190
|
@pytest.fixture
|
|
49
191
|
def _anydi_should_inject(request: pytest.FixtureRequest) -> bool:
|
|
50
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
|
|
51
198
|
inject_all = cast(bool, request.config.getini("anydi_inject_all"))
|
|
52
|
-
|
|
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
|
|
53
206
|
|
|
54
207
|
|
|
55
208
|
@pytest.fixture
|
|
@@ -60,12 +213,8 @@ def _anydi_injected_parameter_iterator(
|
|
|
60
213
|
request.node._fixtureinfo.name2fixturedefs.keys()
|
|
61
214
|
)
|
|
62
215
|
|
|
63
|
-
def _iterator() -> Iterator[tuple[str,
|
|
64
|
-
for name, annotation in
|
|
65
|
-
request.function, eval_str=True
|
|
66
|
-
).items():
|
|
67
|
-
if name == "return":
|
|
68
|
-
continue
|
|
216
|
+
def _iterator() -> Iterator[tuple[str, Any]]:
|
|
217
|
+
for name, annotation in _iter_injectable_parameters(request.function):
|
|
69
218
|
if name not in fixturenames:
|
|
70
219
|
continue
|
|
71
220
|
yield name, annotation
|
|
@@ -84,20 +233,20 @@ def _anydi_inject(
|
|
|
84
233
|
if inspect.iscoroutinefunction(request.function) or not _anydi_should_inject:
|
|
85
234
|
return
|
|
86
235
|
|
|
87
|
-
|
|
88
|
-
|
|
236
|
+
parameters = list(_anydi_injected_parameter_iterator())
|
|
237
|
+
if not parameters:
|
|
238
|
+
return
|
|
89
239
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
240
|
+
container = cast(Container, request.getfixturevalue("container"))
|
|
241
|
+
resolvable = _select_resolvable_parameters(container, parameters)
|
|
242
|
+
if not resolvable:
|
|
243
|
+
return
|
|
94
244
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
)
|
|
245
|
+
resolved = _resolve_dependencies_sync(
|
|
246
|
+
container, resolvable, target=request.node.nodeid
|
|
247
|
+
)
|
|
248
|
+
for argname, value in resolved.items():
|
|
249
|
+
request.node.funcargs[argname] = value
|
|
101
250
|
|
|
102
251
|
|
|
103
252
|
@pytest.fixture(autouse=True)
|
|
@@ -123,25 +272,206 @@ def _anydi_ainject(
|
|
|
123
272
|
)
|
|
124
273
|
pytest.fail(msg, pytrace=False)
|
|
125
274
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
275
|
+
parameters = list(_anydi_injected_parameter_iterator())
|
|
276
|
+
if not parameters:
|
|
277
|
+
return
|
|
129
278
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
279
|
+
container = cast(Container, request.getfixturevalue("container"))
|
|
280
|
+
resolvable = _select_resolvable_parameters(container, parameters)
|
|
281
|
+
if not resolvable:
|
|
282
|
+
return
|
|
134
283
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
)
|
|
284
|
+
async def _awrapper() -> None:
|
|
285
|
+
resolved = await _resolve_dependencies_async(
|
|
286
|
+
container, resolvable, target=request.node.nodeid
|
|
287
|
+
)
|
|
288
|
+
for argname, value in resolved.items():
|
|
289
|
+
request.node.funcargs[argname] = value
|
|
142
290
|
|
|
143
291
|
anyio_backend = request.getfixturevalue("anyio_backend")
|
|
144
292
|
backend_name, backend_options = extract_backend_and_options(anyio_backend)
|
|
145
293
|
|
|
146
294
|
with get_runner(backend_name, backend_options) as runner:
|
|
147
295
|
runner.run_fixture(_awrapper, {})
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _find_container(request: pytest.FixtureRequest) -> Container:
|
|
299
|
+
"""Find container."""
|
|
300
|
+
|
|
301
|
+
# Look for 'anydi_container' defined in pytest.ini (highest priority)
|
|
302
|
+
container_path = cast(str | None, request.config.getini("anydi_container"))
|
|
303
|
+
if container_path:
|
|
304
|
+
try:
|
|
305
|
+
return import_container(container_path)
|
|
306
|
+
except ImportError as exc:
|
|
307
|
+
raise RuntimeError(
|
|
308
|
+
f"Failed to load container from config "
|
|
309
|
+
f"'anydi_container={container_path}': {exc}"
|
|
310
|
+
) from exc
|
|
311
|
+
|
|
312
|
+
# Detect pytest-django + anydi_django availability
|
|
313
|
+
pluginmanager = request.config.pluginmanager
|
|
314
|
+
if pluginmanager.hasplugin("django") and importlib.util.find_spec("anydi_django"):
|
|
315
|
+
return import_container("anydi_django.container")
|
|
316
|
+
|
|
317
|
+
# Neither fixture nor config found
|
|
318
|
+
raise pytest.FixtureLookupError(
|
|
319
|
+
None,
|
|
320
|
+
request,
|
|
321
|
+
"`container` fixture is not found and 'anydi_container' config is not set. "
|
|
322
|
+
"Either define a `container` fixture in your test module "
|
|
323
|
+
"or set 'anydi_container' in pytest.ini.",
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _patch_pytest_fixtures(*, autoinject: bool) -> None: # noqa: C901
|
|
328
|
+
"""Patch pytest.fixture decorator to intercept fixtures with inject markers."""
|
|
329
|
+
from _pytest.fixtures import fixture as original_fixture_decorator
|
|
330
|
+
|
|
331
|
+
def patched_fixture(*args: Any, **kwargs: Any) -> Any: # noqa: C901
|
|
332
|
+
"""Patched fixture decorator that handles inject markers."""
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: anydi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.58.0
|
|
4
4
|
Summary: Dependency Injection library
|
|
5
5
|
Keywords: dependency injection,dependencies,di,async,asyncio,application
|
|
6
6
|
Author: Anton Ruhlov
|
|
@@ -58,9 +58,9 @@ The key features are:
|
|
|
58
58
|
|
|
59
59
|
* **Type-safe**: Dependency resolution is driven by type hints.
|
|
60
60
|
* **Async-ready**: Works the same for sync and async providers or injections.
|
|
61
|
-
* **Scoped**: Built-in singleton, transient, and request
|
|
61
|
+
* **Scoped**: Built-in singleton, transient, and request scopes, plus custom scopes.
|
|
62
62
|
* **Simple**: Small surface area keeps boilerplate low.
|
|
63
|
-
* **Fast**:
|
|
63
|
+
* **Fast**: Has minimal overhead and resolves dependencies quickly.
|
|
64
64
|
* **Named**: `Annotated[...]` makes multiple bindings per type simple.
|
|
65
65
|
* **Managed**: Providers can open/close resources via context managers.
|
|
66
66
|
* **Modular**: Compose containers or modules for large apps.
|
|
@@ -74,7 +74,7 @@ The key features are:
|
|
|
74
74
|
pip install anydi
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
##
|
|
77
|
+
## Quick Example
|
|
78
78
|
|
|
79
79
|
### Define a Service (`app/services.py`)
|
|
80
80
|
|
|
@@ -116,19 +116,18 @@ if __name__ == "__main__":
|
|
|
116
116
|
### Inject Into Functions (`app/main.py`)
|
|
117
117
|
|
|
118
118
|
```python
|
|
119
|
-
from anydi import
|
|
119
|
+
from anydi import Provide
|
|
120
120
|
|
|
121
121
|
from app.container import container
|
|
122
122
|
from app.services import GreetingService
|
|
123
123
|
|
|
124
124
|
|
|
125
|
-
|
|
126
|
-
def greet(service: GreetingService = Inject()) -> str:
|
|
125
|
+
def greet(service: Provide[GreetingService]) -> str:
|
|
127
126
|
return service.greet("World")
|
|
128
127
|
|
|
129
128
|
|
|
130
129
|
if __name__ == "__main__":
|
|
131
|
-
print(greet
|
|
130
|
+
print(container.run(greet))
|
|
132
131
|
```
|
|
133
132
|
|
|
134
133
|
### Test with Overrides (`tests/test_app.py`)
|
|
@@ -146,7 +145,7 @@ def test_greet() -> None:
|
|
|
146
145
|
service_mock.greet.return_value = "Mocked"
|
|
147
146
|
|
|
148
147
|
with container.override(GreetingService, service_mock):
|
|
149
|
-
result = greet
|
|
148
|
+
result = container.run(greet)
|
|
150
149
|
|
|
151
150
|
assert result == "Mocked"
|
|
152
151
|
```
|
|
@@ -158,8 +157,8 @@ from typing import Annotated
|
|
|
158
157
|
|
|
159
158
|
import anydi.ext.fastapi
|
|
160
159
|
from fastapi import FastAPI
|
|
161
|
-
from anydi.ext.fastapi import Inject
|
|
162
160
|
|
|
161
|
+
from anydi import Provide
|
|
163
162
|
from app.container import container
|
|
164
163
|
from app.services import GreetingService
|
|
165
164
|
|
|
@@ -169,7 +168,7 @@ app = FastAPI()
|
|
|
169
168
|
|
|
170
169
|
@app.get("/greeting")
|
|
171
170
|
async def greet(
|
|
172
|
-
service:
|
|
171
|
+
service: Provide[GreetingService]
|
|
173
172
|
) -> dict[str, str]:
|
|
174
173
|
return {"greeting": service.greet("World")}
|
|
175
174
|
|
|
@@ -245,7 +244,7 @@ Wire Django Ninja (`urls.py`):
|
|
|
245
244
|
```python
|
|
246
245
|
from typing import Annotated, Any
|
|
247
246
|
|
|
248
|
-
from anydi import
|
|
247
|
+
from anydi import Provide
|
|
249
248
|
from django.http import HttpRequest
|
|
250
249
|
from django.urls import path
|
|
251
250
|
from ninja import NinjaAPI
|
|
@@ -257,7 +256,7 @@ api = NinjaAPI()
|
|
|
257
256
|
|
|
258
257
|
|
|
259
258
|
@api.get("/greeting")
|
|
260
|
-
def greet(request: HttpRequest, service:
|
|
259
|
+
def greet(request: HttpRequest, service: Provide[GreetingService]) -> Any:
|
|
261
260
|
return {"greeting": service.greet("World")}
|
|
262
261
|
|
|
263
262
|
|
|
@@ -265,3 +264,23 @@ urlpatterns = [
|
|
|
265
264
|
path("api/", api.urls),
|
|
266
265
|
]
|
|
267
266
|
```
|
|
267
|
+
|
|
268
|
+
## What's Next?
|
|
269
|
+
|
|
270
|
+
Ready to learn more? Check out these resources:
|
|
271
|
+
|
|
272
|
+
**Core Documentation:**
|
|
273
|
+
- [Core Concepts](https://anydi.readthedocs.io/en/latest/concepts/) - Understand containers, providers, scopes, and dependency injection
|
|
274
|
+
- [Providers](https://anydi.readthedocs.io/en/latest/usage/providers/) - Learn about registration, named providers, and resource management
|
|
275
|
+
- [Scopes](https://anydi.readthedocs.io/en/latest/usage/scopes/) - Master lifecycle management with built-in and custom scopes
|
|
276
|
+
- [Dependency Injection](https://anydi.readthedocs.io/en/latest/usage/injection/) - Explore injection patterns and techniques
|
|
277
|
+
- [Testing](https://anydi.readthedocs.io/en/latest/usage/testing/) - Write testable code with provider overrides
|
|
278
|
+
|
|
279
|
+
**Framework Integrations:**
|
|
280
|
+
- [FastAPI](https://anydi.readthedocs.io/en/latest/extensions/fastapi/) - Build modern APIs with automatic dependency injection
|
|
281
|
+
- [Django](https://anydi.readthedocs.io/en/latest/extensions/django/) - Integrate with Django and Django Ninja
|
|
282
|
+
- [FastStream](https://anydi.readthedocs.io/en/latest/extensions/faststream/) - Message broker applications
|
|
283
|
+
- [Pydantic Settings](https://anydi.readthedocs.io/en/latest/extensions/pydantic_settings/) - Configuration management
|
|
284
|
+
|
|
285
|
+
**Full Documentation:**
|
|
286
|
+
- [Read the Docs](https://anydi.readthedocs.io/) - Complete documentation with examples and guides
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
anydi/__init__.py,sha256=bQKzn9qfNnIMi1m3J-DdSknSDwNg8j08fdQg_-Edkto,613
|
|
2
|
+
anydi/_async_lock.py,sha256=3dwZr0KthXFYha0XKMyXf8jMmGb1lYoNC0O5w29V9ic,1104
|
|
3
|
+
anydi/_container.py,sha256=j8XM5UYw3PVwPq92TMtEwLwqcMObs-9ZTGAEsTj4caE,26023
|
|
4
|
+
anydi/_context.py,sha256=-9QqeMWo9OpZVXZxZCQgIsswggl3Ch7lgx1KiFX_ezc,3752
|
|
5
|
+
anydi/_decorators.py,sha256=J3W261ZAG7q4XKm4tbAv1wsWr9ysx9_5MUbUvSJB_MQ,2809
|
|
6
|
+
anydi/_injector.py,sha256=IxKTh2rzMHrsW554tbiJl33Hb5sRGKYY_NU1rC4UvxE,4378
|
|
7
|
+
anydi/_module.py,sha256=2kN5uEXLd2Dsc58gz5IWK43wJewr_QgIVGSO3iWp798,2609
|
|
8
|
+
anydi/_provider.py,sha256=OV1WFHTYv7W2U0XDk_Kql1r551Vhq8o-pUV5ep1HQcU,1574
|
|
9
|
+
anydi/_resolver.py,sha256=dMtWU4OHWMYP7J2Rks0sdwyjLHF1uKo77lnvTGhhtBo,30222
|
|
10
|
+
anydi/_scanner.py,sha256=rbRkHzyd2zMu7AFLffN6_tZJcMaW9gy7E-lVdHLHYrs,4294
|
|
11
|
+
anydi/_types.py,sha256=tLJS27j0lWJFd4fIIGlGbbfKBVTPTnWgPYqZlenktis,2939
|
|
12
|
+
anydi/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
anydi/ext/django/__init__.py,sha256=Ve8lncLU9dPY_Vjt4zihPgsSxwAtFHACn0XvBM5JG8k,367
|
|
14
|
+
anydi/ext/fastapi.py,sha256=TQoS1Vh9chV0Tj0qyxjgqoGTYEMC3jp0il9CDZCZLcc,2326
|
|
15
|
+
anydi/ext/faststream.py,sha256=XT80r1FGL-xlU7r8urm9sNpUfl4OPMJseW4dade_fR4,1836
|
|
16
|
+
anydi/ext/pydantic_settings.py,sha256=jVJZ1wPaPpsxdNPlJj9yq282ebqLZ9tckWpZ0eIwWLg,1533
|
|
17
|
+
anydi/ext/pytest_plugin.py,sha256=M54DkA-KxD9GqLnXdoCyn-Qur2c44MB6d0AgJuYCZ5w,16171
|
|
18
|
+
anydi/ext/starlette/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
+
anydi/ext/starlette/middleware.py,sha256=9CQtGg5ZzUz2gFSzJr8U4BWzwNjK8XMctm3n52M77Z0,792
|
|
20
|
+
anydi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
+
anydi/testing.py,sha256=cHg3mMScZbEep9smRqSNQ81BZMQOkyugHe8TvKdPnEg,1347
|
|
22
|
+
anydi-0.58.0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
23
|
+
anydi-0.58.0.dist-info/entry_points.txt,sha256=AgOcQYM5KyS4D37QcYb00tiid0QA-pD1VrjHHq4QAps,44
|
|
24
|
+
anydi-0.58.0.dist-info/METADATA,sha256=KE1cG2fc8ZyuoUBMhGP0pUqcUkr2z8ZOGjPVy3-ILV0,7901
|
|
25
|
+
anydi-0.58.0.dist-info/RECORD,,
|
anydi-0.56.0.dist-info/RECORD
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
anydi/__init__.py,sha256=Cz-beqReX0d05SFDmYcrzIs3FqQkWAwpy1Aqzd5db34,547
|
|
2
|
-
anydi/_async_lock.py,sha256=3dwZr0KthXFYha0XKMyXf8jMmGb1lYoNC0O5w29V9ic,1104
|
|
3
|
-
anydi/_container.py,sha256=dKRT4FB0ONyEt2-MRz4T0MZziwZAMb0XyulV6lVn04g,24997
|
|
4
|
-
anydi/_context.py,sha256=-9QqeMWo9OpZVXZxZCQgIsswggl3Ch7lgx1KiFX_ezc,3752
|
|
5
|
-
anydi/_decorators.py,sha256=J3W261ZAG7q4XKm4tbAv1wsWr9ysx9_5MUbUvSJB_MQ,2809
|
|
6
|
-
anydi/_module.py,sha256=2kN5uEXLd2Dsc58gz5IWK43wJewr_QgIVGSO3iWp798,2609
|
|
7
|
-
anydi/_provider.py,sha256=OV1WFHTYv7W2U0XDk_Kql1r551Vhq8o-pUV5ep1HQcU,1574
|
|
8
|
-
anydi/_resolver.py,sha256=-MF2KsERF5qzU6uqYPF1fI58isgsjxXPLERylzFFDHE,28787
|
|
9
|
-
anydi/_scanner.py,sha256=oycIC9kw9fsIG9qgtRHeBkj3HjmcLK0FTqWLXTLLSWE,3636
|
|
10
|
-
anydi/_types.py,sha256=l3xQ0Zn15gRAwvBoQ9PRfCBigi2rrtSqGV-C50xXrLw,1780
|
|
11
|
-
anydi/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
anydi/ext/django/__init__.py,sha256=Ve8lncLU9dPY_Vjt4zihPgsSxwAtFHACn0XvBM5JG8k,367
|
|
13
|
-
anydi/ext/fastapi.py,sha256=FflFBdK-moyv9Vsfem4NyNt8jgPwFLweeCl5cfU0Iks,2348
|
|
14
|
-
anydi/ext/faststream.py,sha256=dJPInvi0JUx-SS8H4aBRl3u-PAW6S_TqqfxVOl3D1L0,1929
|
|
15
|
-
anydi/ext/pydantic_settings.py,sha256=0GQjw7QpQlT5p6GxFClXYdtc6J42PClmAnRWPEzMjvY,1488
|
|
16
|
-
anydi/ext/pytest_plugin.py,sha256=Es1K1S6_2gIdTUYkbw2d1aZcHnjJutGFafVsLPGcVJc,4684
|
|
17
|
-
anydi/ext/starlette/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
-
anydi/ext/starlette/middleware.py,sha256=MxnzshAs-CMvjJp0r457k52MzBL8O4KAuClnF6exBdU,803
|
|
19
|
-
anydi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
-
anydi/testing.py,sha256=cHg3mMScZbEep9smRqSNQ81BZMQOkyugHe8TvKdPnEg,1347
|
|
21
|
-
anydi-0.56.0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
22
|
-
anydi-0.56.0.dist-info/entry_points.txt,sha256=AgOcQYM5KyS4D37QcYb00tiid0QA-pD1VrjHHq4QAps,44
|
|
23
|
-
anydi-0.56.0.dist-info/METADATA,sha256=JkjqRSFZ-Vu22IYLnWS2uRTZA7uUbX41o2b-51yp0dA,6561
|
|
24
|
-
anydi-0.56.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|