← eval7_submission

fastapi_14794

resolved RESOLVED PASS · 15 tool calls · 147 s · fastapi/fastapi

Task input

✨ Allow `Response` type hint as dependency annotation

## Summary
Fixes #10127

Using `Response` as a type hint with `Depends` would fail with an assertion error:
```python
def endpoint(response: Annotated[Response, Depends(modify_response)]):
    # AssertionError: Cannot specify \`Depends\` for type Response
```

## Changes
Modified the type annotation handling in `analyze_param()` to check for `Depends` before applying special type handling. When a `Depends` is specified, the dependency is called normally and its return value used.

## Usage
Now supports:
- `response: Annotated[Response, Depends(modify_response)]`
- `response: Response = Depends(modify_response)`

While still supporting regular injection:
- `response: Response`

## Testing
Added comprehensive tests in `tests/test_response_dependency.py`

Tool calls (16)

#ToolArgumentsResult
1run_command
{
 "command": "grep -rn \"analyze_param\" fastapi | head -20"
}
{"status": "ok", "stdout": "fastapi/dependencies/utils.py:281: param_details = analyze_param(\nfastapi/dependencies/utils.py:362:def analyze_param(\n", "stderr"
full
{"status": "ok", "stdout": "fastapi/dependencies/utils.py:281:        param_details = analyze_param(\nfastapi/dependencies/utils.py:362:def analyze_param(\n", "stderr": "", "exit_code": 0}
2-3 ×2read_file
{
 "filepath": "fastapi/dependencies/utils.py",
 "start_line": "362"
}
{"status": "error", "error_type": "FileReadError", "error_message": "'>' not supported between instances of 'int' and 'str'"}
full
{"status": "error", "error_type": "FileReadError", "error_message": "'>' not supported between instances of 'int' and 'str'"}
4read_file
{
 "filepath": "fastapi/dependencies/utils.py"
}
{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": "import dataclasses\nimport inspect\nimport sys\nfrom collections.abc import Coroutine,
full
{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": "import dataclasses\nimport inspect\nimport sys\nfrom collections.abc import Coroutine, Mapping, Sequence\nfrom contextlib import AsyncExitStack, contextmanager\nfrom copy import copy, deepcopy\nfrom dataclasses import dataclass\nfrom typing import (\n    Annotated,\n    Any,\n    Callable,\n    ForwardRef,\n    Optional,\n    Union,\n    cast,\n)\n\nimport anyio\nfrom fastapi import params\nfrom fastapi._compat import (\n    ModelField,\n    RequiredParam,\n    Undefined,\n    _regenerate_error_with_loc,\n    copy_field_info,\n    create_body_model,\n    evaluate_forwardref,\n    field_annotation_is_scalar,\n    get_cached_model_fields,\n    get_missing_field_error,\n    is_bytes_field,\n    is_bytes_sequence_field,\n    is_scalar_field,\n    is_scalar_sequence_field,\n    is_sequence_field,\n    is_uploadfile_or_nonable_uploadfile_annotation,\n    is_uploadfile_sequence_annotation,\n    lenient_issubclass,\n    sequence_types,\n    serialize_sequence_value,\n    value_is_sequence,\n)\nfrom fastapi.background import BackgroundTasks\nfrom fastapi.concurrency import (\n    asynccontextmanager,\n    contextmanager_in_threadpool,\n)\nfrom fastapi.dependencies.models import Dependant\nfrom fastapi.exceptions import DependencyScopeError\nfrom fastapi.logger import logger\nfrom fastapi.security.oauth2 import SecurityScopes\nfrom fastapi.types import DependencyCacheKey\nfrom fastapi.utils import create_model_field, get_path_param_names\nfrom pydantic import BaseModel\nfrom pydantic.fields import FieldInfo\nfrom starlette.background import BackgroundTasks as StarletteBackgroundTasks\nfrom starlette.concurrency import run_in_threadpool\nfrom starlette.datastructures import (\n    FormData,\n    Headers,\n    ImmutableMultiDict,\n    QueryParams,\n    UploadFile,\n)\nfrom starlette.requests import HTTPConnection, Request\nfrom starlette.responses import Response\nfrom starlette.websockets import WebSocke
5read_file
{
 "filepath": "fastapi/dependencies/utils.py",
 "start_line": "362"
}
{"status": "error", "error_type": "FileReadError", "error_message": "'>' not supported between instances of 'int' and 'str'"}
full
{"status": "error", "error_type": "FileReadError", "error_message": "'>' not supported between instances of 'int' and 'str'"}
6run_command
{
 "command": "grep -n \"def analyze_param\" fastapi/dependencies/utils.py"
}
{"status": "ok", "stdout": "362:def analyze_param(\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "362:def analyze_param(\n", "stderr": "", "exit_code": 0}
7read_file
{
 "filepath": "fastapi/dependencies/utils.py",
 "start_line": "362",
 "end_line": "500"
}
{"status": "error", "error_type": "FileReadError", "error_message": "'>' not supported between instances of 'int' and 'str'"}
full
{"status": "error", "error_type": "FileReadError", "error_message": "'>' not supported between instances of 'int' and 'str'"}
8run_command
{
 "command": "cat fastapi/dependencies/utils.py | sed -n '362,500p'"
}
{"status": "ok", "stdout": "def analyze_param(\n *,\n param_name: str,\n annotation: Any,\n value: Any,\n is_path_param: bool,\n) -> ParamDetails:\n field_info
full
{"status": "ok", "stdout": "def analyze_param(\n    *,\n    param_name: str,\n    annotation: Any,\n    value: Any,\n    is_path_param: bool,\n) -> ParamDetails:\n    field_info = None\n    depends = None\n    type_annotation: Any = Any\n    use_annotation: Any = Any\n    if annotation is not inspect.Signature.empty:\n        use_annotation = annotation\n        type_annotation = annotation\n    # Extract Annotated info\n    if get_origin(use_annotation) is Annotated:\n        annotated_args = get_args(annotation)\n        type_annotation = annotated_args[0]\n        fastapi_annotations = [\n            arg\n            for arg in annotated_args[1:]\n            if isinstance(arg, (FieldInfo, params.Depends))\n        ]\n        fastapi_specific_annotations = [\n            arg\n            for arg in fastapi_annotations\n            if isinstance(\n                arg,\n                (\n                    params.Param,\n                    params.Body,\n                    params.Depends,\n                ),\n            )\n        ]\n        if fastapi_specific_annotations:\n            fastapi_annotation: Union[FieldInfo, params.Depends, None] = (\n                fastapi_specific_annotations[-1]\n            )\n        else:\n            fastapi_annotation = None\n        # Set default for Annotated FieldInfo\n        if isinstance(fastapi_annotation, FieldInfo):\n            # Copy `field_info` because we mutate `field_info.default` below.\n            field_info = copy_field_info(\n                field_info=fastapi_annotation,\n                annotation=use_annotation,\n            )\n            assert (\n                field_info.default == Undefined or field_info.default == RequiredParam\n            ), (\n                f\"`{field_info.__class__.__name__}` default value cannot be set in\"\n                f\" `Annotated` for {param_name!r}. Set the default value with `=` instead.\"\n            )\n            if value is not inspect.Signature.empty:
9run_command
{
 "command": "cat fastapi/dependencies/utils.py | sed -n '470,485p'"
}
{"status": "ok", "stdout": " if is_path_param:\n # We might check here that `default_value is RequiredParam`, but the fact is that the same\n # parameter might
full
{"status": "ok", "stdout": "        if is_path_param:\n            # We might check here that `default_value is RequiredParam`, but the fact is that the same\n            # parameter might sometimes be a path parameter and sometimes not. See\n            # `tests/test_infer_param_optionality.py` for an example.\n            field_info = params.Path(annotation=use_annotation)\n        elif is_uploadfile_or_nonable_uploadfile_annotation(\n            type_annotation\n        ) or is_uploadfile_sequence_annotation(type_annotation):\n            field_info = params.File(annotation=use_annotation, default=default_value)\n        elif not field_annotation_is_scalar(annotation=type_annotation):\n            field_info = params.Body(annotation=use_annotation, default=default_value)\n        else:\n            field_info = params.Query(annotation=use_annotation, default=default_value)\n\n    field = None\n    # It's a field_info, not a dependency\n", "stderr": "", "exit_code": 0}
10run_command
{
 "command": "cat fastapi/dependencies/utils.py | sed -n '450,470p'"
}
{"status": "ok", "stdout": "\n # Handle non-param type annotations like Request\n if lenient_issubclass(\n type_annotation,\n (\n Request,\n WebSocket,\n HTTPCo
full
{"status": "ok", "stdout": "\n    # Handle non-param type annotations like Request\n    if lenient_issubclass(\n        type_annotation,\n        (\n            Request,\n            WebSocket,\n            HTTPConnection,\n            Response,\n            StarletteBackgroundTasks,\n            SecurityScopes,\n        ),\n    ):\n        assert depends is None, f\"Cannot specify `Depends` for type {type_annotation!r}\"\n        assert field_info is None, (\n            f\"Cannot specify FastAPI annotation for type {type_annotation!r}\"\n        )\n    # Handle default assignations, neither field_info nor depends was not found in Annotated nor default value\n    elif field_info is None and depends is None:\n        default_value = value if value is not inspect.Signature.empty else RequiredParam\n        if is_path_param:\n", "stderr": "", "exit_code": 0}
11edit_file
{
 "filepath": "fastapi/dependencies/utils.py",
 "new_string": "    # Handle non-param type annotations like Request\n    if depends is None and lenient_issubclass(\n        type_annotation,\n        (\n            Request,\n            WebSocket,\n            HTTPConnection,\n            Response,\n            StarletteBackgroundTasks,\n            SecurityScopes,\n        ),\n    ):\n        assert field_info is None, (\n            f\"Cannot specify FastAPI annotation for type {type_annotation!r}\"\n        )\n    # Handle default assignations, neither field_info nor depends was not found in Annotated nor default value\n    elif field_info is None and depends is None:",
 "old_string": "    # Handle non-param type annotations like Request\n    if lenient_issubclass(\n        type_annotation,\n        (\n            Request,\n            WebSocket,\n            HTTPConnection,\n            Response,\n            StarletteBackgroundTasks,\n            SecurityScopes,\n        ),\n    ):\n        assert depends is None, f\"Cannot specify `Depends` for type {type_annotation!r}\"\n        assert field_info is None, (\n            f\"Cannot specify FastAPI annotation for type {type_annotation!r}\"\n        )\n    # Handle default assignations, neither field_info nor depends was not found in Annotated nor default value\n    elif field_info is None and depends is None:"
}
{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "occurrences": 1, "strategy": "exact", "diff": "--- a/fastapi/dependencies/utils.py\n+++ b/fastapi
full
{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "occurrences": 1, "strategy": "exact", "diff": "--- a/fastapi/dependencies/utils.py\n+++ b/fastapi/dependencies/utils.py\n@@ -449,7 +449,7 @@\n         depends = dataclasses.replace(depends, dependency=type_annotation)\n \n     # Handle non-param type annotations like Request\n-    if lenient_issubclass(\n+    if depends is None and lenient_issubclass(\n         type_annotation,\n         (\n             Request,\n@@ -460,7 +460,6 @@\n             SecurityScopes,\n         ),\n     ):\n-        assert depends is None, f\"Cannot specify `Depends` for type {type_annotation!r}\"\n         assert field_info is None, (\n             f\"Cannot specify FastAPI annotation for type {type_annotation!r}\"\n         )\n", "is_truncated": false}
12write_file
{
 "content": "from fastapi import FastAPI, Depends, Response\nfrom fastapi.testclient import TestClient\nfrom typing import Annotated\n\napp = FastAPI()\n\ndef modify_response(response: Response):\n    response.headers[\"X-Custom-Header\"] = \"CustomValue\"\n    return response\n\n@app.get(\"/\")\ndef endpoint(response: Annotated[Response, Depends(modify_response)]):\n    return {\"message\": \"Hello World\"}\n\n@app.get(\"/default\")\ndef endpoint_default(response: Response = Depends(modify_response)):\n    return {\"message\": \"Hello World\"}\n\n@app.get(\"/regular\")\ndef endpoint_regular(response: Response):\n    return {\"message\": \"Hello World\"}\n\nclient = TestClient(app)\n\ndef test_annotated_response_dependency():\n    response = client.get(\"/\")\n    assert response.status_code == 200\n    assert response.headers[\"X-Custom-Header\"] == \"CustomValue\"\n\ndef test_default_response_dependency():\n    response = client.get(\"/default\")\n    assert response.status_code == 200\n    assert response.headers[\"X-Custom-Header\"] == \"CustomValue\"\n\ndef test_regular_response():\n    response = client.get(\"/regular\")\n    assert response.status_code == 200\n    # No custom header here\n    assert \"X-Custom-Header\" not in response.headers\n\nif __name__ == \"__main__\":\n    import pytest\n    pytest.main([__file__])\n",
 "filepath": "/tmp/test_response_dependency.py"
}
{"status": "error", "error_type": "FileWriteError", "error_message": "Path traversal detected: '/tmp/test_response_dependency.py' escapes workspace root."}
full
{"status": "error", "error_type": "FileWriteError", "error_message": "Path traversal detected: '/tmp/test_response_dependency.py' escapes workspace root."}
13write_file
{
 "content": "from fastapi import FastAPI, Depends, Response\nfrom fastapi.testclient import TestClient\nfrom typing import Annotated\n\napp = FastAPI()\n\ndef modify_response(response: Response):\n    response.headers[\"X-Custom-Header\"] = \"CustomValue\"\n    return response\n\n@app.get(\"/\")\ndef endpoint(response: Annotated[Response, Depends(modify_response)]):\n    return {\"message\": \"Hello World\"}\n\n@app.get(\"/default\")\ndef endpoint_default(response: Response = Depends(modify_response)):\n    return {\"message\": \"Hello World\"}\n\n@app.get(\"/regular\")\ndef endpoint_regular(response: Response):\n    return {\"message\": \"Hello World\"}\n\nclient = TestClient(app)\n\ndef test_annotated_response_dependency():\n    response = client.get(\"/\")\n    assert response.status_code == 200\n    assert response.headers[\"X-Custom-Header\"] == \"CustomValue\"\n\ndef test_default_response_dependency():\n    response = client.get(\"/default\")\n    assert response.status_code == 200\n    assert response.headers[\"X-Custom-Header\"] == \"CustomValue\"\n\ndef test_regular_response():\n    response = client.get(\"/regular\")\n    assert response.status_code == 200\n    # No custom header here\n    assert \"X-Custom-Header\" not in response.headers\n\nif __name__ == \"__main__\":\n    import pytest\n    pytest.main([__file__])\n",
 "filepath": "test_response_dependency_scratch.py"
}
{"status": "ok", "filepath": "test_response_dependency_scratch.py", "size": 1255}
full
{"status": "ok", "filepath": "test_response_dependency_scratch.py", "size": 1255}
14run_command
{
 "command": "python test_response_dependency_scratch.py"
}
{"status": "ok", "stdout": "============================= test session starts ==============================\nplatform linux -- Python 3.13.14, pytest-9.1.1, pl
full
{"status": "ok", "stdout": "============================= test session starts ==============================\nplatform linux -- Python 3.13.14, pytest-9.1.1, pluggy-1.6.0\ncodspeed: 5.0.3 (disabled, mode: walltime, callgraph: enabled, timer_resolution: 1.0ns)\nrootdir: /workspace\nconfigfile: pytest.ini (WARNING: ignoring pytest config in pyproject.toml!)\nplugins: anyio-4.15.1, sugar-1.1.1, inline-snapshot-0.35.4, timeout-2.4.0, xdist-3.8.0, codspeed-5.0.3, cov-7.1.0\ncollected 3 items\n\ntest_response_dependency_scratch.py ...                                  [100%]\n\n=============================== warnings summary ===============================\n../../../../../../kaggle/tmp/envs/fastapi/lib/python3.13/site-packages/_pytest/config/__init__.py:1345\n  /kaggle/tmp/envs/fastapi/lib/python3.13/site-packages/_pytest/config/__init__.py:1345: PytestAssertRewriteWarning: Module already imported so cannot be rewritten; anyio\n    self._mark_plugins_for_rewrite(hook, disable_autoload)\n\n-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html\n========================= 3 passed, 1 warning in 0.14s =========================\n", "stderr": "", "exit_code": 0}
15run_command
{
 "command": "rm test_response_dependency_scratch.py"
}
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
16submit_patch
{}
{"status": "ok", "patch_size": 791, "files_changed": 1}
full
{"status": "ok", "patch_size": 791, "files_changed": 1}

Patch

diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py
index fc5dfed8..883f9b74 100644
--- a/fastapi/dependencies/utils.py
+++ b/fastapi/dependencies/utils.py
@@ -449,7 +449,7 @@ def analyze_param(
         depends = dataclasses.replace(depends, dependency=type_annotation)
 
     # Handle non-param type annotations like Request
-    if lenient_issubclass(
+    if depends is None and lenient_issubclass(
         type_annotation,
         (
             Request,
@@ -460,7 +460,6 @@ def analyze_param(
             SecurityScopes,
         ),
     ):
-        assert depends is None, f"Cannot specify `Depends` for type {type_annotation!r}"
         assert field_info is None, (
             f"Cannot specify FastAPI annotation for type {type_annotation!r}"
         )

Test output

show
.......                                                                  [100%]
=============================== warnings summary ===============================
../../../../../../kaggle/tmp/envs/overlays/starlette-0.50.0-py3-none-any/starlette/testclient.py:45
  /kaggle/tmp/envs/overlays/starlette-0.50.0-py3-none-any/starlette/testclient.py:45: DeprecationWarning: The anyio.abc.BlockingPortal alias is deprecated, use anyio.from_thread.BlockingPortal instead.
    _PortalFactoryType = Callable[[], AbstractContextManager[anyio.abc.BlockingPortal]]

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
7 passed, 1 warning in 1.16s