From 3d7e369eae029de8e3bd81a4210efd23ae993271 Mon Sep 17 00:00:00 2001 From: Alex Couper Date: Thu, 23 May 2024 23:30:29 +0000 Subject: [PATCH 01/15] Allow serialization with context --- fastapi/_compat.py | 2 ++ fastapi/applications.py | 36 +++++++++++++++++++++ fastapi/routing.py | 44 ++++++++++++++++++++++++++ tests/test_serialize_response_model.py | 32 +++++++++++++++++++ 4 files changed, 114 insertions(+) diff --git a/fastapi/_compat.py b/fastapi/_compat.py index 06b847b4f..714c9c4d2 100644 --- a/fastapi/_compat.py +++ b/fastapi/_compat.py @@ -143,6 +143,7 @@ if PYDANTIC_V2: exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, + context: dict[str, Any] | None = None, ) -> Any: # What calls this code passes a value that already called # self._type_adapter.validate_python(value) @@ -155,6 +156,7 @@ if PYDANTIC_V2: exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, exclude_none=exclude_none, + context=context, ) def __hash__(self) -> int: diff --git a/fastapi/applications.py b/fastapi/applications.py index 4f5e6f1d9..acd60899b 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -1075,6 +1075,7 @@ class FastAPI(Starlette): response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, response_model_exclude_none: bool = False, + response_model_context: Optional[Dict[str, Any]] = None, include_in_schema: bool = True, response_class: Union[Type[Response], DefaultPlaceholder] = Default( JSONResponse @@ -1105,6 +1106,7 @@ class FastAPI(Starlette): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, @@ -1133,6 +1135,7 @@ class FastAPI(Starlette): response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, response_model_exclude_none: bool = False, + response_model_context: Optional[Dict[str, Any]] = None, include_in_schema: bool = True, response_class: Type[Response] = Default(JSONResponse), name: Optional[str] = None, @@ -1162,6 +1165,7 @@ class FastAPI(Starlette): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, @@ -1711,6 +1715,9 @@ class FastAPI(Starlette): """ ), ] = False, + response_model_context: Annotated[ + Optional[dict], Doc("Context to be used when encoding the response model.") + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -1822,6 +1829,7 @@ class FastAPI(Starlette): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, @@ -2084,6 +2092,9 @@ class FastAPI(Starlette): """ ), ] = False, + response_model_context: Annotated[ + Optional[dict], Doc("Context to be used when encoding the response model.") + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -2200,6 +2211,7 @@ class FastAPI(Starlette): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, @@ -2462,6 +2474,9 @@ class FastAPI(Starlette): """ ), ] = False, + response_model_context: Annotated[ + Optional[dict], Doc("Context to be used when encoding the response model.") + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -2578,6 +2593,7 @@ class FastAPI(Starlette): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, @@ -2840,6 +2856,9 @@ class FastAPI(Starlette): """ ), ] = False, + response_model_context: Annotated[ + Optional[dict], Doc("Context to be used when encoding the response model.") + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -2951,6 +2970,7 @@ class FastAPI(Starlette): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, @@ -3213,6 +3233,9 @@ class FastAPI(Starlette): """ ), ] = False, + response_model_context: Annotated[ + Optional[dict], Doc("Context to be used when encoding the response model.") + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -3324,6 +3347,7 @@ class FastAPI(Starlette): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, @@ -3586,6 +3610,9 @@ class FastAPI(Starlette): """ ), ] = False, + response_model_context: Annotated[ + Optional[dict], Doc("Context to be used when encoding the response model.") + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -3697,6 +3724,7 @@ class FastAPI(Starlette): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, @@ -3959,6 +3987,9 @@ class FastAPI(Starlette): """ ), ] = False, + response_model_context: Annotated[ + Optional[dict], Doc("Context to be used when encoding the response model.") + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -4075,6 +4106,7 @@ class FastAPI(Starlette): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, @@ -4337,6 +4369,9 @@ class FastAPI(Starlette): """ ), ] = False, + response_model_context: Annotated[ + Optional[dict], Doc("Context to be used when encoding the response model.") + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -4448,6 +4483,7 @@ class FastAPI(Starlette): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, diff --git a/fastapi/routing.py b/fastapi/routing.py index fa1351859..afbcd04ee 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -130,6 +130,7 @@ async def serialize_response( exclude_defaults: bool = False, exclude_none: bool = False, is_coroutine: bool = True, + context: dict[str, Any] | None = None, ) -> Any: if field: errors = [] @@ -165,6 +166,7 @@ async def serialize_response( exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, exclude_none=exclude_none, + context=context, ) return jsonable_encoder( @@ -205,6 +207,7 @@ def get_request_handler( response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, response_model_exclude_none: bool = False, + response_model_context: Optional[Dict[str, Any]] = None, dependency_overrides_provider: Optional[Any] = None, ) -> Callable[[Request], Coroutine[Any, Any, Response]]: assert dependant.call is not None, "dependant.call must be a function" @@ -303,6 +306,7 @@ def get_request_handler( exclude_defaults=response_model_exclude_defaults, exclude_none=response_model_exclude_none, is_coroutine=is_coroutine, + context=response_model_context, ) response = actual_response_class(content, **response_args) if not is_body_allowed_for_status_code(response.status_code): @@ -410,6 +414,7 @@ class APIRoute(routing.Route): response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, response_model_exclude_none: bool = False, + response_model_context: Optional[Dict[str, Any]] = None, include_in_schema: bool = True, response_class: Union[Type[Response], DefaultPlaceholder] = Default( JSONResponse @@ -440,6 +445,7 @@ class APIRoute(routing.Route): self.response_model_exclude_unset = response_model_exclude_unset self.response_model_exclude_defaults = response_model_exclude_defaults self.response_model_exclude_none = response_model_exclude_none + self.response_model_context = response_model_context self.include_in_schema = include_in_schema self.response_class = response_class self.dependency_overrides_provider = dependency_overrides_provider @@ -532,6 +538,7 @@ class APIRoute(routing.Route): response_model_exclude_unset=self.response_model_exclude_unset, response_model_exclude_defaults=self.response_model_exclude_defaults, response_model_exclude_none=self.response_model_exclude_none, + response_model_context=self.response_model_context, dependency_overrides_provider=self.dependency_overrides_provider, ) @@ -850,6 +857,7 @@ class APIRouter(routing.Router): response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, response_model_exclude_none: bool = False, + response_model_context: Optional[Dict[str, Any]] = None, include_in_schema: bool = True, response_class: Union[Type[Response], DefaultPlaceholder] = Default( JSONResponse @@ -900,6 +908,7 @@ class APIRouter(routing.Router): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema and self.include_in_schema, response_class=current_response_class, name=name, @@ -931,6 +940,7 @@ class APIRouter(routing.Router): response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, response_model_exclude_none: bool = False, + response_model_context: Optional[Dict[str, Any]] = None, include_in_schema: bool = True, response_class: Type[Response] = Default(JSONResponse), name: Optional[str] = None, @@ -961,6 +971,7 @@ class APIRouter(routing.Router): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, @@ -1269,6 +1280,7 @@ class APIRouter(routing.Router): response_model_exclude_unset=route.response_model_exclude_unset, response_model_exclude_defaults=route.response_model_exclude_defaults, response_model_exclude_none=route.response_model_exclude_none, + response_model_context=route.response_model_context, include_in_schema=route.include_in_schema and self.include_in_schema and include_in_schema, @@ -1563,6 +1575,9 @@ class APIRouter(routing.Router): """ ), ] = False, + response_model_context: Annotated[ + Optional[dict], Doc("Context to be used when encoding the response model.") + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -1678,6 +1693,7 @@ class APIRouter(routing.Router): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, @@ -1940,6 +1956,9 @@ class APIRouter(routing.Router): """ ), ] = False, + response_model_context: Annotated[ + Optional[dict], Doc("Context to be used when encoding the response model.") + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -2060,6 +2079,7 @@ class APIRouter(routing.Router): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, @@ -2322,6 +2342,9 @@ class APIRouter(routing.Router): """ ), ] = False, + response_model_context: Annotated[ + Optional[dict], Doc("Context to be used when encoding the response model.") + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -2442,6 +2465,7 @@ class APIRouter(routing.Router): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, @@ -2704,6 +2728,9 @@ class APIRouter(routing.Router): """ ), ] = False, + response_model_context: Annotated[ + Optional[dict], Doc("Context to be used when encoding the response model.") + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -2819,6 +2846,7 @@ class APIRouter(routing.Router): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, @@ -3081,6 +3109,9 @@ class APIRouter(routing.Router): """ ), ] = False, + response_model_context: Annotated[ + Optional[dict], Doc("Context to be used when encoding the response model.") + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -3196,6 +3227,7 @@ class APIRouter(routing.Router): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, @@ -3458,6 +3490,9 @@ class APIRouter(routing.Router): """ ), ] = False, + response_model_context: Annotated[ + Optional[dict], Doc("Context to be used when encoding the response model.") + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -3578,6 +3613,7 @@ class APIRouter(routing.Router): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, @@ -3840,6 +3876,9 @@ class APIRouter(routing.Router): """ ), ] = False, + response_model_context: Annotated[ + Optional[dict], Doc("Context to be used when encoding the response model.") + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -3960,6 +3999,7 @@ class APIRouter(routing.Router): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, @@ -4222,6 +4262,9 @@ class APIRouter(routing.Router): """ ), ] = False, + response_model_context: Annotated[ + Optional[dict], Doc("Context to be used when encoding the response model.") + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -4342,6 +4385,7 @@ class APIRouter(routing.Router): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, diff --git a/tests/test_serialize_response_model.py b/tests/test_serialize_response_model.py index 3bb46b2e9..612d41fc1 100644 --- a/tests/test_serialize_response_model.py +++ b/tests/test_serialize_response_model.py @@ -12,6 +12,14 @@ class Item(BaseModel): price: Optional[float] = None owner_ids: Optional[List[int]] = None + @model_serializer(mode="wrap") + def _serialize(self, handler, info: SerializationInfo | None = None): + data = handler(self) + if info.context and info.context.get("mode") == "FASTAPI": + if "price" in data: + data.pop("price") + return data + @app.get("/items/valid", response_model=Item) def get_valid(): @@ -83,6 +91,20 @@ def get_validdict_exclude_unset(): } +@app.get( + "/items/validdict-with-context", + response_model=Dict[str, Item], + response_model_context={"mode": "FASTAPI"}, +) +async def get_validdict_with_context(): + + return { + "k1": Item(aliased_name="foo"), + "k2": Item(aliased_name="bar", price=1.0), + "k3": Item(aliased_name="baz", price=2.0, owner_ids=[1, 2, 3]), + } + + client = TestClient(app) @@ -152,3 +174,13 @@ def test_validdict_exclude_unset(): "k2": {"aliased_name": "bar", "price": 1.0}, "k3": {"aliased_name": "baz", "price": 2.0, "owner_ids": [1, 2, 3]}, } + + +def test_validdict_with_context(): + response = client.get("/items/validdict-with-context") + response.raise_for_status() + assert response.json() == { + "k1": {"aliased_name": "foo", "owner_ids": None}, + "k2": {"aliased_name": "bar", "owner_ids": None}, + "k3": {"aliased_name": "baz", "owner_ids": [1, 2, 3]}, + } From 16e7a5cb8a1e02fce3f5ec1dbf55263f569b3612 Mon Sep 17 00:00:00 2001 From: Alex Couper Date: Thu, 23 May 2024 23:47:40 +0000 Subject: [PATCH 02/15] Only use feature if it's available --- fastapi/_compat.py | 6 +++++- tests/test_serialize_response_model.py | 5 ++++- tests/utils.py | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/fastapi/_compat.py b/fastapi/_compat.py index 714c9c4d2..90cec70b5 100644 --- a/fastapi/_compat.py +++ b/fastapi/_compat.py @@ -147,6 +147,10 @@ if PYDANTIC_V2: ) -> Any: # What calls this code passes a value that already called # self._type_adapter.validate_python(value) + # + # context argument was introduced in pydantic 2.7.3 + kwargs = {"context": context} if PYDANTIC_VERSION > "2.7.2" else {} + return self._type_adapter.dump_python( value, mode=mode, @@ -156,7 +160,7 @@ if PYDANTIC_V2: exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, exclude_none=exclude_none, - context=context, + **kwargs, ) def __hash__(self) -> int: diff --git a/tests/test_serialize_response_model.py b/tests/test_serialize_response_model.py index 612d41fc1..754e1a382 100644 --- a/tests/test_serialize_response_model.py +++ b/tests/test_serialize_response_model.py @@ -1,7 +1,9 @@ from typing import Dict, List, Optional +import pytest from fastapi import FastAPI -from pydantic import BaseModel, Field +from fastapi._compat import PYDANTIC_VERSION +from pydantic import BaseModel, Field, SerializationInfo, model_serializer from starlette.testclient import TestClient app = FastAPI() @@ -176,6 +178,7 @@ def test_validdict_exclude_unset(): } +@pytest.mark.skipif(PYDANTIC_VERSION < "2.7.2", reason="requires Pydantic 2.7.3+") def test_validdict_with_context(): response = client.get("/items/validdict-with-context") response.raise_for_status() diff --git a/tests/utils.py b/tests/utils.py index 460c028f7..3f9459b20 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -8,4 +8,4 @@ needs_py310 = pytest.mark.skipif( sys.version_info < (3, 10), reason="requires python3.10+" ) needs_pydanticv2 = pytest.mark.skipif(not PYDANTIC_V2, reason="requires Pydantic v2") -needs_pydanticv1 = pytest.mark.skipif(PYDANTIC_V2, reason="requires Pydantic v1") +needs_pydanticv1 = pytest.mark.skipif(PYDANTIC_V2, reason="requires Pydantic v1") \ No newline at end of file From 78d1336cbb85d26154b73e9fe32ba7d4a5385766 Mon Sep 17 00:00:00 2001 From: Alex Couper Date: Thu, 23 May 2024 23:53:54 +0000 Subject: [PATCH 03/15] Add better docstring --- fastapi/applications.py | 104 ++++++++++++++++++++++++++++++++++++---- fastapi/routing.py | 104 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 192 insertions(+), 16 deletions(-) diff --git a/fastapi/applications.py b/fastapi/applications.py index acd60899b..cda509bb9 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -1716,7 +1716,18 @@ class FastAPI(Starlette): ), ] = False, response_model_context: Annotated[ - Optional[dict], Doc("Context to be used when encoding the response model.") + Optional[Dict[str, Any]], Doc( + """ + Additional context to pass to Pydantic when creating the response. + + This will be passed in as serialization context to the response model. + + Note: This feature is a noop on pydantic < 2.7.2 + + Read more about serialization context in the + [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) + """ + ) ] = None, include_in_schema: Annotated[ bool, @@ -2093,7 +2104,18 @@ class FastAPI(Starlette): ), ] = False, response_model_context: Annotated[ - Optional[dict], Doc("Context to be used when encoding the response model.") + Optional[Dict[str, Any]], Doc( + """ + Additional context to pass to Pydantic when creating the response. + + This will be passed in as serialization context to the response model. + + Note: This feature is a noop on pydantic < 2.7.2 + + Read more about serialization context in the + [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) + """ + ) ] = None, include_in_schema: Annotated[ bool, @@ -2475,7 +2497,18 @@ class FastAPI(Starlette): ), ] = False, response_model_context: Annotated[ - Optional[dict], Doc("Context to be used when encoding the response model.") + Optional[Dict[str, Any]], Doc( + """ + Additional context to pass to Pydantic when creating the response. + + This will be passed in as serialization context to the response model. + + Note: This feature is a noop on pydantic < 2.7.2 + + Read more about serialization context in the + [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) + """ + ) ] = None, include_in_schema: Annotated[ bool, @@ -2857,7 +2890,18 @@ class FastAPI(Starlette): ), ] = False, response_model_context: Annotated[ - Optional[dict], Doc("Context to be used when encoding the response model.") + Optional[Dict[str, Any]], Doc( + """ + Additional context to pass to Pydantic when creating the response. + + This will be passed in as serialization context to the response model. + + Note: This feature is a noop on pydantic < 2.7.2 + + Read more about serialization context in the + [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) + """ + ) ] = None, include_in_schema: Annotated[ bool, @@ -3234,7 +3278,18 @@ class FastAPI(Starlette): ), ] = False, response_model_context: Annotated[ - Optional[dict], Doc("Context to be used when encoding the response model.") + Optional[Dict[str, Any]], Doc( + """ + Additional context to pass to Pydantic when creating the response. + + This will be passed in as serialization context to the response model. + + Note: This feature is a noop on pydantic < 2.7.2 + + Read more about serialization context in the + [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) + """ + ) ] = None, include_in_schema: Annotated[ bool, @@ -3611,7 +3666,18 @@ class FastAPI(Starlette): ), ] = False, response_model_context: Annotated[ - Optional[dict], Doc("Context to be used when encoding the response model.") + Optional[Dict[str, Any]], Doc( + """ + Additional context to pass to Pydantic when creating the response. + + This will be passed in as serialization context to the response model. + + Note: This feature is a noop on pydantic < 2.7.2 + + Read more about serialization context in the + [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) + """ + ) ] = None, include_in_schema: Annotated[ bool, @@ -3988,7 +4054,18 @@ class FastAPI(Starlette): ), ] = False, response_model_context: Annotated[ - Optional[dict], Doc("Context to be used when encoding the response model.") + Optional[Dict[str, Any]], Doc( + """ + Additional context to pass to Pydantic when creating the response. + + This will be passed in as serialization context to the response model. + + Note: This feature is a noop on pydantic < 2.7.2 + + Read more about serialization context in the + [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) + """ + ) ] = None, include_in_schema: Annotated[ bool, @@ -4370,7 +4447,18 @@ class FastAPI(Starlette): ), ] = False, response_model_context: Annotated[ - Optional[dict], Doc("Context to be used when encoding the response model.") + Optional[Dict[str, Any]], Doc( + """ + Additional context to pass to Pydantic when creating the response. + + This will be passed in as serialization context to the response model. + + Note: This feature is a noop on pydantic < 2.7.2 + + Read more about serialization context in the + [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) + """ + ) ] = None, include_in_schema: Annotated[ bool, diff --git a/fastapi/routing.py b/fastapi/routing.py index afbcd04ee..6796dbe5e 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -1576,7 +1576,18 @@ class APIRouter(routing.Router): ), ] = False, response_model_context: Annotated[ - Optional[dict], Doc("Context to be used when encoding the response model.") + Optional[Dict[str, Any]], Doc( + """ + Additional context to pass to Pydantic when creating the response. + + This will be passed in as serialization context to the response model. + + Note: This feature is a noop on pydantic < 2.7.2 + + Read more about serialization context in the + [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) + """ + ), ] = None, include_in_schema: Annotated[ bool, @@ -1957,7 +1968,18 @@ class APIRouter(routing.Router): ), ] = False, response_model_context: Annotated[ - Optional[dict], Doc("Context to be used when encoding the response model.") + Optional[Dict[str, Any]], Doc( + """ + Additional context to pass to Pydantic when creating the response. + + This will be passed in as serialization context to the response model. + + Note: This feature is a noop on pydantic < 2.7.2 + + Read more about serialization context in the + [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) + """ + ) ] = None, include_in_schema: Annotated[ bool, @@ -2343,7 +2365,18 @@ class APIRouter(routing.Router): ), ] = False, response_model_context: Annotated[ - Optional[dict], Doc("Context to be used when encoding the response model.") + Optional[Dict[str, Any]], Doc( + """ + Additional context to pass to Pydantic when creating the response. + + This will be passed in as serialization context to the response model. + + Note: This feature is a noop on pydantic < 2.7.2 + + Read more about serialization context in the + [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) + """ + ) ] = None, include_in_schema: Annotated[ bool, @@ -2729,7 +2762,18 @@ class APIRouter(routing.Router): ), ] = False, response_model_context: Annotated[ - Optional[dict], Doc("Context to be used when encoding the response model.") + Optional[Dict[str, Any]], Doc( + """ + Additional context to pass to Pydantic when creating the response. + + This will be passed in as serialization context to the response model. + + Note: This feature is a noop on pydantic < 2.7.2 + + Read more about serialization context in the + [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) + """ + ) ] = None, include_in_schema: Annotated[ bool, @@ -3110,7 +3154,18 @@ class APIRouter(routing.Router): ), ] = False, response_model_context: Annotated[ - Optional[dict], Doc("Context to be used when encoding the response model.") + Optional[Dict[str, Any]], Doc( + """ + Additional context to pass to Pydantic when creating the response. + + This will be passed in as serialization context to the response model. + + Note: This feature is a noop on pydantic < 2.7.2 + + Read more about serialization context in the + [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) + """ + ) ] = None, include_in_schema: Annotated[ bool, @@ -3491,7 +3546,18 @@ class APIRouter(routing.Router): ), ] = False, response_model_context: Annotated[ - Optional[dict], Doc("Context to be used when encoding the response model.") + Optional[Dict[str, Any]], Doc( + """ + Additional context to pass to Pydantic when creating the response. + + This will be passed in as serialization context to the response model. + + Note: This feature is a noop on pydantic < 2.7.2 + + Read more about serialization context in the + [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) + """ + ) ] = None, include_in_schema: Annotated[ bool, @@ -3877,7 +3943,18 @@ class APIRouter(routing.Router): ), ] = False, response_model_context: Annotated[ - Optional[dict], Doc("Context to be used when encoding the response model.") + Optional[Dict[str, Any]], Doc( + """ + Additional context to pass to Pydantic when creating the response. + + This will be passed in as serialization context to the response model. + + Note: This feature is a noop on pydantic < 2.7.2 + + Read more about serialization context in the + [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) + """ + ) ] = None, include_in_schema: Annotated[ bool, @@ -4263,7 +4340,18 @@ class APIRouter(routing.Router): ), ] = False, response_model_context: Annotated[ - Optional[dict], Doc("Context to be used when encoding the response model.") + Optional[Dict[str, Any]], Doc( + """ + Additional context to pass to Pydantic when creating the response. + + This will be passed in as serialization context to the response model. + + Note: This feature is a noop on pydantic < 2.7.2 + + Read more about serialization context in the + [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) + """ + ) ] = None, include_in_schema: Annotated[ bool, From aa503064c03ddcb12e681e05ecc175f93dacac10 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 24 May 2024 00:10:34 +0000 Subject: [PATCH 04/15] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/applications.py | 40 +++++++++++++++----------- fastapi/routing.py | 38 ++++++++++++++---------- tests/test_serialize_response_model.py | 1 - tests/utils.py | 2 +- 4 files changed, 48 insertions(+), 33 deletions(-) diff --git a/fastapi/applications.py b/fastapi/applications.py index cda509bb9..02a67c6e0 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -1716,7 +1716,8 @@ class FastAPI(Starlette): ), ] = False, response_model_context: Annotated[ - Optional[Dict[str, Any]], Doc( + Optional[Dict[str, Any]], + Doc( """ Additional context to pass to Pydantic when creating the response. @@ -1727,7 +1728,7 @@ class FastAPI(Starlette): Read more about serialization context in the [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) """ - ) + ), ] = None, include_in_schema: Annotated[ bool, @@ -2104,7 +2105,8 @@ class FastAPI(Starlette): ), ] = False, response_model_context: Annotated[ - Optional[Dict[str, Any]], Doc( + Optional[Dict[str, Any]], + Doc( """ Additional context to pass to Pydantic when creating the response. @@ -2115,7 +2117,7 @@ class FastAPI(Starlette): Read more about serialization context in the [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) """ - ) + ), ] = None, include_in_schema: Annotated[ bool, @@ -2497,7 +2499,8 @@ class FastAPI(Starlette): ), ] = False, response_model_context: Annotated[ - Optional[Dict[str, Any]], Doc( + Optional[Dict[str, Any]], + Doc( """ Additional context to pass to Pydantic when creating the response. @@ -2508,7 +2511,7 @@ class FastAPI(Starlette): Read more about serialization context in the [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) """ - ) + ), ] = None, include_in_schema: Annotated[ bool, @@ -2890,7 +2893,8 @@ class FastAPI(Starlette): ), ] = False, response_model_context: Annotated[ - Optional[Dict[str, Any]], Doc( + Optional[Dict[str, Any]], + Doc( """ Additional context to pass to Pydantic when creating the response. @@ -2901,7 +2905,7 @@ class FastAPI(Starlette): Read more about serialization context in the [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) """ - ) + ), ] = None, include_in_schema: Annotated[ bool, @@ -3278,7 +3282,8 @@ class FastAPI(Starlette): ), ] = False, response_model_context: Annotated[ - Optional[Dict[str, Any]], Doc( + Optional[Dict[str, Any]], + Doc( """ Additional context to pass to Pydantic when creating the response. @@ -3289,7 +3294,7 @@ class FastAPI(Starlette): Read more about serialization context in the [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) """ - ) + ), ] = None, include_in_schema: Annotated[ bool, @@ -3666,7 +3671,8 @@ class FastAPI(Starlette): ), ] = False, response_model_context: Annotated[ - Optional[Dict[str, Any]], Doc( + Optional[Dict[str, Any]], + Doc( """ Additional context to pass to Pydantic when creating the response. @@ -3677,7 +3683,7 @@ class FastAPI(Starlette): Read more about serialization context in the [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) """ - ) + ), ] = None, include_in_schema: Annotated[ bool, @@ -4054,7 +4060,8 @@ class FastAPI(Starlette): ), ] = False, response_model_context: Annotated[ - Optional[Dict[str, Any]], Doc( + Optional[Dict[str, Any]], + Doc( """ Additional context to pass to Pydantic when creating the response. @@ -4065,7 +4072,7 @@ class FastAPI(Starlette): Read more about serialization context in the [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) """ - ) + ), ] = None, include_in_schema: Annotated[ bool, @@ -4447,7 +4454,8 @@ class FastAPI(Starlette): ), ] = False, response_model_context: Annotated[ - Optional[Dict[str, Any]], Doc( + Optional[Dict[str, Any]], + Doc( """ Additional context to pass to Pydantic when creating the response. @@ -4458,7 +4466,7 @@ class FastAPI(Starlette): Read more about serialization context in the [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) """ - ) + ), ] = None, include_in_schema: Annotated[ bool, diff --git a/fastapi/routing.py b/fastapi/routing.py index 6796dbe5e..165a3bf50 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -1576,7 +1576,8 @@ class APIRouter(routing.Router): ), ] = False, response_model_context: Annotated[ - Optional[Dict[str, Any]], Doc( + Optional[Dict[str, Any]], + Doc( """ Additional context to pass to Pydantic when creating the response. @@ -1968,7 +1969,8 @@ class APIRouter(routing.Router): ), ] = False, response_model_context: Annotated[ - Optional[Dict[str, Any]], Doc( + Optional[Dict[str, Any]], + Doc( """ Additional context to pass to Pydantic when creating the response. @@ -1979,7 +1981,7 @@ class APIRouter(routing.Router): Read more about serialization context in the [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) """ - ) + ), ] = None, include_in_schema: Annotated[ bool, @@ -2365,7 +2367,8 @@ class APIRouter(routing.Router): ), ] = False, response_model_context: Annotated[ - Optional[Dict[str, Any]], Doc( + Optional[Dict[str, Any]], + Doc( """ Additional context to pass to Pydantic when creating the response. @@ -2376,7 +2379,7 @@ class APIRouter(routing.Router): Read more about serialization context in the [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) """ - ) + ), ] = None, include_in_schema: Annotated[ bool, @@ -2762,7 +2765,8 @@ class APIRouter(routing.Router): ), ] = False, response_model_context: Annotated[ - Optional[Dict[str, Any]], Doc( + Optional[Dict[str, Any]], + Doc( """ Additional context to pass to Pydantic when creating the response. @@ -2773,7 +2777,7 @@ class APIRouter(routing.Router): Read more about serialization context in the [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) """ - ) + ), ] = None, include_in_schema: Annotated[ bool, @@ -3154,7 +3158,8 @@ class APIRouter(routing.Router): ), ] = False, response_model_context: Annotated[ - Optional[Dict[str, Any]], Doc( + Optional[Dict[str, Any]], + Doc( """ Additional context to pass to Pydantic when creating the response. @@ -3165,7 +3170,7 @@ class APIRouter(routing.Router): Read more about serialization context in the [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) """ - ) + ), ] = None, include_in_schema: Annotated[ bool, @@ -3546,7 +3551,8 @@ class APIRouter(routing.Router): ), ] = False, response_model_context: Annotated[ - Optional[Dict[str, Any]], Doc( + Optional[Dict[str, Any]], + Doc( """ Additional context to pass to Pydantic when creating the response. @@ -3557,7 +3563,7 @@ class APIRouter(routing.Router): Read more about serialization context in the [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) """ - ) + ), ] = None, include_in_schema: Annotated[ bool, @@ -3943,7 +3949,8 @@ class APIRouter(routing.Router): ), ] = False, response_model_context: Annotated[ - Optional[Dict[str, Any]], Doc( + Optional[Dict[str, Any]], + Doc( """ Additional context to pass to Pydantic when creating the response. @@ -3954,7 +3961,7 @@ class APIRouter(routing.Router): Read more about serialization context in the [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) """ - ) + ), ] = None, include_in_schema: Annotated[ bool, @@ -4340,7 +4347,8 @@ class APIRouter(routing.Router): ), ] = False, response_model_context: Annotated[ - Optional[Dict[str, Any]], Doc( + Optional[Dict[str, Any]], + Doc( """ Additional context to pass to Pydantic when creating the response. @@ -4351,7 +4359,7 @@ class APIRouter(routing.Router): Read more about serialization context in the [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) """ - ) + ), ] = None, include_in_schema: Annotated[ bool, diff --git a/tests/test_serialize_response_model.py b/tests/test_serialize_response_model.py index 754e1a382..b03a521c5 100644 --- a/tests/test_serialize_response_model.py +++ b/tests/test_serialize_response_model.py @@ -99,7 +99,6 @@ def get_validdict_exclude_unset(): response_model_context={"mode": "FASTAPI"}, ) async def get_validdict_with_context(): - return { "k1": Item(aliased_name="foo"), "k2": Item(aliased_name="bar", price=1.0), diff --git a/tests/utils.py b/tests/utils.py index 3f9459b20..460c028f7 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -8,4 +8,4 @@ needs_py310 = pytest.mark.skipif( sys.version_info < (3, 10), reason="requires python3.10+" ) needs_pydanticv2 = pytest.mark.skipif(not PYDANTIC_V2, reason="requires Pydantic v2") -needs_pydanticv1 = pytest.mark.skipif(PYDANTIC_V2, reason="requires Pydantic v1") \ No newline at end of file +needs_pydanticv1 = pytest.mark.skipif(PYDANTIC_V2, reason="requires Pydantic v1") From 45b3e76e5328b8db9f9ca1380d10d10a39a9c736 Mon Sep 17 00:00:00 2001 From: Alex Couper Date: Fri, 24 May 2024 00:19:07 +0000 Subject: [PATCH 05/15] Support older versions --- fastapi/_compat.py | 3 ++- fastapi/routing.py | 2 +- tests/test_serialize_response_model.py | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/fastapi/_compat.py b/fastapi/_compat.py index 90cec70b5..c260f33a6 100644 --- a/fastapi/_compat.py +++ b/fastapi/_compat.py @@ -10,6 +10,7 @@ from typing import ( FrozenSet, List, Mapping, + Optional, Sequence, Set, Tuple, @@ -143,7 +144,7 @@ if PYDANTIC_V2: exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, - context: dict[str, Any] | None = None, + context: Optional[Dict[str, Any]]= None, ) -> Any: # What calls this code passes a value that already called # self._type_adapter.validate_python(value) diff --git a/fastapi/routing.py b/fastapi/routing.py index 165a3bf50..ad15b3b8e 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -130,7 +130,7 @@ async def serialize_response( exclude_defaults: bool = False, exclude_none: bool = False, is_coroutine: bool = True, - context: dict[str, Any] | None = None, + context: Optional[Dict[str, Any]]= None, ) -> Any: if field: errors = [] diff --git a/tests/test_serialize_response_model.py b/tests/test_serialize_response_model.py index b03a521c5..7ef7879f9 100644 --- a/tests/test_serialize_response_model.py +++ b/tests/test_serialize_response_model.py @@ -3,7 +3,7 @@ from typing import Dict, List, Optional import pytest from fastapi import FastAPI from fastapi._compat import PYDANTIC_VERSION -from pydantic import BaseModel, Field, SerializationInfo, model_serializer +from pydantic import BaseModel, Field, model_serializer from starlette.testclient import TestClient app = FastAPI() @@ -15,7 +15,7 @@ class Item(BaseModel): owner_ids: Optional[List[int]] = None @model_serializer(mode="wrap") - def _serialize(self, handler, info: SerializationInfo | None = None): + def _serialize(self, handler, info): data = handler(self) if info.context and info.context.get("mode") == "FASTAPI": if "price" in data: From 7a14e73e572616830b67039fdc9a2d38c0a0cc2d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 24 May 2024 00:20:42 +0000 Subject: [PATCH 06/15] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/_compat.py | 2 +- fastapi/routing.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastapi/_compat.py b/fastapi/_compat.py index c260f33a6..d3be652c7 100644 --- a/fastapi/_compat.py +++ b/fastapi/_compat.py @@ -144,7 +144,7 @@ if PYDANTIC_V2: exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, - context: Optional[Dict[str, Any]]= None, + context: Optional[Dict[str, Any]] = None, ) -> Any: # What calls this code passes a value that already called # self._type_adapter.validate_python(value) diff --git a/fastapi/routing.py b/fastapi/routing.py index ad15b3b8e..07c9c971c 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -130,7 +130,7 @@ async def serialize_response( exclude_defaults: bool = False, exclude_none: bool = False, is_coroutine: bool = True, - context: Optional[Dict[str, Any]]= None, + context: Optional[Dict[str, Any]] = None, ) -> Any: if field: errors = [] From 886c1f4643f4de07ad70894bab949d631706ee6b Mon Sep 17 00:00:00 2001 From: Alex Couper Date: Fri, 24 May 2024 00:27:15 +0000 Subject: [PATCH 07/15] Shield pydantic v2 things from pydantic v1 path --- tests/test_serialize_response_model.py | 54 +++++++++++++++----------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/tests/test_serialize_response_model.py b/tests/test_serialize_response_model.py index 7ef7879f9..30e87aa00 100644 --- a/tests/test_serialize_response_model.py +++ b/tests/test_serialize_response_model.py @@ -1,10 +1,11 @@ from typing import Dict, List, Optional import pytest +from pydantic import BaseModel, Field +from starlette.testclient import TestClient + from fastapi import FastAPI from fastapi._compat import PYDANTIC_VERSION -from pydantic import BaseModel, Field, model_serializer -from starlette.testclient import TestClient app = FastAPI() @@ -14,14 +15,6 @@ class Item(BaseModel): price: Optional[float] = None owner_ids: Optional[List[int]] = None - @model_serializer(mode="wrap") - def _serialize(self, handler, info): - data = handler(self) - if info.context and info.context.get("mode") == "FASTAPI": - if "price" in data: - data.pop("price") - return data - @app.get("/items/valid", response_model=Item) def get_valid(): @@ -93,18 +86,6 @@ def get_validdict_exclude_unset(): } -@app.get( - "/items/validdict-with-context", - response_model=Dict[str, Item], - response_model_context={"mode": "FASTAPI"}, -) -async def get_validdict_with_context(): - return { - "k1": Item(aliased_name="foo"), - "k2": Item(aliased_name="bar", price=1.0), - "k3": Item(aliased_name="baz", price=2.0, owner_ids=[1, 2, 3]), - } - client = TestClient(app) @@ -179,6 +160,35 @@ def test_validdict_exclude_unset(): @pytest.mark.skipif(PYDANTIC_VERSION < "2.7.2", reason="requires Pydantic 2.7.3+") def test_validdict_with_context(): + from pydantic import SerializationInfo, model_serializer + class Item(BaseModel): + name: str = Field(alias="aliased_name") + price: Optional[float] = None + owner_ids: Optional[List[int]] = None + + @model_serializer(mode="wrap") + def _serialize(self, handler, info: SerializationInfo): + data = handler(self) + if info.context and info.context.get("mode") == "FASTAPI": + if "price" in data: + data.pop("price") + return data + + app_new = FastAPI() + + @app_new.get( + "/items/validdict-with-context", + response_model=Dict[str, Item], + response_model_context={"mode": "FASTAPI"}, + ) + async def get_validdict_with_context(): + return { + "k1": Item(aliased_name="foo"), + "k2": Item(aliased_name="bar", price=1.0), + "k3": Item(aliased_name="baz", price=2.0, owner_ids=[1, 2, 3]), + } + + client = TestClient(app_new) response = client.get("/items/validdict-with-context") response.raise_for_status() assert response.json() == { From c2d27f54488c34b0d4126913bc7a8c872168bd03 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 24 May 2024 00:27:57 +0000 Subject: [PATCH 08/15] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_serialize_response_model.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/test_serialize_response_model.py b/tests/test_serialize_response_model.py index 30e87aa00..98da41f5d 100644 --- a/tests/test_serialize_response_model.py +++ b/tests/test_serialize_response_model.py @@ -1,11 +1,10 @@ from typing import Dict, List, Optional import pytest -from pydantic import BaseModel, Field -from starlette.testclient import TestClient - from fastapi import FastAPI from fastapi._compat import PYDANTIC_VERSION +from pydantic import BaseModel, Field +from starlette.testclient import TestClient app = FastAPI() @@ -86,7 +85,6 @@ def get_validdict_exclude_unset(): } - client = TestClient(app) @@ -161,6 +159,7 @@ def test_validdict_exclude_unset(): @pytest.mark.skipif(PYDANTIC_VERSION < "2.7.2", reason="requires Pydantic 2.7.3+") def test_validdict_with_context(): from pydantic import SerializationInfo, model_serializer + class Item(BaseModel): name: str = Field(alias="aliased_name") price: Optional[float] = None From 8b802d1e1e213355cdbbfb1274e8bc303d546f15 Mon Sep 17 00:00:00 2001 From: Alex Couper Date: Fri, 24 May 2024 00:36:45 +0000 Subject: [PATCH 09/15] Add a test for pydantic < 2.7.3 --- fastapi/_compat.py | 2 +- tests/test_serialize_response_model.py | 49 ++++++++++++++++++-------- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/fastapi/_compat.py b/fastapi/_compat.py index d3be652c7..b37338766 100644 --- a/fastapi/_compat.py +++ b/fastapi/_compat.py @@ -150,7 +150,7 @@ if PYDANTIC_V2: # self._type_adapter.validate_python(value) # # context argument was introduced in pydantic 2.7.3 - kwargs = {"context": context} if PYDANTIC_VERSION > "2.7.2" else {} + kwargs = {"context": context} if PYDANTIC_VERSION >= "2.7.3" else {} return self._type_adapter.dump_python( value, diff --git a/tests/test_serialize_response_model.py b/tests/test_serialize_response_model.py index 98da41f5d..210a44c8b 100644 --- a/tests/test_serialize_response_model.py +++ b/tests/test_serialize_response_model.py @@ -2,10 +2,11 @@ from typing import Dict, List, Optional import pytest from fastapi import FastAPI -from fastapi._compat import PYDANTIC_VERSION +from fastapi._compat import PYDANTIC_V2, PYDANTIC_VERSION from pydantic import BaseModel, Field from starlette.testclient import TestClient + app = FastAPI() @@ -156,21 +157,20 @@ def test_validdict_exclude_unset(): } -@pytest.mark.skipif(PYDANTIC_VERSION < "2.7.2", reason="requires Pydantic 2.7.3+") -def test_validdict_with_context(): +if PYDANTIC_V2: from pydantic import SerializationInfo, model_serializer class Item(BaseModel): name: str = Field(alias="aliased_name") - price: Optional[float] = None + secret: Optional[str] = None owner_ids: Optional[List[int]] = None @model_serializer(mode="wrap") def _serialize(self, handler, info: SerializationInfo): data = handler(self) if info.context and info.context.get("mode") == "FASTAPI": - if "price" in data: - data.pop("price") + if "secret" in data: + data.pop("secret") return data app_new = FastAPI() @@ -183,15 +183,34 @@ def test_validdict_with_context(): async def get_validdict_with_context(): return { "k1": Item(aliased_name="foo"), - "k2": Item(aliased_name="bar", price=1.0), - "k3": Item(aliased_name="baz", price=2.0, owner_ids=[1, 2, 3]), + "k2": Item(aliased_name="bar", secret="sEcReT"), + "k3": Item(aliased_name="baz", secret="sEcReT", owner_ids=[1, 2, 3]), } client = TestClient(app_new) - response = client.get("/items/validdict-with-context") - response.raise_for_status() - assert response.json() == { - "k1": {"aliased_name": "foo", "owner_ids": None}, - "k2": {"aliased_name": "bar", "owner_ids": None}, - "k3": {"aliased_name": "baz", "owner_ids": [1, 2, 3]}, - } + + @pytest.mark.skipif(PYDANTIC_VERSION < "2.7.3", reason="requires Pydantic v2.7.3+") + def test_validdict_with_context__pydantic_supported(): + response = client.get("/items/validdict-with-context") + response.raise_for_status() + + expected_response = { + "k1": {"aliased_name": "foo", "owner_ids": None}, + "k2": {"aliased_name": "bar", "owner_ids": None}, + "k3": {"aliased_name": "baz", "owner_ids": [1, 2, 3]}, + } + + assert response.json() == expected_response + + @pytest.mark.skipif(PYDANTIC_VERSION >= "2.7.3", reason="Pydantic supports the feature from this point on") + def test_validdict_with_context__pre_pydantic_support(): + response = client.get("/items/validdict-with-context") + response.raise_for_status() + + expected_response = { + "k1": {"aliased_name": "foo", "owner_ids": None, "secret": None}, + "k2": {"aliased_name": "bar", "owner_ids": None, "secret": "sEcReT"}, + "k3": {"aliased_name": "baz", "owner_ids": [1, 2, 3], "secret": "sEcReT"}, + } + + assert response.json() == expected_response \ No newline at end of file From bf51d6025cca47cd0a4de5a571da0cc6307cbf5b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 24 May 2024 00:39:47 +0000 Subject: [PATCH 10/15] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_serialize_response_model.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test_serialize_response_model.py b/tests/test_serialize_response_model.py index 210a44c8b..790a1ebd7 100644 --- a/tests/test_serialize_response_model.py +++ b/tests/test_serialize_response_model.py @@ -6,7 +6,6 @@ from fastapi._compat import PYDANTIC_V2, PYDANTIC_VERSION from pydantic import BaseModel, Field from starlette.testclient import TestClient - app = FastAPI() @@ -202,7 +201,10 @@ if PYDANTIC_V2: assert response.json() == expected_response - @pytest.mark.skipif(PYDANTIC_VERSION >= "2.7.3", reason="Pydantic supports the feature from this point on") + @pytest.mark.skipif( + PYDANTIC_VERSION >= "2.7.3", + reason="Pydantic supports the feature from this point on", + ) def test_validdict_with_context__pre_pydantic_support(): response = client.get("/items/validdict-with-context") response.raise_for_status() @@ -213,4 +215,4 @@ if PYDANTIC_V2: "k3": {"aliased_name": "baz", "owner_ids": [1, 2, 3], "secret": "sEcReT"}, } - assert response.json() == expected_response \ No newline at end of file + assert response.json() == expected_response From 14ed35937fb775a31b7bf5a849ba8f7460bd46e8 Mon Sep 17 00:00:00 2001 From: Alex Couper Date: Fri, 24 May 2024 00:43:19 +0000 Subject: [PATCH 11/15] Rename client --- tests/test_serialize_response_model.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/test_serialize_response_model.py b/tests/test_serialize_response_model.py index 790a1ebd7..470946357 100644 --- a/tests/test_serialize_response_model.py +++ b/tests/test_serialize_response_model.py @@ -159,7 +159,7 @@ def test_validdict_exclude_unset(): if PYDANTIC_V2: from pydantic import SerializationInfo, model_serializer - class Item(BaseModel): + class MultiUseItem(BaseModel): name: str = Field(alias="aliased_name") secret: Optional[str] = None owner_ids: Optional[List[int]] = None @@ -172,25 +172,25 @@ if PYDANTIC_V2: data.pop("secret") return data - app_new = FastAPI() + app_v2 = FastAPI() - @app_new.get( + @app_v2.get( "/items/validdict-with-context", - response_model=Dict[str, Item], + response_model=Dict[str, MultiUseItem], response_model_context={"mode": "FASTAPI"}, ) async def get_validdict_with_context(): return { - "k1": Item(aliased_name="foo"), - "k2": Item(aliased_name="bar", secret="sEcReT"), - "k3": Item(aliased_name="baz", secret="sEcReT", owner_ids=[1, 2, 3]), + "k1": MultiUseItem(aliased_name="foo"), + "k2": MultiUseItem(aliased_name="bar", secret="sEcReT"), + "k3": MultiUseItem(aliased_name="baz", secret="sEcReT", owner_ids=[1, 2, 3]), } - client = TestClient(app_new) + client_v2 = TestClient(app_v2) @pytest.mark.skipif(PYDANTIC_VERSION < "2.7.3", reason="requires Pydantic v2.7.3+") def test_validdict_with_context__pydantic_supported(): - response = client.get("/items/validdict-with-context") + response = client_v2.get("/items/validdict-with-context") response.raise_for_status() expected_response = { @@ -206,7 +206,7 @@ if PYDANTIC_V2: reason="Pydantic supports the feature from this point on", ) def test_validdict_with_context__pre_pydantic_support(): - response = client.get("/items/validdict-with-context") + response = client_v2.get("/items/validdict-with-context") response.raise_for_status() expected_response = { From 25716e308d4282e305693aa5cbd923ad8026f56d Mon Sep 17 00:00:00 2001 From: Alex Couper Date: Fri, 9 Aug 2024 13:30:19 +0000 Subject: [PATCH 12/15] Update now that pydantic has been released --- fastapi/_compat.py | 64 +++++++++++++------------- fastapi/applications.py | 37 +++++++-------- fastapi/routing.py | 52 ++++++++++----------- tests/test_serialize_response_model.py | 9 ++-- 4 files changed, 83 insertions(+), 79 deletions(-) diff --git a/fastapi/_compat.py b/fastapi/_compat.py index b37338766..3e0261ae8 100644 --- a/fastapi/_compat.py +++ b/fastapi/_compat.py @@ -18,13 +18,14 @@ from typing import ( Union, ) -from fastapi.exceptions import RequestErrorModel -from fastapi.types import IncEx, ModelNameMap, UnionType from pydantic import BaseModel, create_model from pydantic.version import VERSION as P_VERSION from starlette.datastructures import UploadFile from typing_extensions import Annotated, Literal, get_args, get_origin +from fastapi.exceptions import RequestErrorModel +from fastapi.types import IncEx, ModelNameMap, UnionType + # Reassign variable to make it reexported for mypy PYDANTIC_VERSION = P_VERSION PYDANTIC_V2 = PYDANTIC_VERSION.startswith("2.") @@ -50,8 +51,8 @@ if PYDANTIC_V2: from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError from pydantic import TypeAdapter from pydantic import ValidationError as ValidationError - from pydantic._internal._schema_generation_shared import ( # type: ignore[attr-defined] - GetJsonSchemaHandler as GetJsonSchemaHandler, + from pydantic._internal._schema_generation_shared import ( + GetJsonSchemaHandler as GetJsonSchemaHandler, # type: ignore[attr-defined] ) from pydantic._internal._typing_extra import eval_type_lenient from pydantic._internal._utils import lenient_issubclass as lenient_issubclass @@ -67,8 +68,8 @@ if PYDANTIC_V2: with_info_plain_validator_function as with_info_plain_validator_function, ) except ImportError: # pragma: no cover - from pydantic_core.core_schema import ( - general_plain_validator_function as with_info_plain_validator_function, # noqa: F401 + from pydantic_core.core_schema import ( # noqa: F401 + general_plain_validator_function as with_info_plain_validator_function, ) Required = PydanticUndefined @@ -149,8 +150,8 @@ if PYDANTIC_V2: # What calls this code passes a value that already called # self._type_adapter.validate_python(value) # - # context argument was introduced in pydantic 2.7.3 - kwargs = {"context": context} if PYDANTIC_VERSION >= "2.7.3" else {} + # context argument was introduced in pydantic 2.8 + kwargs = {"context": context} if PYDANTIC_VERSION >= "2.8" else {} return self._type_adapter.dump_python( value, @@ -287,17 +288,16 @@ if PYDANTIC_V2: return BodyModel else: - from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX from pydantic import AnyUrl as Url # noqa: F401 - from pydantic import ( # type: ignore[assignment] - BaseConfig as BaseConfig, # noqa: F401 + from pydantic import ( # type: ignore[assignment]; noqa: F401 + BaseConfig as BaseConfig, ) from pydantic import ValidationError as ValidationError # noqa: F401 - from pydantic.class_validators import ( # type: ignore[no-redef] - Validator as Validator, # noqa: F401 + from pydantic.class_validators import ( # type: ignore[no-redef]; noqa: F401 + Validator as Validator, ) - from pydantic.error_wrappers import ( # type: ignore[no-redef] - ErrorWrapper as ErrorWrapper, # noqa: F401 + from pydantic.error_wrappers import ( # type: ignore[no-redef]; noqa: F401 + ErrorWrapper as ErrorWrapper, ) from pydantic.errors import MissingError from pydantic.fields import ( # type: ignore[attr-defined] @@ -310,34 +310,36 @@ else: SHAPE_TUPLE_ELLIPSIS, ) from pydantic.fields import FieldInfo as FieldInfo - from pydantic.fields import ( # type: ignore[no-redef,attr-defined] - ModelField as ModelField, # noqa: F401 + from pydantic.fields import ( # type: ignore[no-redef,attr-defined]; noqa: F401 + ModelField as ModelField, ) - from pydantic.fields import ( # type: ignore[no-redef,attr-defined] - Required as Required, # noqa: F401 + from pydantic.fields import ( # type: ignore[no-redef,attr-defined]; noqa: F401 + Required as Required, ) - from pydantic.fields import ( # type: ignore[no-redef,attr-defined] - Undefined as Undefined, + from pydantic.fields import ( + Undefined as Undefined, # type: ignore[no-redef,attr-defined] ) - from pydantic.fields import ( # type: ignore[no-redef, attr-defined] - UndefinedType as UndefinedType, # noqa: F401 + from pydantic.fields import ( # type: ignore[no-redef, attr-defined]; noqa: F401 + UndefinedType as UndefinedType, + ) + from pydantic.schema import field_schema + from pydantic.schema import ( # type: ignore[no-redef] # noqa: F401 + get_annotation_from_field_info as get_annotation_from_field_info, ) from pydantic.schema import ( - field_schema, get_flat_models_from_fields, get_model_name_map, model_process_schema, ) - from pydantic.schema import ( # type: ignore[no-redef] # noqa: F401 - get_annotation_from_field_info as get_annotation_from_field_info, + from pydantic.typing import ( # type: ignore[no-redef]; noqa: F401 + evaluate_forwardref as evaluate_forwardref, ) - from pydantic.typing import ( # type: ignore[no-redef] - evaluate_forwardref as evaluate_forwardref, # noqa: F401 - ) - from pydantic.utils import ( # type: ignore[no-redef] - lenient_issubclass as lenient_issubclass, # noqa: F401 + from pydantic.utils import ( # type: ignore[no-redef]; noqa: F401 + lenient_issubclass as lenient_issubclass, ) + from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX + GetJsonSchemaHandler = Any # type: ignore[assignment,misc] JsonSchemaValue = Dict[str, Any] # type: ignore[misc] CoreSchema = Any # type: ignore[assignment,misc] diff --git a/fastapi/applications.py b/fastapi/applications.py index 02a67c6e0..63c797f55 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -13,6 +13,17 @@ from typing import ( Union, ) +from starlette.applications import Starlette +from starlette.datastructures import State +from starlette.exceptions import HTTPException +from starlette.middleware import Middleware +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request +from starlette.responses import HTMLResponse, JSONResponse, Response +from starlette.routing import BaseRoute +from starlette.types import ASGIApp, Lifespan, Receive, Scope, Send +from typing_extensions import Annotated, Doc, deprecated + from fastapi import routing from fastapi.datastructures import Default, DefaultPlaceholder from fastapi.exception_handlers import ( @@ -31,16 +42,6 @@ from fastapi.openapi.utils import get_openapi from fastapi.params import Depends from fastapi.types import DecoratedCallable, IncEx from fastapi.utils import generate_unique_id -from starlette.applications import Starlette -from starlette.datastructures import State -from starlette.exceptions import HTTPException -from starlette.middleware import Middleware -from starlette.middleware.base import BaseHTTPMiddleware -from starlette.requests import Request -from starlette.responses import HTMLResponse, JSONResponse, Response -from starlette.routing import BaseRoute -from starlette.types import ASGIApp, Lifespan, Receive, Scope, Send -from typing_extensions import Annotated, Doc, deprecated AppType = TypeVar("AppType", bound="FastAPI") @@ -1723,7 +1724,7 @@ class FastAPI(Starlette): This will be passed in as serialization context to the response model. - Note: This feature is a noop on pydantic < 2.7.2 + Note: This feature is a noop on pydantic < 2.8 Read more about serialization context in the [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) @@ -2112,7 +2113,7 @@ class FastAPI(Starlette): This will be passed in as serialization context to the response model. - Note: This feature is a noop on pydantic < 2.7.2 + Note: This feature is a noop on pydantic < 2.8 Read more about serialization context in the [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) @@ -2506,7 +2507,7 @@ class FastAPI(Starlette): This will be passed in as serialization context to the response model. - Note: This feature is a noop on pydantic < 2.7.2 + Note: This feature is a noop on pydantic < 2.8 Read more about serialization context in the [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) @@ -2900,7 +2901,7 @@ class FastAPI(Starlette): This will be passed in as serialization context to the response model. - Note: This feature is a noop on pydantic < 2.7.2 + Note: This feature is a noop on pydantic < 2.8 Read more about serialization context in the [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) @@ -3289,7 +3290,7 @@ class FastAPI(Starlette): This will be passed in as serialization context to the response model. - Note: This feature is a noop on pydantic < 2.7.2 + Note: This feature is a noop on pydantic < 2.8 Read more about serialization context in the [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) @@ -3678,7 +3679,7 @@ class FastAPI(Starlette): This will be passed in as serialization context to the response model. - Note: This feature is a noop on pydantic < 2.7.2 + Note: This feature is a noop on pydantic < 2.8 Read more about serialization context in the [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) @@ -4067,7 +4068,7 @@ class FastAPI(Starlette): This will be passed in as serialization context to the response model. - Note: This feature is a noop on pydantic < 2.7.2 + Note: This feature is a noop on pydantic < 2.8 Read more about serialization context in the [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) @@ -4461,7 +4462,7 @@ class FastAPI(Starlette): This will be passed in as serialization context to the response model. - Note: This feature is a noop on pydantic < 2.7.2 + Note: This feature is a noop on pydantic < 2.8 Read more about serialization context in the [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) diff --git a/fastapi/routing.py b/fastapi/routing.py index 07c9c971c..116eaa88a 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -19,6 +19,24 @@ from typing import ( Union, ) +from pydantic import BaseModel +from starlette import routing +from starlette.concurrency import run_in_threadpool +from starlette.exceptions import HTTPException +from starlette.requests import Request +from starlette.responses import JSONResponse, Response +from starlette.routing import BaseRoute, Match +from starlette.routing import Mount as Mount # noqa +from starlette.routing import ( + compile_path, + get_name, + request_response, + websocket_session, +) +from starlette.types import ASGIApp, Lifespan, Scope +from starlette.websockets import WebSocket +from typing_extensions import Annotated, Doc, deprecated + from fastapi import params from fastapi._compat import ( ModelField, @@ -52,24 +70,6 @@ from fastapi.utils import ( get_value_or_default, is_body_allowed_for_status_code, ) -from pydantic import BaseModel -from starlette import routing -from starlette.concurrency import run_in_threadpool -from starlette.exceptions import HTTPException -from starlette.requests import Request -from starlette.responses import JSONResponse, Response -from starlette.routing import ( - BaseRoute, - Match, - compile_path, - get_name, - request_response, - websocket_session, -) -from starlette.routing import Mount as Mount # noqa -from starlette.types import ASGIApp, Lifespan, Scope -from starlette.websockets import WebSocket -from typing_extensions import Annotated, Doc, deprecated def _prepare_response_content( @@ -1583,7 +1583,7 @@ class APIRouter(routing.Router): This will be passed in as serialization context to the response model. - Note: This feature is a noop on pydantic < 2.7.2 + Note: This feature is a noop on pydantic < 2.8 Read more about serialization context in the [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) @@ -1976,7 +1976,7 @@ class APIRouter(routing.Router): This will be passed in as serialization context to the response model. - Note: This feature is a noop on pydantic < 2.7.2 + Note: This feature is a noop on pydantic < 2.8 Read more about serialization context in the [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) @@ -2374,7 +2374,7 @@ class APIRouter(routing.Router): This will be passed in as serialization context to the response model. - Note: This feature is a noop on pydantic < 2.7.2 + Note: This feature is a noop on pydantic < 2.8 Read more about serialization context in the [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) @@ -2772,7 +2772,7 @@ class APIRouter(routing.Router): This will be passed in as serialization context to the response model. - Note: This feature is a noop on pydantic < 2.7.2 + Note: This feature is a noop on pydantic < 2.8 Read more about serialization context in the [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) @@ -3165,7 +3165,7 @@ class APIRouter(routing.Router): This will be passed in as serialization context to the response model. - Note: This feature is a noop on pydantic < 2.7.2 + Note: This feature is a noop on pydantic < 2.8 Read more about serialization context in the [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) @@ -3558,7 +3558,7 @@ class APIRouter(routing.Router): This will be passed in as serialization context to the response model. - Note: This feature is a noop on pydantic < 2.7.2 + Note: This feature is a noop on pydantic < 2.8 Read more about serialization context in the [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) @@ -3956,7 +3956,7 @@ class APIRouter(routing.Router): This will be passed in as serialization context to the response model. - Note: This feature is a noop on pydantic < 2.7.2 + Note: This feature is a noop on pydantic < 2.8 Read more about serialization context in the [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) @@ -4354,7 +4354,7 @@ class APIRouter(routing.Router): This will be passed in as serialization context to the response model. - Note: This feature is a noop on pydantic < 2.7.2 + Note: This feature is a noop on pydantic < 2.8 Read more about serialization context in the [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) diff --git a/tests/test_serialize_response_model.py b/tests/test_serialize_response_model.py index 470946357..ab9d4c6df 100644 --- a/tests/test_serialize_response_model.py +++ b/tests/test_serialize_response_model.py @@ -1,11 +1,12 @@ from typing import Dict, List, Optional import pytest -from fastapi import FastAPI -from fastapi._compat import PYDANTIC_V2, PYDANTIC_VERSION from pydantic import BaseModel, Field from starlette.testclient import TestClient +from fastapi import FastAPI +from fastapi._compat import PYDANTIC_V2, PYDANTIC_VERSION + app = FastAPI() @@ -188,7 +189,7 @@ if PYDANTIC_V2: client_v2 = TestClient(app_v2) - @pytest.mark.skipif(PYDANTIC_VERSION < "2.7.3", reason="requires Pydantic v2.7.3+") + @pytest.mark.skipif(PYDANTIC_VERSION < "2.8", reason="requires Pydantic v2.8+") def test_validdict_with_context__pydantic_supported(): response = client_v2.get("/items/validdict-with-context") response.raise_for_status() @@ -202,7 +203,7 @@ if PYDANTIC_V2: assert response.json() == expected_response @pytest.mark.skipif( - PYDANTIC_VERSION >= "2.7.3", + PYDANTIC_VERSION >= "2.8", reason="Pydantic supports the feature from this point on", ) def test_validdict_with_context__pre_pydantic_support(): From 606a7fea2083169a48331aad9befde40786546ec Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 9 Aug 2024 13:31:12 +0000 Subject: [PATCH 13/15] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/_compat.py | 16 +++++------- fastapi/applications.py | 21 +++++++-------- fastapi/routing.py | 36 +++++++++++++------------- tests/test_serialize_response_model.py | 9 ++++--- 4 files changed, 40 insertions(+), 42 deletions(-) diff --git a/fastapi/_compat.py b/fastapi/_compat.py index 3e0261ae8..6a05684cb 100644 --- a/fastapi/_compat.py +++ b/fastapi/_compat.py @@ -18,14 +18,13 @@ from typing import ( Union, ) +from fastapi.exceptions import RequestErrorModel +from fastapi.types import IncEx, ModelNameMap, UnionType from pydantic import BaseModel, create_model from pydantic.version import VERSION as P_VERSION from starlette.datastructures import UploadFile from typing_extensions import Annotated, Literal, get_args, get_origin -from fastapi.exceptions import RequestErrorModel -from fastapi.types import IncEx, ModelNameMap, UnionType - # Reassign variable to make it reexported for mypy PYDANTIC_VERSION = P_VERSION PYDANTIC_V2 = PYDANTIC_VERSION.startswith("2.") @@ -288,6 +287,7 @@ if PYDANTIC_V2: return BodyModel else: + from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX from pydantic import AnyUrl as Url # noqa: F401 from pydantic import ( # type: ignore[assignment]; noqa: F401 BaseConfig as BaseConfig, @@ -322,15 +322,15 @@ else: from pydantic.fields import ( # type: ignore[no-redef, attr-defined]; noqa: F401 UndefinedType as UndefinedType, ) - from pydantic.schema import field_schema - from pydantic.schema import ( # type: ignore[no-redef] # noqa: F401 - get_annotation_from_field_info as get_annotation_from_field_info, - ) from pydantic.schema import ( + field_schema, get_flat_models_from_fields, get_model_name_map, model_process_schema, ) + from pydantic.schema import ( # type: ignore[no-redef] # noqa: F401 + get_annotation_from_field_info as get_annotation_from_field_info, + ) from pydantic.typing import ( # type: ignore[no-redef]; noqa: F401 evaluate_forwardref as evaluate_forwardref, ) @@ -338,8 +338,6 @@ else: lenient_issubclass as lenient_issubclass, ) - from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX - GetJsonSchemaHandler = Any # type: ignore[assignment,misc] JsonSchemaValue = Dict[str, Any] # type: ignore[misc] CoreSchema = Any # type: ignore[assignment,misc] diff --git a/fastapi/applications.py b/fastapi/applications.py index 63c797f55..b880c6d4f 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -13,17 +13,6 @@ from typing import ( Union, ) -from starlette.applications import Starlette -from starlette.datastructures import State -from starlette.exceptions import HTTPException -from starlette.middleware import Middleware -from starlette.middleware.base import BaseHTTPMiddleware -from starlette.requests import Request -from starlette.responses import HTMLResponse, JSONResponse, Response -from starlette.routing import BaseRoute -from starlette.types import ASGIApp, Lifespan, Receive, Scope, Send -from typing_extensions import Annotated, Doc, deprecated - from fastapi import routing from fastapi.datastructures import Default, DefaultPlaceholder from fastapi.exception_handlers import ( @@ -42,6 +31,16 @@ from fastapi.openapi.utils import get_openapi from fastapi.params import Depends from fastapi.types import DecoratedCallable, IncEx from fastapi.utils import generate_unique_id +from starlette.applications import Starlette +from starlette.datastructures import State +from starlette.exceptions import HTTPException +from starlette.middleware import Middleware +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request +from starlette.responses import HTMLResponse, JSONResponse, Response +from starlette.routing import BaseRoute +from starlette.types import ASGIApp, Lifespan, Receive, Scope, Send +from typing_extensions import Annotated, Doc, deprecated AppType = TypeVar("AppType", bound="FastAPI") diff --git a/fastapi/routing.py b/fastapi/routing.py index 116eaa88a..7f7f45611 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -19,24 +19,6 @@ from typing import ( Union, ) -from pydantic import BaseModel -from starlette import routing -from starlette.concurrency import run_in_threadpool -from starlette.exceptions import HTTPException -from starlette.requests import Request -from starlette.responses import JSONResponse, Response -from starlette.routing import BaseRoute, Match -from starlette.routing import Mount as Mount # noqa -from starlette.routing import ( - compile_path, - get_name, - request_response, - websocket_session, -) -from starlette.types import ASGIApp, Lifespan, Scope -from starlette.websockets import WebSocket -from typing_extensions import Annotated, Doc, deprecated - from fastapi import params from fastapi._compat import ( ModelField, @@ -70,6 +52,24 @@ from fastapi.utils import ( get_value_or_default, is_body_allowed_for_status_code, ) +from pydantic import BaseModel +from starlette import routing +from starlette.concurrency import run_in_threadpool +from starlette.exceptions import HTTPException +from starlette.requests import Request +from starlette.responses import JSONResponse, Response +from starlette.routing import ( + BaseRoute, + Match, + compile_path, + get_name, + request_response, + websocket_session, +) +from starlette.routing import Mount as Mount # noqa +from starlette.types import ASGIApp, Lifespan, Scope +from starlette.websockets import WebSocket +from typing_extensions import Annotated, Doc, deprecated def _prepare_response_content( diff --git a/tests/test_serialize_response_model.py b/tests/test_serialize_response_model.py index ab9d4c6df..8ac3a5f1f 100644 --- a/tests/test_serialize_response_model.py +++ b/tests/test_serialize_response_model.py @@ -1,11 +1,10 @@ from typing import Dict, List, Optional import pytest -from pydantic import BaseModel, Field -from starlette.testclient import TestClient - from fastapi import FastAPI from fastapi._compat import PYDANTIC_V2, PYDANTIC_VERSION +from pydantic import BaseModel, Field +from starlette.testclient import TestClient app = FastAPI() @@ -184,7 +183,9 @@ if PYDANTIC_V2: return { "k1": MultiUseItem(aliased_name="foo"), "k2": MultiUseItem(aliased_name="bar", secret="sEcReT"), - "k3": MultiUseItem(aliased_name="baz", secret="sEcReT", owner_ids=[1, 2, 3]), + "k3": MultiUseItem( + aliased_name="baz", secret="sEcReT", owner_ids=[1, 2, 3] + ), } client_v2 = TestClient(app_v2) From f5b7890621fe9d649446c016142c5ed34f43644a Mon Sep 17 00:00:00 2001 From: Alex Couper Date: Fri, 9 Aug 2024 13:46:05 +0000 Subject: [PATCH 14/15] Lint --- fastapi/_compat.py | 76 ++++++++++++++++++++++++++-------------------- 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/fastapi/_compat.py b/fastapi/_compat.py index 6a05684cb..2e0194d12 100644 --- a/fastapi/_compat.py +++ b/fastapi/_compat.py @@ -50,8 +50,8 @@ if PYDANTIC_V2: from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError from pydantic import TypeAdapter from pydantic import ValidationError as ValidationError - from pydantic._internal._schema_generation_shared import ( - GetJsonSchemaHandler as GetJsonSchemaHandler, # type: ignore[attr-defined] + from pydantic._internal._schema_generation_shared import ( # type: ignore[attr-defined] + GetJsonSchemaHandler as GetJsonSchemaHandler, ) from pydantic._internal._typing_extra import eval_type_lenient from pydantic._internal._utils import lenient_issubclass as lenient_issubclass @@ -150,19 +150,29 @@ if PYDANTIC_V2: # self._type_adapter.validate_python(value) # # context argument was introduced in pydantic 2.8 - kwargs = {"context": context} if PYDANTIC_VERSION >= "2.8" else {} - - return self._type_adapter.dump_python( - value, - mode=mode, - include=include, - exclude=exclude, - by_alias=by_alias, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - **kwargs, - ) + if PYDANTIC_VERSION >= "2.8": + return self._type_adapter.dump_python( + value, + mode=mode, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + context=context, + ) + else: + return self._type_adapter.dump_python( + value, + mode=mode, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) def __hash__(self) -> int: # Each ModelField is unique for our purposes, to allow making a dict from @@ -289,15 +299,15 @@ if PYDANTIC_V2: else: from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX from pydantic import AnyUrl as Url # noqa: F401 - from pydantic import ( # type: ignore[assignment]; noqa: F401 - BaseConfig as BaseConfig, + from pydantic import ( # type: ignore[assignment] + BaseConfig as BaseConfig, # noqa: F401 ) from pydantic import ValidationError as ValidationError # noqa: F401 - from pydantic.class_validators import ( # type: ignore[no-redef]; noqa: F401 - Validator as Validator, + from pydantic.class_validators import ( # type: ignore[no-redef] + Validator as Validator, # noqa: F401 ) - from pydantic.error_wrappers import ( # type: ignore[no-redef]; noqa: F401 - ErrorWrapper as ErrorWrapper, + from pydantic.error_wrappers import ( # type: ignore[no-redef] + ErrorWrapper as ErrorWrapper, # noqa: F401 ) from pydantic.errors import MissingError from pydantic.fields import ( # type: ignore[attr-defined] @@ -310,17 +320,17 @@ else: SHAPE_TUPLE_ELLIPSIS, ) from pydantic.fields import FieldInfo as FieldInfo - from pydantic.fields import ( # type: ignore[no-redef,attr-defined]; noqa: F401 - ModelField as ModelField, + from pydantic.fields import ( # type: ignore[no-redef,attr-defined] + ModelField as ModelField, # noqa: F401 ) - from pydantic.fields import ( # type: ignore[no-redef,attr-defined]; noqa: F401 - Required as Required, + from pydantic.fields import ( # type: ignore[no-redef,attr-defined] + Required as Required, # noqa: F401 ) - from pydantic.fields import ( - Undefined as Undefined, # type: ignore[no-redef,attr-defined] + from pydantic.fields import ( # type: ignore[no-redef,attr-defined] + Undefined as Undefined, ) - from pydantic.fields import ( # type: ignore[no-redef, attr-defined]; noqa: F401 - UndefinedType as UndefinedType, + from pydantic.fields import ( # type: ignore[no-redef, attr-defined] + UndefinedType as UndefinedType, # noqa: F401 ) from pydantic.schema import ( field_schema, @@ -331,11 +341,11 @@ else: from pydantic.schema import ( # type: ignore[no-redef] # noqa: F401 get_annotation_from_field_info as get_annotation_from_field_info, ) - from pydantic.typing import ( # type: ignore[no-redef]; noqa: F401 - evaluate_forwardref as evaluate_forwardref, + from pydantic.typing import ( # type: ignore[no-redef] + evaluate_forwardref as evaluate_forwardref, # noqa: F401 ) - from pydantic.utils import ( # type: ignore[no-redef]; noqa: F401 - lenient_issubclass as lenient_issubclass, + from pydantic.utils import ( # type: ignore[no-redef] + lenient_issubclass as lenient_issubclass, # noqa: F401 ) GetJsonSchemaHandler = Any # type: ignore[assignment,misc] From 663439dfa52c94f55bcc9d25796c26123d484577 Mon Sep 17 00:00:00 2001 From: Alex Couper Date: Fri, 9 Aug 2024 13:51:51 +0000 Subject: [PATCH 15/15] Include tests for pydantic 2.8 and 2.7 --- .github/workflows/test.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a33b6a68a..af1ba6a8d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -51,7 +51,7 @@ jobs: - "3.10" - "3.9" - "3.8" - pydantic-version: ["pydantic-v1", "pydantic-v2"] + pydantic-version: ["pydantic-v1", "pydantic-v2.7", "pydantic-v2.8+"] fail-fast: false steps: - name: Dump GitHub context @@ -78,8 +78,11 @@ jobs: if: matrix.pydantic-version == 'pydantic-v1' run: pip install "pydantic>=1.10.0,<2.0.0" - name: Install Pydantic v2 - if: matrix.pydantic-version == 'pydantic-v2' - run: pip install "pydantic>=2.0.2,<3.0.0" + if: matrix.pydantic-version == 'pydantic-v2.7' + run: pip install "pydantic>=2.0.2,<2.8" + - name: Install Pydantic v2.8+ + if: matrix.pydantic-version == 'pydantic-v2.8+' + run: pip install "pydantic>=2.8,<3.0" - run: mkdir coverage - name: Test run: bash scripts/test.sh