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 ### 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). * ✨ 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 ### 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). * ♻️ 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 ## 0.123.4

4
fastapi/_compat/v2.py

@ -17,7 +17,7 @@ from typing import (
from fastapi._compat import may_v1, shared from fastapi._compat import may_v1, shared
from fastapi.openapi.constants import REF_TEMPLATE 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 BaseModel, TypeAdapter, create_model
from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError
from pydantic import PydanticUndefinedAnnotation as PydanticUndefinedAnnotation 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]: def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
origin_type = get_origin(field.field_info.annotation) or field.field_info.annotation 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) union_args = get_args(field.field_info.annotation)
for union_arg in union_args: for union_arg in union_args:
if union_arg is type(None): 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: def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
signature = inspect.signature(call) signature = inspect.signature(call)
globalns = getattr(call, "__globals__", {}) unwrapped = inspect.unwrap(call)
globalns = getattr(unwrapped, "__globals__", {})
typed_params = [ typed_params = [
inspect.Parameter( inspect.Parameter(
name=param.name, 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: def get_typed_return_annotation(call: Callable[..., Any]) -> Any:
signature = inspect.signature(call) signature = inspect.signature(call)
unwrapped = inspect.unwrap(call)
annotation = signature.return_annotation annotation = signature.return_annotation
if annotation is inspect.Signature.empty: if annotation is inspect.Signature.empty:
return None return None
globalns = getattr(call, "__globals__", {}) globalns = getattr(unwrapped, "__globals__", {})
return get_typed_annotation(annotation, globalns) return get_typed_annotation(annotation, globalns)

6
fastapi/routing.py

@ -3,7 +3,6 @@ import email.message
import functools import functools
import inspect import inspect
import json import json
import sys
from contextlib import AsyncExitStack, asynccontextmanager from contextlib import AsyncExitStack, asynccontextmanager
from enum import Enum, IntEnum from enum import Enum, IntEnum
from typing import ( from typing import (
@ -79,11 +78,6 @@ from starlette.types import AppType, ASGIApp, Lifespan, Receive, Scope, Send
from starlette.websockets import WebSocket from starlette.websockets import WebSocket
from typing_extensions import Annotated, deprecated 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 # Copy of starlette.routing.request_response modified to include the
# dependencies' AsyncExitStack # 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 import BaseModel, ConfigDict
from pydantic.fields import FieldInfo 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 @needs_pydanticv2
@ -148,6 +148,19 @@ def test_serialize_sequence_value_with_optional_list():
assert isinstance(result, 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 @needs_pydanticv2
def test_serialize_sequence_value_with_none_first_in_union(): def test_serialize_sequence_value_with_none_first_in_union():
"""Test that serialize_sequence_value handles Union[None, List[...]] correctly.""" """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