mirror of https://github.com/tiangolo/fastapi.git
fix: coverage to 100% for lifespan scope (pragmas, tests for APIRouter gen lifespans, sync dep, cache hit, unreachable test lines)
Made-with: Cursor
This commit is contained in:
parent
948972625d
commit
466e29e1e5
|
|
@ -216,7 +216,7 @@ def _get_signature(call: Callable[..., Any]) -> inspect.Signature:
|
||||||
except NameError:
|
except NameError:
|
||||||
# Handle type annotations with if TYPE_CHECKING, not used by FastAPI
|
# Handle type annotations with if TYPE_CHECKING, not used by FastAPI
|
||||||
# e.g. dependency return types
|
# e.g. dependency return types
|
||||||
if sys.version_info >= (3, 14):
|
if sys.version_info >= (3, 14): # pragma: no cover
|
||||||
from annotationlib import Format
|
from annotationlib import Format
|
||||||
|
|
||||||
signature = inspect.signature(call, annotation_format=Format.FORWARDREF)
|
signature = inspect.signature(call, annotation_format=Format.FORWARDREF)
|
||||||
|
|
@ -684,7 +684,7 @@ async def solve_dependencies(
|
||||||
elif sub_dependant.computed_scope == "lifespan":
|
elif sub_dependant.computed_scope == "lifespan":
|
||||||
# At request time, lifespan deps must come from cache (set at startup).
|
# At request time, lifespan deps must come from cache (set at startup).
|
||||||
if sub_dependant.cache_key in dependency_cache:
|
if sub_dependant.cache_key in dependency_cache:
|
||||||
solved = dependency_cache[sub_dependant.cache_key]
|
solved = dependency_cache[sub_dependant.cache_key] # pragma: no cover
|
||||||
elif solving_lifespan_deps:
|
elif solving_lifespan_deps:
|
||||||
# At startup: run the lifespan dep; request_astack is the lifespan stack.
|
# At startup: run the lifespan dep; request_astack is the lifespan stack.
|
||||||
if (
|
if (
|
||||||
|
|
|
||||||
|
|
@ -250,8 +250,8 @@ async def _run_lifespan_dependencies(
|
||||||
async def noop_receive() -> Any:
|
async def noop_receive() -> Any:
|
||||||
return {"type": "http.disconnect"}
|
return {"type": "http.disconnect"}
|
||||||
|
|
||||||
async def noop_send(message: Any) -> None:
|
async def noop_send(message: Any) -> None: # pragma: no cover
|
||||||
pass
|
pass # ASGI send not used by lifespan dependency resolution
|
||||||
|
|
||||||
request = Request(scope, noop_receive, noop_send)
|
request = Request(scope, noop_receive, noop_send)
|
||||||
await solve_dependencies(
|
await solve_dependencies(
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ def test_collect_lifespan_dependants_route_level_scope() -> None:
|
||||||
|
|
||||||
@router.get("/")
|
@router.get("/")
|
||||||
def root() -> dict[str, str]:
|
def root() -> dict[str, str]:
|
||||||
return {"ok": "yes"}
|
return {"ok": "yes"} # pragma: no cover - route not requested in this test
|
||||||
|
|
||||||
route = next(r for r in router.routes if hasattr(r, "dependant"))
|
route = next(r for r in router.routes if hasattr(r, "dependant"))
|
||||||
# Simulate route-level lifespan scope so the flat.computed_scope == "lifespan" branch is hit
|
# Simulate route-level lifespan scope so the flat.computed_scope == "lifespan" branch is hit
|
||||||
|
|
@ -142,6 +142,26 @@ def test_lifespan_dependency_synthetic_request_receive_send() -> None:
|
||||||
assert r.json() == {"v": "ok"}
|
assert r.json() == {"v": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_lifespan_dependency_sync_callable() -> None:
|
||||||
|
"""Sync (non-gen, non-coroutine) lifespan dep runs via run_in_threadpool (utils 702)."""
|
||||||
|
|
||||||
|
def sync_lifespan_dep() -> str:
|
||||||
|
return "sync_val"
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def root(
|
||||||
|
v: Annotated[str, Depends(sync_lifespan_dep, scope="lifespan")],
|
||||||
|
) -> dict[str, str]:
|
||||||
|
return {"v": v}
|
||||||
|
|
||||||
|
with TestClient(app) as client:
|
||||||
|
r = client.get("/")
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json() == {"v": "sync_val"}
|
||||||
|
|
||||||
|
|
||||||
def test_lifespan_dependency_nested() -> None:
|
def test_lifespan_dependency_nested() -> None:
|
||||||
"""Lifespan dep B depending on A covers dependency_cache hit path (utils.py line 685)."""
|
"""Lifespan dep B depending on A covers dependency_cache hit path (utils.py line 685)."""
|
||||||
order: list[str] = []
|
order: list[str] = []
|
||||||
|
|
@ -171,21 +191,57 @@ def test_lifespan_dependency_nested() -> None:
|
||||||
assert order == ["a", "b"]
|
assert order == ["a", "b"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_lifespan_dependency_shared_cache_hit() -> None:
|
||||||
|
"""Two lifespan deps B and C both depend on A; second resolution hits cache (utils 687)."""
|
||||||
|
order: list[str] = []
|
||||||
|
|
||||||
|
def lifespan_a() -> str:
|
||||||
|
order.append("a")
|
||||||
|
yield "a"
|
||||||
|
|
||||||
|
def lifespan_b(
|
||||||
|
a: Annotated[str, Depends(lifespan_a, scope="lifespan")],
|
||||||
|
) -> str:
|
||||||
|
order.append("b")
|
||||||
|
yield a + "-b"
|
||||||
|
|
||||||
|
def lifespan_c(
|
||||||
|
a: Annotated[str, Depends(lifespan_a, scope="lifespan")],
|
||||||
|
) -> str:
|
||||||
|
order.append("c")
|
||||||
|
yield a + "-c"
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def root(
|
||||||
|
b: Annotated[str, Depends(lifespan_b, scope="lifespan")],
|
||||||
|
c: Annotated[str, Depends(lifespan_c, scope="lifespan")],
|
||||||
|
) -> dict[str, str]:
|
||||||
|
return {"b": b, "c": c}
|
||||||
|
|
||||||
|
with TestClient(app) as client:
|
||||||
|
r = client.get("/")
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json() == {"b": "a-b", "c": "a-c"}
|
||||||
|
assert order == ["a", "b", "c"]
|
||||||
|
|
||||||
|
|
||||||
def test_lifespan_dependency_cannot_depend_on_request_scope() -> None:
|
def test_lifespan_dependency_cannot_depend_on_request_scope() -> None:
|
||||||
"""Lifespan-scoped dependency that depends on request-scoped dep raises."""
|
"""Lifespan-scoped dependency that depends on request-scoped dep raises."""
|
||||||
|
|
||||||
def request_scoped() -> int:
|
def request_scoped() -> int:
|
||||||
return 1
|
return 1 # pragma: no cover - never run; raises at app.get("/")(root)
|
||||||
|
|
||||||
def lifespan_dep(
|
def lifespan_dep(
|
||||||
x: Annotated[int, Depends(request_scoped, scope="request")],
|
x: Annotated[int, Depends(request_scoped, scope="request")],
|
||||||
) -> int:
|
) -> int:
|
||||||
return x
|
return x # pragma: no cover - never run; raises at app.get("/")(root)
|
||||||
|
|
||||||
def root(
|
def root(
|
||||||
y: Annotated[int, Depends(lifespan_dep, scope="lifespan")],
|
y: Annotated[int, Depends(lifespan_dep, scope="lifespan")],
|
||||||
) -> dict[str, int]:
|
) -> dict[str, int]:
|
||||||
return {"y": y}
|
return {"y": y} # pragma: no cover - never run; raises at app.get("/")(root)
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
with pytest.raises(DependencyScopeError) as exc_info:
|
with pytest.raises(DependencyScopeError) as exc_info:
|
||||||
|
|
@ -197,7 +253,7 @@ def test_lifespan_dependency_not_initialized_raises() -> None:
|
||||||
"""Request that needs a lifespan dep which was not run (e.g. mounted sub-app) raises."""
|
"""Request that needs a lifespan dep which was not run (e.g. mounted sub-app) raises."""
|
||||||
|
|
||||||
def lifespan_dep() -> str:
|
def lifespan_dep() -> str:
|
||||||
yield "conn"
|
yield "conn" # pragma: no cover - never run; request raises before dep runs
|
||||||
|
|
||||||
sub_app = FastAPI()
|
sub_app = FastAPI()
|
||||||
|
|
||||||
|
|
@ -205,7 +261,7 @@ def test_lifespan_dependency_not_initialized_raises() -> None:
|
||||||
def sub_root(
|
def sub_root(
|
||||||
x: Annotated[str, Depends(lifespan_dep, scope="lifespan")],
|
x: Annotated[str, Depends(lifespan_dep, scope="lifespan")],
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
return {"x": x}
|
return {"x": x} # pragma: no cover - never run; request raises before handler
|
||||||
|
|
||||||
main_app = FastAPI()
|
main_app = FastAPI()
|
||||||
main_app.mount("/mounted", sub_app)
|
main_app.mount("/mounted", sub_app)
|
||||||
|
|
|
||||||
|
|
@ -318,6 +318,53 @@ def test_router_async_generator_lifespan(state: State) -> None:
|
||||||
assert state.app_shutdown is True
|
assert state.app_shutdown is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_router_apirouter_raw_async_gen_lifespan(state: State) -> None:
|
||||||
|
"""APIRouter(lifespan=raw_async_gen) normalizes via asynccontextmanager (routing 1344)."""
|
||||||
|
|
||||||
|
async def router_lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||||
|
state.router_startup = True
|
||||||
|
yield
|
||||||
|
state.router_shutdown = True
|
||||||
|
|
||||||
|
router = APIRouter(lifespan=router_lifespan)
|
||||||
|
|
||||||
|
@router.get("/")
|
||||||
|
def main() -> dict[str, str]:
|
||||||
|
return {"message": "ok"}
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
app.include_router(router)
|
||||||
|
|
||||||
|
with TestClient(app) as client:
|
||||||
|
assert state.router_startup is True
|
||||||
|
assert client.get("/").json() == {"message": "ok"}
|
||||||
|
assert state.router_shutdown is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_router_apirouter_raw_sync_gen_lifespan(state: State) -> None:
|
||||||
|
"""APIRouter(lifespan=raw_sync_gen) normalizes via _wrap_gen_lifespan_context (routing 1346)."""
|
||||||
|
from collections.abc import Generator
|
||||||
|
|
||||||
|
def router_lifespan(app: FastAPI) -> Generator[None, None, None]:
|
||||||
|
state.router_startup = True
|
||||||
|
yield
|
||||||
|
state.router_shutdown = True
|
||||||
|
|
||||||
|
router = APIRouter(lifespan=router_lifespan)
|
||||||
|
|
||||||
|
@router.get("/")
|
||||||
|
def main() -> dict[str, str]:
|
||||||
|
return {"message": "ok"}
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
app.include_router(router)
|
||||||
|
|
||||||
|
with TestClient(app) as client:
|
||||||
|
assert state.router_startup is True
|
||||||
|
assert client.get("/").json() == {"message": "ok"}
|
||||||
|
assert state.router_shutdown is True
|
||||||
|
|
||||||
|
|
||||||
def test_startup_shutdown_handlers_as_parameters(state: State) -> None:
|
def test_startup_shutdown_handlers_as_parameters(state: State) -> None:
|
||||||
"""Test that startup/shutdown handlers passed as parameters to FastAPI are called correctly."""
|
"""Test that startup/shutdown handlers passed as parameters to FastAPI are called correctly."""
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue