Skip to content

Commit 771ab7f

Browse files
committed
Merge branch 'release/1.3.1'
2 parents 2c1d219 + 5e32f6f commit 771ab7f

File tree

5 files changed

+161
-23
lines changed

5 files changed

+161
-23
lines changed

.github/workflows/test.yml

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,7 @@ name: Testing
33
on: [push, pull_request]
44

55
jobs:
6-
pre_job:
7-
# continue-on-error: true # Uncomment once integration is finished
8-
runs-on: ubuntu-latest
9-
# Map a step output to a job output
10-
outputs:
11-
should_skip: ${{ steps.skip_check.outputs.should_skip }}
12-
steps:
13-
- id: skip_check
14-
uses: fkirc/skip-duplicate-actions@master
15-
with:
16-
# All of these options are optional, so you can remove them if you are happy with the defaults
17-
concurrent_skipping: 'same_content'
18-
skip_after_successful_duplicate: 'true'
19-
paths_ignore: '["**/README.md"]'
206
lint:
21-
needs: pre_job
22-
if: ${{ needs.pre_job.outputs.should_skip != 'true' }}
237
strategy:
248
matrix:
259
cmd:
@@ -43,8 +27,6 @@ jobs:
4327
- name: Run lint check
4428
run: poetry run pre-commit run -a ${{ matrix.cmd }}
4529
pytest:
46-
needs: pre_job
47-
if: ${{ needs.pre_job.outputs.should_skip != 'true' }}
4830
permissions:
4931
checks: write
5032
pull-requests: write

