From 3e5ceb0eebaba134454b136f19c5762ee3073d59 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Mon, 29 Jul 2024 09:00:24 +0200 Subject: [PATCH] Serializer: use alias, make it configurable for different use cases --- pkgs/clan-cli/clan_cli/api/serde.py | 53 ++++++++++++----------- pkgs/clan-cli/tests/test_deserializers.py | 17 +++++--- 2 files changed, 40 insertions(+), 30 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/api/serde.py b/pkgs/clan-cli/clan_cli/api/serde.py index 25fa33498..0f9c5d1fc 100644 --- a/pkgs/clan-cli/clan_cli/api/serde.py +++ b/pkgs/clan-cli/clan_cli/api/serde.py @@ -49,32 +49,35 @@ def sanitize_string(s: str) -> str: return json.dumps(s)[1:-1] -def dataclass_to_dict(obj: Any) -> Any: - """ - Utility function to convert dataclasses to dictionaries - It converts all nested dataclasses, lists, tuples, and dictionaries to dictionaries +def dataclass_to_dict(obj: Any, *, use_alias: bool = True) -> Any: + def _to_dict(obj: Any) -> Any: + """ + Utility function to convert dataclasses to dictionaries + It converts all nested dataclasses, lists, tuples, and dictionaries to dictionaries - It does NOT convert member functions. - """ - if is_dataclass(obj): - return { - # Use either the original name or name - sanitize_string( - field.metadata.get("original_name", field.name) - ): dataclass_to_dict(getattr(obj, field.name)) - for field in fields(obj) - if not field.name.startswith("_") # type: ignore - } - elif isinstance(obj, list | tuple): - return [dataclass_to_dict(item) for item in obj] - elif isinstance(obj, dict): - return {sanitize_string(k): dataclass_to_dict(v) for k, v in obj.items()} - elif isinstance(obj, Path): - return sanitize_string(str(obj)) - elif isinstance(obj, str): - return sanitize_string(obj) - else: - return obj + It does NOT convert member functions. + """ + if is_dataclass(obj): + return { + # Use either the original name or name + sanitize_string( + field.metadata.get("alias", field.name) if use_alias else field.name + ): _to_dict(getattr(obj, field.name)) + for field in fields(obj) + if not field.name.startswith("_") # type: ignore + } + elif isinstance(obj, list | tuple): + return [_to_dict(item) for item in obj] + elif isinstance(obj, dict): + return {sanitize_string(k): _to_dict(v) for k, v in obj.items()} + elif isinstance(obj, Path): + return sanitize_string(str(obj)) + elif isinstance(obj, str): + return sanitize_string(obj) + else: + return obj + + return _to_dict(obj) T = TypeVar("T", bound=dataclass) # type: ignore diff --git a/pkgs/clan-cli/tests/test_deserializers.py b/pkgs/clan-cli/tests/test_deserializers.py index 778d3728f..c93fa041f 100644 --- a/pkgs/clan-cli/tests/test_deserializers.py +++ b/pkgs/clan-cli/tests/test_deserializers.py @@ -4,10 +4,7 @@ from pathlib import Path import pytest # Functions to test -from clan_cli.api import ( - dataclass_to_dict, - from_dict, -) +from clan_cli.api import dataclass_to_dict, from_dict from clan_cli.errors import ClanError from clan_cli.inventory import ( Inventory, @@ -87,6 +84,7 @@ def test_simple_field_missing() -> None: def test_deserialize_extensive_inventory() -> None: + # TODO: Make this an abstract test, so it doesn't break the test if the inventory changes data = { "meta": {"name": "superclan", "description": "nice clan"}, "services": { @@ -130,7 +128,16 @@ def test_alias_field() -> None: data = {"--user-name--": "John"} expected = Person(name="John") - assert from_dict(Person, data) == expected + person = from_dict(Person, data) + + # Deserialize + assert person == expected + + # Serialize with alias + assert dataclass_to_dict(person) == data + + # Serialize without alias + assert dataclass_to_dict(person, use_alias=False) == {"name": "John"} def test_path_field() -> None: