]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
🐛 Fix using `Annotated` in routers or path operations decorated multiple times (...
authorSharon Yogev <31185192+sharonyogev@users.noreply.github.com>
Thu, 13 Apr 2023 17:49:22 +0000 (20:49 +0300)
committerGitHub <noreply@github.com>
Thu, 13 Apr 2023 17:49:22 +0000 (10:49 -0700)
* Fix: copy FieldInfo from Annotated arguments

We need to copy the field_info to prevent ourselves from
mutating it.  This allows multiple path or nested routers ,etc.

* 📝 Add comment in fastapi/dependencies/utils.py

Co-authored-by: Nadav Zingerman <7372858+nzig@users.noreply.github.com>
* ✅ Extend and tweak tests for Annotated

* ✅ Tweak coverage, it's probably covered by a different version of Python

---------

Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
Co-authored-by: Nadav Zingerman <7372858+nzig@users.noreply.github.com>
fastapi/dependencies/utils.py
tests/test_annotated.py

index c581348c9d26c493bb7b2b2e86106c3fd4172c00..f131001ce2c9f103f3e4df10ccd7518d62dc9e69 100644 (file)
@@ -1,7 +1,7 @@
 import dataclasses
 import inspect
 from contextlib import contextmanager
-from copy import deepcopy
+from copy import copy, deepcopy
 from typing import (
     Any,
     Callable,
@@ -383,7 +383,8 @@ def analyze_param(
         ), f"Cannot specify multiple `Annotated` FastAPI arguments for {param_name!r}"
         fastapi_annotation = next(iter(fastapi_annotations), None)
         if isinstance(fastapi_annotation, FieldInfo):
-            field_info = fastapi_annotation
+            # Copy `field_info` because we mutate `field_info.default` below.
+            field_info = copy(fastapi_annotation)
             assert field_info.default is Undefined or field_info.default is Required, (
                 f"`{field_info.__class__.__name__}` default value cannot be set in"
                 f" `Annotated` for {param_name!r}. Set the default value with `=` instead."
index 556019897a53ec28bd2a74fdd12e58473a1155f8..30c8efe0140657185d2eefb45b97602de3bfcae2 100644 (file)
@@ -1,5 +1,5 @@
 import pytest
-from fastapi import FastAPI, Query
+from fastapi import APIRouter, FastAPI, Query
 from fastapi.testclient import TestClient
 from typing_extensions import Annotated
 
@@ -224,3 +224,44 @@ def test_get(path, expected_status, expected_response):
     response = client.get(path)
     assert response.status_code == expected_status
     assert response.json() == expected_response
+
+
+def test_multiple_path():
+    @app.get("/test1")
+    @app.get("/test2")
+    async def test(var: Annotated[str, Query()] = "bar"):
+        return {"foo": var}
+
+    response = client.get("/test1")
+    assert response.status_code == 200
+    assert response.json() == {"foo": "bar"}
+
+    response = client.get("/test1", params={"var": "baz"})
+    assert response.status_code == 200
+    assert response.json() == {"foo": "baz"}
+
+    response = client.get("/test2")
+    assert response.status_code == 200
+    assert response.json() == {"foo": "bar"}
+
+    response = client.get("/test2", params={"var": "baz"})
+    assert response.status_code == 200
+    assert response.json() == {"foo": "baz"}
+
+
+def test_nested_router():
+    app = FastAPI()
+
+    router = APIRouter(prefix="/nested")
+
+    @router.get("/test")
+    async def test(var: Annotated[str, Query()] = "bar"):
+        return {"foo": var}
+
+    app.include_router(router)
+
+    client = TestClient(app)
+
+    response = client.get("/nested/test")
+    assert response.status_code == 200
+    assert response.json() == {"foo": "bar"}