From cfb9701c87fe8aef26d1872f113f222bb82fe42d Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 20 Dec 2024 12:47:05 +0100 Subject: [PATCH] fix serialization of primitives in Response (#120) * fix serialization of primitives in Response * remove shortcut, better to pass it properly through json_encode * beautify release notes --- docs/en/docs/release-notes.md | 6 ++++++ lilya/responses.py | 15 ++++++++++----- tests/test_make_response.py | 31 +++++++++++++++++++++++++++++-- 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index af803fb..fcc440e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -15,6 +15,12 @@ hide: - Refactor `SessionMiddleware`. +## 0.11.9 + +### Fixed + +- Fix serialization of primitives in the Response. Strip `"` by default. + ## 0.11.8 ### Fixed diff --git a/lilya/responses.py b/lilya/responses.py index 3e2dffd..8c4d46b 100644 --- a/lilya/responses.py +++ b/lilya/responses.py @@ -99,12 +99,11 @@ def transform(cls, content: Any) -> Any: transform_kwargs = {} return json_encode(content, **transform_kwargs) - def make_response(self, content: Any) -> bytes | str: + def make_response(self, content: Any) -> bytes | memoryview: """ Makes the Response object type. """ - # only handle empty string not empty bytes. Bytes are handled later - if content is None or content is NoReturn or content == "": + if content is None or content is NoReturn: return b"" if isinstance(content, (bytes, memoryview)): return content @@ -113,6 +112,11 @@ def make_response(self, content: Any) -> bytes | str: transform_kwargs = transform_kwargs.copy() if self.encoders: transform_kwargs["with_encoders"] = (*self.encoders, *ENCODER_TYPES.get()) + # strip " from stringified primitives + transform_kwargs.setdefault( + "post_transform_fn", + lambda x: x.strip('"') if isinstance(x, str) else x.strip(b'"'), + ) content = json_encode(content, **transform_kwargs) if isinstance(content, (bytes, memoryview)): @@ -310,7 +314,7 @@ def __init__( encoders=encoders, ) - def make_response(self, content: Any) -> bytes: + def make_response(self, content: Any) -> bytes | memoryview: if content is NoReturn: return b"" new_params = RESPONSE_TRANSFORM_KWARGS.get() @@ -546,7 +550,8 @@ def make_response( headers: Mapping[str, str] | None = None, background: Task | None = None, encoders: Sequence[Encoder | type[Encoder]] | None = None, - json_encode_extra_kwargs: dict | None = None, + # passing mutables as default argument is not a good style but here is no other way + json_encode_extra_kwargs: dict | None = {}, # noqa: B006 ) -> Response: """ Build JSON responses from a given content and diff --git a/tests/test_make_response.py b/tests/test_make_response.py index a813d1a..b80a440 100644 --- a/tests/test_make_response.py +++ b/tests/test_make_response.py @@ -1,3 +1,4 @@ +import datetime from collections import deque from dataclasses import dataclass @@ -8,7 +9,7 @@ from pydantic import BaseModel from lilya.encoders import Encoder, apply_structure -from lilya.responses import make_response +from lilya.responses import Response, make_response from lilya.routing import Path from lilya.testclient import create_client @@ -130,7 +131,7 @@ def test_attrs_custom_make_response_list(): @pytest.mark.parametrize( "json_encode_kwargs", [{}, {"json_encode_fn": orjson.dumps, "post_transform_fn": orjson.loads}] ) -@pytest.mark.parametrize("value", ["1", 2, 2.2, None], ids=["str", "int", "float", "none"]) +@pytest.mark.parametrize("value", ["hello", 2, 2.2, None], ids=["str", "int", "float", "none"]) def test_primitive_responses(value, json_encode_kwargs): def home(): return make_response(value, status_code=201, json_encode_extra_kwargs=json_encode_kwargs) @@ -142,6 +143,32 @@ def home(): assert response.json() == value +@pytest.mark.parametrize( + "json_encode_kwargs", [{}, {"json_encode_fn": orjson.dumps, "post_transform_fn": orjson.loads}] +) +@pytest.mark.parametrize( + "value,result", + [ + ("hello", "hello"), + (b"hello", "hello"), + (2, "2"), + (2.2, "2.2"), + (None, ""), + (datetime.datetime(2014, 11, 10), "2014-11-10T00:00:00"), + ], + ids=["str", "bytes", "int", "float", "none", "datetime"], +) +def test_classic_response(value, result, json_encode_kwargs): + def home(): + return make_response(value, status_code=201, response_class=Response) + + with create_client(routes=[Path("/", home)]) as client: + response = client.get("/") + + assert response.status_code == 201 + assert response.text == result + + def test_dict_response(): def home(): return make_response({"message": "works"}, status_code=201)