From fdf66c825ed70da423724180e91b02345aa36326 Mon Sep 17 00:00:00 2001 From: Sharon Yogev <31185192+sharonyogev@users.noreply.github.com> Date: Thu, 13 Apr 2023 20:49:22 +0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Fix=20using=20`Annotated`=20in?= =?UTF-8?q?=20routers=20or=20path=20operations=20decorated=20multiple=20ti?= =?UTF-8?q?mes=20(#9315)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 Co-authored-by: Nadav Zingerman <7372858+nzig@users.noreply.github.com> --- fastapi/dependencies/utils.py | 5 ++-- tests/test_annotated.py | 43 ++++++++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index c581348c9..f131001ce 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -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." diff --git a/tests/test_annotated.py b/tests/test_annotated.py index 556019897..30c8efe01 100644 --- a/tests/test_annotated.py +++ b/tests/test_annotated.py @@ -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"}