Browse Source

Fix python 3.8 compatibility for deps parallelization. (#639)

pull/13756/head
Fodor Zoltan 10 months ago
parent
commit
8a117cc000
  1. 3
      docs_src/dependencies/parallel_per_dep_disable.py
  2. 4
      docs_src/dependencies/parallel_per_dep_enable.py
  3. 3
      docs_src/dependencies/parallel_security.py
  4. 17
      fastapi/dependencies/utils.py
  5. 58
      tests/test_dependency_parallelization.py

3
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)],

4
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)],

3
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)],

17
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:

58
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())

Loading…
Cancel
Save