From 0286abe0dd452f278befbf3fa4f08d7277af5e93 Mon Sep 17 00:00:00 2001 From: scorpp Date: Fri, 9 Aug 2024 19:14:46 +0300 Subject: [PATCH 1/3] Add serialisation context (fixes #1233) Pydantic starting with 2.7 added support for context param in `model_dump` and `model_dump_json` allowing to pass arbitrary additional data into custom serializers. --- ninja/operation.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ninja/operation.py b/ninja/operation.py index b47eb4f2..3a79eab6 100644 --- a/ninja/operation.py +++ b/ninja/operation.py @@ -21,7 +21,7 @@ from ninja.constants import NOT_SET, NOT_SET_TYPE from ninja.errors import AuthenticationError, ConfigError, Throttled, ValidationError from ninja.params.models import TModels -from ninja.schema import Schema +from ninja.schema import Schema, pydantic_version from ninja.signature import ViewSignature, is_async from ninja.throttling import BaseThrottle from ninja.types import DictStrAny @@ -261,11 +261,17 @@ def _result_to_response( resp_object, context={"request": request, "response_status": status} ) + model_dump_kwargs = {} + if pydantic_version >= [2,7]: + # pydantic added support for serialization context at 2.7 + model_dump_kwargs.update(context={"request": request, "response_status": status}) + result = validated_object.model_dump( by_alias=self.by_alias, exclude_unset=self.exclude_unset, exclude_defaults=self.exclude_defaults, exclude_none=self.exclude_none, + **model_dump_kwargs, )["response"] return self.api.create_response( request, result, temporal_response=temporal_response From b2d1bd409fa24c6e0dd2df041f854532d0cb89de Mon Sep 17 00:00:00 2001 From: scorpp Date: Fri, 9 Aug 2024 20:20:47 +0300 Subject: [PATCH 2/3] reformat --- ninja/operation.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ninja/operation.py b/ninja/operation.py index 3a79eab6..4d6bb649 100644 --- a/ninja/operation.py +++ b/ninja/operation.py @@ -262,9 +262,11 @@ def _result_to_response( ) model_dump_kwargs = {} - if pydantic_version >= [2,7]: + if pydantic_version >= [2, 7]: # pydantic added support for serialization context at 2.7 - model_dump_kwargs.update(context={"request": request, "response_status": status}) + model_dump_kwargs.update( + context={"request": request, "response_status": status} + ) result = validated_object.model_dump( by_alias=self.by_alias, From 387f211848135b42247967db9786f58e63f81da7 Mon Sep 17 00:00:00 2001 From: scorpp Date: Sun, 11 Aug 2024 20:48:53 +0300 Subject: [PATCH 3/3] Add tests --- tests/test_serialization_context.py | 75 +++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 tests/test_serialization_context.py diff --git a/tests/test_serialization_context.py b/tests/test_serialization_context.py new file mode 100644 index 00000000..3b24f4cc --- /dev/null +++ b/tests/test_serialization_context.py @@ -0,0 +1,75 @@ +from unittest import mock + +import pytest +from pydantic import model_serializer + +from ninja import Router, Schema +from ninja.schema import pydantic_version +from ninja.testing import TestClient + + +def api_endpoint_test(request): + return { + "test1": "foo", + "test2": "bar", + } + + +@pytest.mark.skipif( + pydantic_version < [2, 7], + reason="Serialization context was introduced in Pydantic 2.7", +) +def test_request_is_passed_in_context_when_supported(): + class SchemaWithCustomSerializer(Schema): + test1: str + test2: str + + @model_serializer(mode="wrap") + def ser_model(self, handler, info): + assert "request" in info.context + assert info.context["request"].path == "/test" # check it is HttRequest + assert "response_status" in info.context + + return handler(self) + + router = Router() + router.add_api_operation( + "/test", ["GET"], api_endpoint_test, response=SchemaWithCustomSerializer + ) + + TestClient(router).get("/test") + + +@pytest.mark.parametrize( + ["pydantic_version"], + [ + [[2, 0]], + [[2, 4]], + [[2, 6]], + ], +) +def test_no_serialisation_context_used_when_no_supported(pydantic_version): + class SchemaWithCustomSerializer(Schema): + test1: str + test2: str + + @model_serializer(mode="wrap") + def ser_model(self, handler, info): + if hasattr(info, "context"): + # an actually newer Pydantic, but pydantic_version is still mocked, so no context is expected + assert info.context is None + + return handler(self) + + with mock.patch("ninja.operation.pydantic_version", pydantic_version): + router = Router() + router.add_api_operation( + "/test", ["GET"], api_endpoint_test, response=SchemaWithCustomSerializer + ) + + resp_json = TestClient(router).get("/test").json() + + assert resp_json == { + "test1": "foo", + "test2": "bar", + }