Browse Source

Merge remote-tracking branch 'upstream/master' into sync-german-docs-2025-11-19

pull/14367/head
Yurii Motov 8 months ago
parent
commit
95b1bdf4b0
  1. 6
      docs/en/docs/release-notes.md
  2. 4
      fastapi/_compat/v2.py
  3. 6
      fastapi/dependencies/utils.py
  4. 6
      fastapi/routing.py
  5. 9
      tests/forward_reference_type.py
  6. 15
      tests/test_compat.py
  7. 31
      tests/test_wrapped_method_forward_reference.py

6
docs/en/docs/release-notes.md

@ -9,10 +9,16 @@ 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
* 🐛 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).
* ♻️ 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

4
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):

6
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)

6
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

9
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

15
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."""

31
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}
Loading…
Cancel
Save