From 8a117cc000848bb24534258d345fa37aaa622a15 Mon Sep 17 00:00:00 2001 From: Fodor Zoltan Date: Sun, 21 Sep 2025 14:59:45 +0300 Subject: [PATCH] Fix python 3.8 compatibility for deps parallelization. (#639) --- .../dependencies/parallel_per_dep_disable.py | 3 + .../dependencies/parallel_per_dep_enable.py | 4 ++ docs_src/dependencies/parallel_security.py | 3 + fastapi/dependencies/utils.py | 17 +++--- tests/test_dependency_parallelization.py | 58 +++++++++++++------ 5 files changed, 58 insertions(+), 27 deletions(-) diff --git a/docs_src/dependencies/parallel_per_dep_disable.py b/docs_src/dependencies/parallel_per_dep_disable.py index d9e32c117..e2e65e306 100644 --- a/docs_src/dependencies/parallel_per_dep_disable.py +++ b/docs_src/dependencies/parallel_per_dep_disable.py @@ -1,11 +1,14 @@ from typing import Annotated + from fastapi import Depends, FastAPI app = FastAPI(depends_default_parallelizable=True) + async def dep_seq() -> int: return 1 + @app.get("/users") async def read_users( x: Annotated[int, Depends(dep_seq, parallelizable=False)], diff --git a/docs_src/dependencies/parallel_per_dep_enable.py b/docs_src/dependencies/parallel_per_dep_enable.py index cda3fa989..af2184532 100644 --- a/docs_src/dependencies/parallel_per_dep_enable.py +++ b/docs_src/dependencies/parallel_per_dep_enable.py @@ -1,14 +1,18 @@ from typing import Annotated + from fastapi import Depends, FastAPI app = FastAPI(depends_default_parallelizable=False) + async def dep_a() -> int: return 1 + async def dep_b() -> int: return 2 + @app.get("/items") async def read_items( a: Annotated[int, Depends(dep_a, parallelizable=True)], diff --git a/docs_src/dependencies/parallel_security.py b/docs_src/dependencies/parallel_security.py index c563d07be..c44f7449a 100644 --- a/docs_src/dependencies/parallel_security.py +++ b/docs_src/dependencies/parallel_security.py @@ -1,11 +1,14 @@ from typing import Annotated + from fastapi import FastAPI, Security app = FastAPI(depends_default_parallelizable=True) + async def get_api_key() -> str: return "secret" + @app.get("/secure") async def secure( k1: Annotated[str, Security(get_api_key, scopes=["a"], parallelizable=True)], diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index b8c8153de..8d6e123cd 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -591,7 +591,9 @@ class DependencySolveException(Exception): def is_context_sensitive(dependant: Dependant, *, default_parallelizable: bool) -> bool: effective_parallelizable = ( - default_parallelizable if dependant.parallelizable is None else dependant.parallelizable + default_parallelizable + if dependant.parallelizable is None + else dependant.parallelizable ) if effective_parallelizable is False or ( dependant.call is not None @@ -743,7 +745,7 @@ async def solve_dependencies( return sub_dependant.name, resolved, None, None def unpack_results( - results: list[ + results: List[ Tuple[Optional[str], Any, Optional[List[Any]], Optional[BaseException]] ], ) -> None: @@ -757,11 +759,8 @@ async def solve_dependencies( if name is not None: values[name] = value - - app = request.app # type: ignore[attr-defined] - default_parallelizable = bool( - getattr(app, "depends_default_parallelizable", False) - ) + app = request.app + default_parallelizable = bool(getattr(app, "depends_default_parallelizable", False)) sequential_deps = [] parallel_deps = [] @@ -771,7 +770,7 @@ async def solve_dependencies( else: parallel_deps.append(sub) - sequential_results: list[ + sequential_results: List[ Tuple[Optional[str], Any, Optional[List[Any]], Optional[BaseException]] ] = [] for sub in sequential_deps: @@ -779,7 +778,7 @@ async def solve_dependencies( sequential_results.append(s_result) unpack_results(sequential_results) - parallel_results: list[ + parallel_results: List[ Tuple[Optional[str], Any, Optional[List[Any]], Optional[BaseException]] ] = [] if parallel_deps: diff --git a/tests/test_dependency_parallelization.py b/tests/test_dependency_parallelization.py index 32f0b3c96..43cd313cd 100644 --- a/tests/test_dependency_parallelization.py +++ b/tests/test_dependency_parallelization.py @@ -1,14 +1,18 @@ import asyncio import time import warnings +from contextlib import AsyncExitStack +from typing import Dict, List, Optional import pytest -from starlette.requests import Request -from contextlib import AsyncExitStack from fastapi import Depends, FastAPI +from fastapi.dependencies.utils import ( + get_dependant, + silence_future_exception, + solve_dependencies, +) from fastapi.testclient import TestClient -from fastapi.dependencies.utils import get_dependant, solve_dependencies, silence_future_exception - +from starlette.requests import Request PAR_LOW, PAR_HIGH = 0.08, 0.25 SEQPAR_LOW, SEQPAR_HIGH = 0.18, 0.35 @@ -45,12 +49,23 @@ def t_end(ts: dict, key: str) -> None: ts[key]["end"] = time.perf_counter() -def assert_overlaps(ts: dict, a: str, b: str, *, min_overlap: float = OVERLAP_EPS) -> None: +def assert_overlaps( + ts: dict, a: str, b: str, *, min_overlap: float = OVERLAP_EPS +) -> None: overlap = min(ts[a]["end"], ts[b]["end"]) - max(ts[a]["start"], ts[b]["start"]) - assert overlap > min_overlap, f"no sufficient overlap between {a} and {b}: {overlap:.4f}s" - - -def make_async_timed_dep(ts: dict, key: str, *, delay: float = 0.1, value: int = 1, order: list[str] | None = None): + assert overlap > min_overlap, ( + f"no sufficient overlap between {a} and {b}: {overlap:.4f}s" + ) + + +def make_async_timed_dep( + ts: dict, + key: str, + *, + delay: float = 0.1, + value: int = 1, + order: Optional[List[str]] = None, +): async def dep(): t_start(ts, key) await asyncio.sleep(delay) @@ -72,7 +87,9 @@ def make_sync_timed_dep(ts: dict, key: str, *, delay: float = 0.1, value: int = return dep -def make_security_counter_dep(calls: dict, ts: dict | None = None, key_factory=None, *, delay: float = 0.1): +def make_security_counter_dep( + calls: Dict, ts: Optional[Dict] = None, key_factory=None, *, delay: float = 0.1 +): counter = {"i": 0} async def dep(): @@ -192,7 +209,7 @@ def test_parallel_exception_silences_future_warning_and_raises_once(): @app.get("/fail1") async def fail_endpoint(a: int = Depends(failing), b: int = Depends(failing)): - return {"ok": True} # pragma: no cover + return {"ok": True} # pragma: no cover client = TestClient(app) @@ -208,15 +225,18 @@ def test_dependency_overrides_preserve_parallelizable(): app = FastAPI(depends_default_parallelizable=False) async def original(): - await asyncio.sleep(0.1) # pragma: no cover - return "original" # pragma: no cover + await asyncio.sleep(0.1) # pragma: no cover + return "original" # pragma: no cover async def override(): await asyncio.sleep(0.1) return "override" @app.get("/override") - async def ep(x: str = Depends(original, parallelizable=True), y: str = Depends(original, parallelizable=True)): + async def ep( + x: str = Depends(original, parallelizable=True), + y: str = Depends(original, parallelizable=True), + ): return {"x": x, "y": y} app.dependency_overrides[original] = override @@ -402,7 +422,7 @@ def test_exception_order_sequential_then_parallel(): @app.get("/ex-order-a") async def ep(_: int = Depends(fail_seq), __: int = Depends(fail_par)): - return {"ok": True} # pragma: no cover + return {"ok": True} # pragma: no cover client = TestClient(app) with pytest.raises(SeqErr): @@ -428,7 +448,7 @@ def test_exception_order_with_parallel_siblings(): @app.get("/ex-order-b") async def ep(_: int = Depends(fail_a), __: int = Depends(fail_b)): - return {"ok": True} # pragma: no cover + return {"ok": True} # pragma: no cover client = TestClient(app) with pytest.raises(AErr): @@ -480,14 +500,15 @@ def test_security_scopes_cache_key_multiple(): assert data["calls"] == 2 assert_overlaps(ts, "call1", "call2") + def test_cached_value_converted_to_future_and_awaited(): app = FastAPI() async def low() -> int: - return 999 # pragma: no cover + return 999 # pragma: no cover async def wrapper(x: int = Depends(low)) -> int: - return x # pragma: no cover + return x # pragma: no cover @app.get("/cov") async def cov(request: Request): @@ -514,4 +535,5 @@ def test_silence_future_exception_handles_exception_path() -> None: class Dummy: def exception(self): raise Exception("boom") + silence_future_exception(Dummy())