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 typing import Annotated
from fastapi import Depends, FastAPI from fastapi import Depends, FastAPI
app = FastAPI(depends_default_parallelizable=True) app = FastAPI(depends_default_parallelizable=True)
async def dep_seq() -> int: async def dep_seq() -> int:
return 1 return 1
@app.get("/users") @app.get("/users")
async def read_users( async def read_users(
x: Annotated[int, Depends(dep_seq, parallelizable=False)], 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 typing import Annotated
from fastapi import Depends, FastAPI from fastapi import Depends, FastAPI
app = FastAPI(depends_default_parallelizable=False) app = FastAPI(depends_default_parallelizable=False)
async def dep_a() -> int: async def dep_a() -> int:
return 1 return 1
async def dep_b() -> int: async def dep_b() -> int:
return 2 return 2
@app.get("/items") @app.get("/items")
async def read_items( async def read_items(
a: Annotated[int, Depends(dep_a, parallelizable=True)], a: Annotated[int, Depends(dep_a, parallelizable=True)],

3
docs_src/dependencies/parallel_security.py

@ -1,11 +1,14 @@
from typing import Annotated from typing import Annotated
from fastapi import FastAPI, Security from fastapi import FastAPI, Security
app = FastAPI(depends_default_parallelizable=True) app = FastAPI(depends_default_parallelizable=True)
async def get_api_key() -> str: async def get_api_key() -> str:
return "secret" return "secret"
@app.get("/secure") @app.get("/secure")
async def secure( async def secure(
k1: Annotated[str, Security(get_api_key, scopes=["a"], parallelizable=True)], 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: def is_context_sensitive(dependant: Dependant, *, default_parallelizable: bool) -> bool:
effective_parallelizable = ( 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 ( if effective_parallelizable is False or (
dependant.call is not None dependant.call is not None
@ -743,7 +745,7 @@ async def solve_dependencies(
return sub_dependant.name, resolved, None, None return sub_dependant.name, resolved, None, None
def unpack_results( def unpack_results(
results: list[ results: List[
Tuple[Optional[str], Any, Optional[List[Any]], Optional[BaseException]] Tuple[Optional[str], Any, Optional[List[Any]], Optional[BaseException]]
], ],
) -> None: ) -> None:
@ -757,11 +759,8 @@ async def solve_dependencies(
if name is not None: if name is not None:
values[name] = value values[name] = value
app = request.app
app = request.app # type: ignore[attr-defined] default_parallelizable = bool(getattr(app, "depends_default_parallelizable", False))
default_parallelizable = bool(
getattr(app, "depends_default_parallelizable", False)
)
sequential_deps = [] sequential_deps = []
parallel_deps = [] parallel_deps = []
@ -771,7 +770,7 @@ async def solve_dependencies(
else: else:
parallel_deps.append(sub) parallel_deps.append(sub)
sequential_results: list[ sequential_results: List[
Tuple[Optional[str], Any, Optional[List[Any]], Optional[BaseException]] Tuple[Optional[str], Any, Optional[List[Any]], Optional[BaseException]]
] = [] ] = []
for sub in sequential_deps: for sub in sequential_deps:
@ -779,7 +778,7 @@ async def solve_dependencies(
sequential_results.append(s_result) sequential_results.append(s_result)
unpack_results(sequential_results) unpack_results(sequential_results)
parallel_results: list[ parallel_results: List[
Tuple[Optional[str], Any, Optional[List[Any]], Optional[BaseException]] Tuple[Optional[str], Any, Optional[List[Any]], Optional[BaseException]]
] = [] ] = []
if parallel_deps: if parallel_deps:

58
tests/test_dependency_parallelization.py

@ -1,14 +1,18 @@
import asyncio import asyncio
import time import time
import warnings import warnings
from contextlib import AsyncExitStack
from typing import Dict, List, Optional
import pytest import pytest
from starlette.requests import Request
from contextlib import AsyncExitStack
from fastapi import Depends, FastAPI from fastapi import Depends, FastAPI
from fastapi.dependencies.utils import (
get_dependant,
silence_future_exception,
solve_dependencies,
)
from fastapi.testclient import TestClient 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 PAR_LOW, PAR_HIGH = 0.08, 0.25
SEQPAR_LOW, SEQPAR_HIGH = 0.18, 0.35 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() 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"]) 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" 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):
def make_async_timed_dep(
ts: dict,
key: str,
*,
delay: float = 0.1,
value: int = 1,
order: Optional[List[str]] = None,
):
async def dep(): async def dep():
t_start(ts, key) t_start(ts, key)
await asyncio.sleep(delay) 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 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} counter = {"i": 0}
async def dep(): async def dep():
@ -192,7 +209,7 @@ def test_parallel_exception_silences_future_warning_and_raises_once():
@app.get("/fail1") @app.get("/fail1")
async def fail_endpoint(a: int = Depends(failing), b: int = Depends(failing)): 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) client = TestClient(app)
@ -208,15 +225,18 @@ def test_dependency_overrides_preserve_parallelizable():
app = FastAPI(depends_default_parallelizable=False) app = FastAPI(depends_default_parallelizable=False)
async def original(): async def original():
await asyncio.sleep(0.1) # pragma: no cover await asyncio.sleep(0.1) # pragma: no cover
return "original" # pragma: no cover return "original" # pragma: no cover
async def override(): async def override():
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
return "override" return "override"
@app.get("/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} return {"x": x, "y": y}
app.dependency_overrides[original] = override app.dependency_overrides[original] = override
@ -402,7 +422,7 @@ def test_exception_order_sequential_then_parallel():
@app.get("/ex-order-a") @app.get("/ex-order-a")
async def ep(_: int = Depends(fail_seq), __: int = Depends(fail_par)): 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) client = TestClient(app)
with pytest.raises(SeqErr): with pytest.raises(SeqErr):
@ -428,7 +448,7 @@ def test_exception_order_with_parallel_siblings():
@app.get("/ex-order-b") @app.get("/ex-order-b")
async def ep(_: int = Depends(fail_a), __: int = Depends(fail_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) client = TestClient(app)
with pytest.raises(AErr): with pytest.raises(AErr):
@ -480,14 +500,15 @@ def test_security_scopes_cache_key_multiple():
assert data["calls"] == 2 assert data["calls"] == 2
assert_overlaps(ts, "call1", "call2") assert_overlaps(ts, "call1", "call2")
def test_cached_value_converted_to_future_and_awaited(): def test_cached_value_converted_to_future_and_awaited():
app = FastAPI() app = FastAPI()
async def low() -> int: async def low() -> int:
return 999 # pragma: no cover return 999 # pragma: no cover
async def wrapper(x: int = Depends(low)) -> int: async def wrapper(x: int = Depends(low)) -> int:
return x # pragma: no cover return x # pragma: no cover
@app.get("/cov") @app.get("/cov")
async def cov(request: Request): async def cov(request: Request):
@ -514,4 +535,5 @@ def test_silence_future_exception_handles_exception_path() -> None:
class Dummy: class Dummy:
def exception(self): def exception(self):
raise Exception("boom") raise Exception("boom")
silence_future_exception(Dummy()) silence_future_exception(Dummy())

Loading…
Cancel
Save