From a020a2dacd20c7548de7792d414928cf010620d4 Mon Sep 17 00:00:00 2001 From: Hugo Mortreux <70602545+hxjo@users.noreply.github.com> Date: Fri, 19 Jul 2024 17:00:21 +0200 Subject: [PATCH] Feat: Handle serialization of lists of Pydantic model --- .coverage | Bin 53248 -> 53248 bytes inertia/inertia.py | 15 ++++-- .../test_pydantic_basemodel_are_encoded.py | 49 ++++++++++++++++++ 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/.coverage b/.coverage index 5594bdc6634ab3fc899d0fe528d7d6498d60fec5..f4fb99362d5f7630021df6d74ffcec26f55ecc1a 100644 GIT binary patch delta 82 zcmV-Y0ImOkpaX!Q1F!~w4tf9&_z$=bsSlP9iw}CU5fEk%lYEap20j7=0SSJSz>gXr oXMX|yT|sY!pS1gb2p^%qwt*Ag9Kbo-kNqEPa-cu~v-pn)K*`J?HUIzs delta 80 zcmV-W0I&aX!Q1F!~w4txL)_z$`ds}GnDjSqaY5fEq(lX{On20Q`;0SS7OzK None: @classmethod def _deep_transform_callables( - cls, prop: Union[Callable[..., Any], Dict[str, Any], BaseModel, Any] + cls, + prop: Union[ + Callable[..., Any], + Dict[str, Any], + BaseModel, + List[BaseModel], + List[Any], + Any, + ], ) -> Any: """ Deeply transform callables in a dictionary, evaluating them if they are callables @@ -219,6 +226,8 @@ def _deep_transform_callables( return prop() if isinstance(prop, BaseModel): return json.loads(prop.model_dump_json()) + if isinstance(prop, list): + return [cls._deep_transform_callables(p) for p in prop] return prop prop_ = prop.copy() @@ -350,7 +359,7 @@ def back(self) -> RedirectResponse: ) async def render( - self, component: str, props: Optional[Dict[str, Any]] = None + self, component: str, props: Union[Dict[str, Any], BaseModel, None] = None ) -> InertiaResponse: """ Render the page diff --git a/inertia/tests/test_pydantic_basemodel_are_encoded.py b/inertia/tests/test_pydantic_basemodel_are_encoded.py index 53f0659..4652b12 100644 --- a/inertia/tests/test_pydantic_basemodel_are_encoded.py +++ b/inertia/tests/test_pydantic_basemodel_are_encoded.py @@ -32,6 +32,8 @@ class Person(BaseModel): } } +EXPECTED_PROPS_MULTIPLE = {"persons": [EXPECTED_PROPS["person"] for _ in range(2)]} + COMPONENT = "IndexPage" @@ -52,6 +54,26 @@ async def index(inertia: InertiaDep) -> InertiaResponse: ) +@app.get("/multiple", response_model=None) +async def index_multiple(inertia: InertiaDep) -> InertiaResponse: + name = PROPS["person"]["name"] + age = PROPS["person"]["age"] + created_at = PROPS["person"]["created_at"] + return await inertia.render( + COMPONENT, + { + "persons": [ + Person( + name=cast(str, name), + age=cast(int, age), + created_at=cast(datetime, created_at), + ) + for _ in range(2) + ] + }, + ) + + def test_pydantic_basemodel_are_encoded_on_json_response() -> None: with TestClient(app) as client: response = client.get("/", headers={"X-Inertia": "true"}) @@ -77,3 +99,30 @@ def test_pydantic_basemodel_are_encoded_on_html_response() -> None: expected_props=EXPECTED_PROPS, expected_url=expected_url, ) + + +def test_pydantic_model_list_are_encoded_on_json_response() -> None: + with TestClient(app) as client: + response = client.get("/multiple", headers={"X-Inertia": "true"}) + assert response.status_code == 200 + assert response.headers.get("content-type").split(";")[0] == "application/json" + assert response.json() == { + "component": COMPONENT, + "props": EXPECTED_PROPS_MULTIPLE, + "url": f"{client.base_url}/multiple", + "version": "1.0", + } + + +def test_pydantic_model_list_are_encoded_on_html_response() -> None: + with TestClient(app) as client: + response = client.get("/multiple") + assert response.status_code == 200 + assert response.headers.get("content-type").split(";")[0] == "text/html" + expected_url = str(client.base_url) + "/multiple" + assert_response_content( + response, + expected_component=COMPONENT, + expected_props=EXPECTED_PROPS_MULTIPLE, + expected_url=expected_url, + )