README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,3 +260,52 @@ This code will is going to print:
260260
strstr
261261
100
262262
```
263+
264+
## Dependencies replacement
265+
266+
You can replace dependencies in runtime, it will recalculate graph
267+
and will execute your function with updated dependencies.
268+
269+
**!!! This functionality tremendously slows down dependency resolution.**
270+
271+
Use this functionality only for tests. Otherwise, you will end up building dependency graphs on every resolution request. Which is very slow.
272+
273+
But for tests it may be a game changer, since you don't want to change your code, but some dependencies instead.
274+
275+
Here's an example. Imagine you have a built graph for a specific function, like this:
276+
277+
```python
278+
from taskiq_dependencies import DependencyGraph, Depends
279+
280+
281+
def dependency() -> int:
282+
return 1
283+
284+
285+
def target(dep_value: int = Depends(dependency)) -> None:
286+
assert dep_value == 1
287+
288+
graph = DependencyGraph(target)
289+
```
290+
291+
Normally, you would call the target, by writing something like this:
292+
293+
```python
294+
with graph.sync_ctx() as ctx:
295+
target(**ctx.resolve_kwargs())
296+
```
297+
298+
But what if you want to replace dependency in runtime, just
299+
before resolving kwargs? The solution is to add `replaced_deps`
300+
parameter to the context method. For example:
301+
302+
```python
303+
def replaced() -> int:
304+
return 2
305+
306+
307+
with graph.sync_ctx(replaced_deps={dependency: replaced}) as ctx:
308+
target(**ctx.resolve_kwargs())
309+
```
310+
311+
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.

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.0"
3+
version = "1.3.1"
44
description = "FastAPI like dependency injection implementation"
55
authors = ["Pavel Kirilin <[email protected]>"]
66
readme = "README.md"

taskiq_dependencies/graph.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class DependencyGraph:
2020
def __init__(
2121
self,
2222
target: Callable[..., Any],
23+
replaced_deps: Optional[Dict[Any, Any]] = None,
2324
) -> None:
2425
self.target = target
2526
# Ordinary dependencies with cache.
@@ -28,6 +29,7 @@ def __init__(
2829
# Can be considered as sub graphs.
2930
self.subgraphs: Dict[Any, DependencyGraph] = {}
3031
self.ordered_deps: List[Dependency] = []
32+
self.replaced_deps = replaced_deps
3133
self._build_graph()
3234

3335
def is_empty(self) -> bool:
@@ -41,6 +43,7 @@ def is_empty(self) -> bool:
4143
def async_ctx(
4244
self,
4345
initial_cache: Optional[Dict[Any, Any]] = None,
46+
replaced_deps: Optional[Dict[Any, Any]] = None,
4447
exception_propagation: bool = True,
4548
) -> AsyncResolveContext:
4649
"""
@@ -51,17 +54,22 @@ def async_ctx(
5154
:param initial_cache: initial cache dict.
5255
:param exception_propagation: If true, all found errors within
5356
context will be propagated to dependencies.
57+
:param replaced_deps: Dependencies to replace during runtime.
5458
:return: new resolver context.
5559
"""
60+
graph = self
61+
if replaced_deps:
62+
graph = DependencyGraph(self.target, replaced_deps)
5663
return AsyncResolveContext(
57-
self,
64+
graph,
5865
initial_cache,
5966
exception_propagation,
6067
)
6168

6269
def sync_ctx(
6370
self,
6471
initial_cache: Optional[Dict[Any, Any]] = None,
72+
replaced_deps: Optional[Dict[Any, Any]] = None,
6573
exception_propagation: bool = True,
6674
) -> SyncResolveContext:
6775
"""
@@ -72,10 +80,14 @@ def sync_ctx(
7280
:param initial_cache: initial cache dict.
7381
:param exception_propagation: If true, all found errors within
7482
context will be propagated to dependencies.
83+
:param replaced_deps: Dependencies to replace during runtime.
7584
:return: new resolver context.
7685
"""
86+
graph = self
87+
if replaced_deps:
88+
graph = DependencyGraph(self.target, replaced_deps)
7789
return SyncResolveContext(
78-
self,
90+
graph,
7991
initial_cache,
8092
exception_propagation,
8193
)
@@ -102,6 +114,8 @@ def _build_graph(self) -> None: # noqa: C901, WPS210
102114
continue
103115
if dep.dependency is None:
104116
continue
117+
if self.replaced_deps and dep.dependency in self.replaced_deps:
118+
dep.dependency = self.replaced_deps[dep.dependency]
105119
# Get signature and type hints.
106120
origin = getattr(dep.dependency, "__origin__", None)
107121
if origin is None:

tests/test_graph.py

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
import re
23
import uuid
34
from typing import Any, AsyncGenerator, Generator, Generic, Tuple, TypeVar
45

@@ -36,8 +37,9 @@ def testfunc(a: int = Depends(dep1)) -> int:
3637
return a
3738

3839
with DependencyGraph(testfunc).sync_ctx({}) as sctx:
39-
with pytest.raises(RuntimeError):
40-
assert sctx.resolve_kwargs() == {"a": 1}
40+
with pytest.warns(match=re.compile(".*was never awaited.*")):
41+
with pytest.raises(RuntimeError):
42+
assert sctx.resolve_kwargs() == {"a": 1}
4143

4244
async with DependencyGraph(testfunc).async_ctx({}) as actx:
4345
assert await actx.resolve_kwargs() == {"a": 1}
@@ -611,3 +613,94 @@ def target(
611613
assert dep_obj.dependency == GenericClass[Tuple[str, int]]
612614
assert dep_obj.signature.name == "class_val"
613615
assert dep_obj.signature.annotation == GenericClass[Tuple[str, int]]
616+
617+
618+
@pytest.mark.anyio
619+
async def test_replaced_dep_simple() -> None:
620+
def replaced() -> int:
621+
return 321
622+
623+
def dep() -> int:
624+
return 123
625+
626+
def target(val: int = Depends(dep)) -> None:
627+
return None
628+
629+
graph = DependencyGraph(target=target)
630+
async with graph.async_ctx(replaced_deps={dep: replaced}) as ctx:
631+
kwargs = await ctx.resolve_kwargs()
632+
assert kwargs["val"] == 321
633+
634+
635+
@pytest.mark.anyio
636+
async def test_replaced_dep_generators() -> None:
637+
call_count = 0
638+
639+
def replaced() -> Generator[int, None, None]:
640+
nonlocal call_count
641+
yield 321
642+
call_count += 1
643+
644+
def dep() -> int:
645+
return 123
646+
647+
def target(val: int = Depends(dep)) -> None:
648+
return None
649+
650+
graph = DependencyGraph(target=target)
651+
async with graph.async_ctx(replaced_deps={dep: replaced}) as ctx:
652+
kwargs = await ctx.resolve_kwargs()
653+
assert kwargs["val"] == 321
654+
assert call_count == 1
655+
656+
657+
@pytest.mark.anyio
658+
async def test_replaced_dep_exception_propogation() -> None:
659+
exc_count = 0
660+
661+
def replaced() -> Generator[int, None, None]:
662+
nonlocal exc_count
663+
try:
664+
yield 321
665+
except ValueError:
666+
exc_count += 1
667+
668+
def dep() -> int:
669+
return 123
670+
671+
def target(val: int = Depends(dep)) -> None:
672+
raise ValueError("lol")
673+
674+
graph = DependencyGraph(target=target)
675+
with pytest.raises(ValueError):
676+
async with graph.async_ctx(
677+
replaced_deps={dep: replaced},
678+
exception_propagation=True,
679+
) as ctx:
680+
kwargs = await ctx.resolve_kwargs()
681+
assert kwargs["val"] == 321
682+
target(**kwargs)
683+
assert exc_count == 1
684+
685+
686+
@pytest.mark.anyio
687+
async def test_replaced_dep_subdependencies() -> None:
688+
def subdep() -> int:
689+
return 321
690+
691+
def replaced(ret_val: int = Depends(subdep)) -> int:
692+
return ret_val
693+
694+
def dep() -> int:
695+
return 123
696+
697+
def target(val: int = Depends(dep)) -> None:
698+
"""Stub function."""
699+
700+
graph = DependencyGraph(target=target)
701+
async with graph.async_ctx(
702+
replaced_deps={dep: replaced},
703+
exception_propagation=True,
704+
) as ctx:
705+
kwargs = await ctx.resolve_kwargs()
706+
assert kwargs["val"] == 321

0 commit comments

Comments
 (0)