From c940ab71975c6d2fb9c99c2f036f956cfb838729 Mon Sep 17 00:00:00 2001 From: ollie-bell <56110893+ollie-bell@users.noreply.github.com> Date: Mon, 9 Feb 2026 19:06:06 +0000 Subject: [PATCH] feat: support starlette 0.52.0+ generic Request --- fastapi/dependencies/utils.py | 3 ++- tests/test_router_events.py | 38 +++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index ab18ec2db..2de42b80a 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -329,6 +329,7 @@ def get_dependant( def add_non_field_param_to_dependency( *, param_name: str, type_annotation: Any, dependant: Dependant ) -> bool | None: + type_annotation = get_origin(type_annotation) or type_annotation if lenient_issubclass(type_annotation, Request): dependant.request_param_name = param_name return True @@ -453,7 +454,7 @@ def analyze_param( # Only apply special handling when there's no explicit Depends - if there's a Depends, # the dependency will be called and its return value used instead of the special injection if depends is None and lenient_issubclass( - type_annotation, + get_origin(type_annotation) or type_annotation, ( Request, WebSocket, diff --git a/tests/test_router_events.py b/tests/test_router_events.py index 7869a7afc..e93ae2502 100644 --- a/tests/test_router_events.py +++ b/tests/test_router_events.py @@ -1,10 +1,15 @@ from collections.abc import AsyncGenerator from contextlib import asynccontextmanager +from typing import TypedDict import pytest from fastapi import APIRouter, FastAPI, Request from fastapi.testclient import TestClient from pydantic import BaseModel +from starlette import __version__ as STARLETTE_VERSION +from typing_extensions import Self + +STARLETTE_MINOR_VERSION_TUPLE = tuple(int(x) for x in STARLETTE_VERSION.split(".")[:2]) class State(BaseModel): @@ -171,6 +176,39 @@ def test_router_nested_lifespan_state(state: State) -> None: assert state.sub_router_shutdown is True +@pytest.mark.skipif( + STARLETTE_MINOR_VERSION_TUPLE < (0, 52), + reason="Starlette Request with generic type is not supported in Starlette < 0.52.0", +) +def test_router_generic_request_typed_dict_lifespan_state() -> None: + class MyClass: + async def __aenter__(self) -> Self: + return self + + async def __aexit__(self, exc_type, exc_value, traceback) -> None: + pass + + class MyState(TypedDict): + my_class: MyClass + + @asynccontextmanager + async def lifespan(app: FastAPI) -> AsyncGenerator[MyState]: + async with MyClass() as my_class: + yield {"my_class": my_class} + + app = FastAPI(lifespan=lifespan) + + @app.get("/") + def main(request: Request[MyState]) -> dict[str, str]: + assert isinstance(request.state["my_class"], MyClass) + return {"message": "Hello World"} + + with TestClient(app) as client: + response = client.get("/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "Hello World"} + + def test_router_nested_lifespan_state_overriding_by_parent() -> None: @asynccontextmanager async def lifespan(