Skip to content

Commit b5de2e0

Browse files
committed
Merge branch 'release/1.4.0'
2 parents 771ab7f + dc1b47f commit b5de2e0

File tree

7 files changed

+240
-9
lines changed

7 files changed

+240
-9
lines changed

.python-version

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
3.11.4
2+
3.10.12
3+
3.9.17
4+
3.8.17

README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,3 +309,56 @@ with graph.sync_ctx(replaced_deps={dependency: replaced}) as ctx:
309309
```
310310

311311
Furthermore, the new dependency can depend on other dependencies. Or you can change type of your dependency, like generator instead of plain return. Everything should work as you would expect it.
312+
313+
## Annotated types
314+
315+
Taskiq dependenices also support dependency injection through Annotated types.
316+
317+
```python
318+
from typing import Annotated
319+
320+
async def my_function(dependency: Annotated[int, Depends(my_func)]):
321+
pass
322+
```
323+
324+
Or you can specify classes
325+
326+
327+
```python
328+
from typing import Annotated
329+
330+
class MyClass:
331+
pass
332+
333+
async def my_function(dependency: Annotated[MyClass, Depends(my_func)]):
334+
pass
335+
```
336+
337+
And, of course you can easily save such type aliases in variables.
338+
339+
```python
340+
from typing import Annotated
341+
342+
DepType = Annotated[int, Depends(my_func)]
343+
344+
def my_function(dependency: DepType):
345+
pass
346+
347+
```
348+
349+
Also we support overrides for annotated types.
350+
351+
For example:
352+
353+
```python
354+
from typing import Annotated
355+
356+
DepType = Annotated[int, Depends(my_func)]
357+
358+
def my_function(
359+
dependency: DepType,
360+
no_cache_dep: Annotated[DepType, Depends(my_func, use_cache=False)],
361+
) -> None:
362+
pass
363+
364+
```

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "taskiq-dependencies"
3-
version = "1.3.1"
3+
version = "1.4.0"
44
description = "FastAPI like dependency injection implementation"
55
authors = ["Pavel Kirilin <[email protected]>"]
66
readme = "README.md"

taskiq_dependencies/graph.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
try:
1010
from fastapi.params import Depends as FastapiDepends # noqa: WPS433
1111
except ImportError:
12-
FastapiDepends = None # type: ignore
12+
FastapiDepends = Dependency # type: ignore
1313

1414

1515
class DependencyGraph:
@@ -136,9 +136,8 @@ def _build_graph(self) -> None: # noqa: C901, WPS210
136136
+ f"Please provide a type in param `{dep.parent.param_name}`"
137137
+ f" of `{dep.parent.dependency}`",
138138
)
139-
# We zip together names of parameters and the subsctituted values
140-
# In parameters we would see TypeVars in args
141-
# we would find actual classes.
139+
# We zip together names of parameters and the substituted values
140+
# for generics.
142141
generics = zip(
143142
parent_cls_origin.__parameters__,
144143
parent_cls.__args__, # type: ignore
@@ -172,13 +171,19 @@ def _build_graph(self) -> None: # noqa: C901, WPS210
172171
# default vaule.
173172
for param_name, param in sign.parameters.items():
174173
default_value = param.default
174+
if hasattr(param.annotation, "__metadata__"): # noqa: WPS421
175+
# We go backwards,
176+
# because you may want to override your annotation
177+
# and the overriden value will appear to be after
178+
# the original `Depends` annotation.
179+
for meta in reversed(param.annotation.__metadata__):
180+
if isinstance(meta, (Dependency, FastapiDepends)):
181+
default_value = meta
182+
break
175183

176184
# This is for FastAPI integration. So you can
177185
# use Depends from taskiq mixed with fastapi's dependencies.
178-
if FastapiDepends is not None and isinstance( # noqa: WPS337
179-
default_value,
180-
FastapiDepends,
181-
):
186+
if isinstance(default_value, FastapiDepends):
182187
default_value = Dependency(
183188
dependency=default_value.dependency,
184189
use_cache=default_value.use_cache,

tests/test_annotated.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import sys
2+
3+
import pytest
4+
5+
if sys.version_info < (3, 10):
6+
pytest.skip("Annotated is available only for python 3.10+", allow_module_level=True)
7+
8+
from typing import Annotated, AsyncGenerator, Generic, Tuple, TypeVar
9+
10+
from taskiq_dependencies import DependencyGraph, Depends
11+
12+
13+
def test_annotated_func() -> None:
14+
def get_int() -> int:
15+
return 1
16+
17+
def target_func(dep: Annotated[int, Depends(get_int)]) -> int:
18+
return dep
19+
20+
with DependencyGraph(target_func).sync_ctx() as ctx:
21+
res = target_func(**ctx.resolve_kwargs())
22+
assert res == 1
23+
24+
25+
def test_annotated_class() -> None:
26+
class TestClass:
27+
pass
28+
29+
def target_func(dep: Annotated[TestClass, Depends()]) -> TestClass:
30+
return dep
31+
32+
with DependencyGraph(target_func).sync_ctx() as ctx:
33+
res = target_func(**ctx.resolve_kwargs())
34+
assert isinstance(res, TestClass)
35+
36+
37+
def test_annotated_generic() -> None:
38+
_T = TypeVar("_T")
39+
40+
class MyClass:
41+
pass
42+
43+
class MainClass(Generic[_T]):
44+
def __init__(self, val: _T = Depends()) -> None:
45+
self.val = val
46+
47+
def test_func(a: Annotated[MainClass[MyClass], Depends()]) -> MyClass:
48+
return a.val
49+
50+
with DependencyGraph(target=test_func).sync_ctx(exception_propagation=False) as g:
51+
value = test_func(**(g.resolve_kwargs()))
52+
53+
assert isinstance(value, MyClass)
54+
55+
56+
@pytest.mark.anyio
57+
async def test_annotated_asyncgen() -> None:
58+
opened = False
59+
closed = False
60+
61+
async def my_gen() -> AsyncGenerator[int, None]:
62+
nonlocal opened, closed
63+
opened = True
64+
65+
yield 1
66+
67+
closed = True
68+
69+
def test_func(dep: Annotated[int, Depends(my_gen)]) -> int:
70+
return dep
71+
72+
async with DependencyGraph(target=test_func).async_ctx() as g:
73+
value = test_func(**(await g.resolve_kwargs()))
74+
assert value == 1
75+
76+
assert opened and closed
77+
78+
79+
def test_multiple() -> None:
80+
class TestClass:
81+
pass
82+
83+
MyType = Annotated[TestClass, Depends(use_cache=False)]
84+
85+
def test_func(dep: MyType, dep2: MyType) -> Tuple[MyType, MyType]:
86+
return dep, dep2
87+
88+
with DependencyGraph(target=test_func).sync_ctx(exception_propagation=False) as g:
89+
value = test_func(**(g.resolve_kwargs()))
90+
assert value[0] != value[1]
91+
assert isinstance(value[0], TestClass)
92+
assert isinstance(value[1], TestClass)
93+
94+
95+
def test_multiple_with_cache() -> None:
96+
class TestClass:
97+
pass
98+
99+
MyType = Annotated[TestClass, Depends()]
100+
101+
def test_func(dep: MyType, dep2: MyType) -> Tuple[MyType, MyType]:
102+
return dep, dep2
103+
104+
with DependencyGraph(target=test_func).sync_ctx(exception_propagation=False) as g:
105+
value = test_func(**(g.resolve_kwargs()))
106+
assert id(value[0]) == id(value[1])
107+
assert isinstance(value[0], TestClass)
108+
109+
110+
def test_override() -> None:
111+
class TestClass:
112+
pass
113+
114+
MyType = Annotated[TestClass, Depends()]
115+
116+
def test_func(
117+
dep: MyType,
118+
dep2: Annotated[MyType, Depends(use_cache=False)],
119+
) -> Tuple[MyType, MyType]:
120+
return dep, dep2
121+
122+
with DependencyGraph(target=test_func).sync_ctx(exception_propagation=False) as g:
123+
value = test_func(**(g.resolve_kwargs()))
124+
assert id(value[0]) != id(value[1])
125+
assert isinstance(value[0], TestClass)
126+
assert isinstance(value[1], TestClass)

tests/test_fastapi.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import sys
12
from typing import Any
23
from unittest.mock import patch
34

5+
import pytest
6+
47
from taskiq_dependencies import DependencyGraph
58

69

@@ -29,3 +32,27 @@ def func_b(dep_a: int = MyFastapiDepends(func_a)) -> int: # type: ignore
2932
kwargs = ctx.resolve_kwargs()
3033

3134
assert kwargs == {"dep_a": 1}
35+
36+
37+
@pytest.mark.skipif(sys.version_info < (3, 10), reason="Only for python 3.10+")
38+
def test_dependency_swap_annotated() -> None:
39+
"""
40+
Test that dependency classes are swapped.
41+
42+
This test checks that if function depends on FastAPI depends, it will
43+
be swapped and resolved.
44+
"""
45+
from typing import Annotated
46+
47+
with patch("taskiq_dependencies.graph.FastapiDepends", MyFastapiDepends):
48+
49+
def func_a() -> int:
50+
return 1
51+
52+
def func_b(dep_a: Annotated[int, MyFastapiDepends(func_a)]) -> int: # type: ignore
53+
return dep_a
54+
55+
with DependencyGraph(func_b).sync_ctx() as ctx:
56+
kwargs = ctx.resolve_kwargs()
57+
58+
assert kwargs == {"dep_a": 1}

tox.ini

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[tox]
2+
isolated_build = true
3+
env_list =
4+
py311
5+
py310
6+
py39
7+
py38
8+
9+
[testenv]
10+
skip_install = true
11+
allowlist_externals = poetry
12+
commands_pre =
13+
poetry install
14+
commands =
15+
pre-commit run --all-files
16+
poetry run pytest -vv

0 commit comments

Comments
 (0)