From a79ae3d66fb093adfe9f8d15a53af74b49947562 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 2 Dec 2025 08:48:46 -0800 Subject: [PATCH 1/6] =?UTF-8?q?=F0=9F=94=A5=20Remove=20dangling=20extra=20?= =?UTF-8?q?condiitonal=20no=20longer=20needed=20(#14435)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/routing.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/fastapi/routing.py b/fastapi/routing.py index 94e8b0722..c10175b16 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -3,7 +3,6 @@ import email.message import functools import inspect import json -import sys from contextlib import AsyncExitStack, asynccontextmanager from enum import Enum, IntEnum from typing import ( @@ -79,11 +78,6 @@ from starlette.types import AppType, ASGIApp, Lifespan, Receive, Scope, Send from starlette.websockets import WebSocket from typing_extensions import Annotated, deprecated -if sys.version_info >= (3, 13): # pragma: no cover - pass -else: # pragma: no cover - pass - # Copy of starlette.routing.request_response modified to include the # dependencies' AsyncExitStack From cff2236dacb7aedcaf2d2de0873cfbc9bb2b897a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 2 Dec 2025 16:49:12 +0000 Subject: [PATCH 2/6] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f540ac78e..e80df1709 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Refactors +* 🔥 Remove dangling extra condiitonal no longer needed. PR [#14435](https://github.com/fastapi/fastapi/pull/14435) by [@tiangolo](https://github.com/tiangolo). * ♻️ Refactor internals, update `is_coroutine` check to reuse internal supported variants (unwrap, check class). PR [#14434](https://github.com/fastapi/fastapi/pull/14434) by [@tiangolo](https://github.com/tiangolo). ## 0.123.4 From 80d69ae0bb393c728c61c41086b770237b7b676c Mon Sep 17 00:00:00 2001 From: Victorien <65306057+Viicos@users.noreply.github.com> Date: Tue, 2 Dec 2025 17:59:38 +0100 Subject: [PATCH 3/6] =?UTF-8?q?=F0=9F=90=9B=20Fix=20optional=20sequence=20?= =?UTF-8?q?handling=20with=20new=20union=20syntax=20from=20Python=203.10?= =?UTF-8?q?=20(#14430)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- fastapi/_compat/v2.py | 4 ++-- tests/test_compat.py | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py index 543a42dda..0faa7d5a8 100644 --- a/fastapi/_compat/v2.py +++ b/fastapi/_compat/v2.py @@ -17,7 +17,7 @@ from typing import ( from fastapi._compat import may_v1, shared from fastapi.openapi.constants import REF_TEMPLATE -from fastapi.types import IncEx, ModelNameMap +from fastapi.types import IncEx, ModelNameMap, UnionType from pydantic import BaseModel, TypeAdapter, create_model from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError from pydantic import PydanticUndefinedAnnotation as PydanticUndefinedAnnotation @@ -381,7 +381,7 @@ def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: origin_type = get_origin(field.field_info.annotation) or field.field_info.annotation - if origin_type is Union: # Handle optional sequences + if origin_type is Union or origin_type is UnionType: # Handle optional sequences union_args = get_args(field.field_info.annotation) for union_arg in union_args: if union_arg is type(None): diff --git a/tests/test_compat.py b/tests/test_compat.py index c3a97209a..26537c5ab 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -14,7 +14,7 @@ from fastapi.testclient import TestClient from pydantic import BaseModel, ConfigDict from pydantic.fields import FieldInfo -from .utils import needs_py_lt_314, needs_pydanticv2 +from .utils import needs_py310, needs_py_lt_314, needs_pydanticv2 @needs_pydanticv2 @@ -148,6 +148,19 @@ def test_serialize_sequence_value_with_optional_list(): assert isinstance(result, list) +@needs_pydanticv2 +@needs_py310 +def test_serialize_sequence_value_with_optional_list_pipe_union(): + """Test that serialize_sequence_value handles optional lists correctly (with new syntax).""" + from fastapi._compat import v2 + + field_info = FieldInfo(annotation=list[str] | None) + field = v2.ModelField(name="items", field_info=field_info) + result = v2.serialize_sequence_value(field=field, value=["a", "b", "c"]) + assert result == ["a", "b", "c"] + assert isinstance(result, list) + + @needs_pydanticv2 def test_serialize_sequence_value_with_none_first_in_union(): """Test that serialize_sequence_value handles Union[None, List[...]] correctly.""" From 930b27e5fa32ce150ddcea61f0c913435ae71c83 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 2 Dec 2025 17:00:02 +0000 Subject: [PATCH 4/6] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index e80df1709..6fffdc58c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -11,6 +11,10 @@ hide: * ✨ Handle wrapped dependencies. PR [#9555](https://github.com/fastapi/fastapi/pull/9555) by [@phy1729](https://github.com/phy1729). +### Fixes + +* 🐛 Fix optional sequence handling with new union syntax from Python 3.10. PR [#14430](https://github.com/fastapi/fastapi/pull/14430) by [@Viicos](https://github.com/Viicos). + ### Refactors * 🔥 Remove dangling extra condiitonal no longer needed. PR [#14435](https://github.com/fastapi/fastapi/pull/14435) by [@tiangolo](https://github.com/tiangolo). From 1c1e584abd5cb98920bf22966f7a8b79d3ec64be Mon Sep 17 00:00:00 2001 From: "[object Object]" Date: Tue, 2 Dec 2025 09:23:14 -0800 Subject: [PATCH 5/6] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20wrapped?= =?UTF-8?q?=20functions=20(e.g.=20`@functools.wraps()`)=20used=20with=20fo?= =?UTF-8?q?rward=20references=20(#5077)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yurii Karabas <1998uriyyo@gmail.com> Co-authored-by: Sebastián Ramírez --- fastapi/dependencies/utils.py | 6 ++-- tests/forward_reference_type.py | 9 ++++++ .../test_wrapped_method_forward_reference.py | 31 +++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 tests/forward_reference_type.py create mode 100644 tests/test_wrapped_method_forward_reference.py diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index ef3f56417..1a493a9fd 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -192,7 +192,8 @@ def get_flat_params(dependant: Dependant) -> List[ModelField]: def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature: signature = inspect.signature(call) - globalns = getattr(call, "__globals__", {}) + unwrapped = inspect.unwrap(call) + globalns = getattr(unwrapped, "__globals__", {}) typed_params = [ inspect.Parameter( name=param.name, @@ -217,12 +218,13 @@ def get_typed_annotation(annotation: Any, globalns: Dict[str, Any]) -> Any: def get_typed_return_annotation(call: Callable[..., Any]) -> Any: signature = inspect.signature(call) + unwrapped = inspect.unwrap(call) annotation = signature.return_annotation if annotation is inspect.Signature.empty: return None - globalns = getattr(call, "__globals__", {}) + globalns = getattr(unwrapped, "__globals__", {}) return get_typed_annotation(annotation, globalns) diff --git a/tests/forward_reference_type.py b/tests/forward_reference_type.py new file mode 100644 index 000000000..52a0d4a70 --- /dev/null +++ b/tests/forward_reference_type.py @@ -0,0 +1,9 @@ +from pydantic import BaseModel + + +def forwardref_method(input: "ForwardRefModel") -> "ForwardRefModel": + return ForwardRefModel(x=input.x + 1) + + +class ForwardRefModel(BaseModel): + x: int = 0 diff --git a/tests/test_wrapped_method_forward_reference.py b/tests/test_wrapped_method_forward_reference.py new file mode 100644 index 000000000..7403f6002 --- /dev/null +++ b/tests/test_wrapped_method_forward_reference.py @@ -0,0 +1,31 @@ +import functools + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from .forward_reference_type import forwardref_method + + +def passthrough(f): + @functools.wraps(f) + def method(*args, **kwargs): + return f(*args, **kwargs) + + return method + + +def test_wrapped_method_type_inference(): + """ + Regression test ensuring that when a method imported from another module + is decorated with something that sets the __wrapped__ attribute (functools.wraps), + then the types are still processed correctly, including dereferencing of forward + references. + """ + app = FastAPI() + client = TestClient(app) + app.post("/endpoint")(passthrough(forwardref_method)) + app.post("/endpoint2")(passthrough(passthrough(forwardref_method))) + with client: + response = client.post("/endpoint", json={"input": {"x": 0}}) + response2 = client.post("/endpoint2", json={"input": {"x": 0}}) + assert response.json() == response2.json() == {"x": 1} From 4ade6d62e2274e596e396dfde368d32879d935b6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 2 Dec 2025 17:23:36 +0000 Subject: [PATCH 6/6] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 6fffdc58c..d46f1d4f4 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Features +* ✨ Add support for wrapped functions (e.g. `@functools.wraps()`) used with forward references. PR [#5077](https://github.com/fastapi/fastapi/pull/5077) by [@lucaswiman](https://github.com/lucaswiman). * ✨ Handle wrapped dependencies. PR [#9555](https://github.com/fastapi/fastapi/pull/9555) by [@phy1729](https://github.com/phy1729). ### Fixes