Skip to content

Commit

Permalink
Feat: Handle serialization of lists of Pydantic model (#25)
Browse files Browse the repository at this point in the history
- Handle prop types which are list by recursively calling
_deep_transform_callable when a prop is of type list

This allows, for example, a user to pass a list of Pydantic's BaseModels
to inertia.render and they are encoded as expected
  • Loading branch information
hxjo authored Jul 19, 2024
1 parent 6c75289 commit ae17f38
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 4 deletions.
Binary file modified .coverage
Binary file not shown.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.2] - 2024-07-19

- Feat: handle serialization / encoding of prop which are of type list
- this allows passing to inertia, as prop value, a list of models and they will be encoded as expected

## [1.0.1] - 2024-07-18

- Fix: SSR failed when the inertia server responded with an empty array
Expand Down
15 changes: 12 additions & 3 deletions inertia/inertia.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
Callable,
Dict,
List,
Optional,
TypeVar,
TypedDict,
Union,
Expand Down Expand Up @@ -204,7 +203,15 @@ def _set_inertia_files(self) -> 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
Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand Down
49 changes: 49 additions & 0 deletions inertia/tests/test_pydantic_basemodel_are_encoded.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ class Person(BaseModel):
}
}

EXPECTED_PROPS_MULTIPLE = {"persons": [EXPECTED_PROPS["person"] for _ in range(2)]}

COMPONENT = "IndexPage"


Expand All @@ -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"})
Expand All @@ -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,
)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "fastapi-inertia"
version = "1.0.1"
version = "1.0.2"
description = "An implementation of the Inertia protocol for FastAPI."
authors = ["Hugo Mortreux <[email protected]>"]
license = "MIT"
Expand Down

0 comments on commit ae17f38

Please sign in to comment.