From 1a0e14b8ee0866621a388a09e41c7f173e874e25 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 16 Sep 2024 17:39:24 +0200 Subject: [PATCH] fix: serializer omit `None` values as expected (#136) Signed-off-by: Jan Kowalleck --- serializable/__init__.py | 10 ++++-- .../the-phoenix-project-bookedition-none.json | 19 +++++++++++ .../the-phoenix-project-bookedition-none.xml | 14 ++++++++ tests/model.py | 33 +++++++++++++++++++ tests/test_json.py | 17 +++++++++- tests/test_xml.py | 6 ++++ 6 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 tests/fixtures/the-phoenix-project-bookedition-none.json create mode 100644 tests/fixtures/the-phoenix-project-bookedition-none.xml diff --git a/serializable/__init__.py b/serializable/__init__.py index 050e173..10c41c5 100644 --- a/serializable/__init__.py +++ b/serializable/__init__.py @@ -436,6 +436,11 @@ def as_xml(self: Any, view_: Optional[Type[ViewType]] = None, elif prop_info.is_enum: v = v.value + if v is None: + v = prop_info.get_none_value_for_view(view_=view_) + if v is None: + continue + this_e_attributes[_namespace_element_name(new_key, xmlns)] = \ _xs_string_mod_apply(str(v), prop_info.xml_string_config) @@ -453,9 +458,6 @@ def as_xml(self: Any, view_: Optional[Type[ViewType]] = None, # Skip as rendering for a view and this Property is not registered form this View continue - if v is None: - v = prop_info.get_none_value_for_view(view_=view_) - new_key = BaseNameFormatter.decode_handle_python_builtins_and_keywords(name=k) if not prop_info: @@ -464,6 +466,8 @@ def as_xml(self: Any, view_: Optional[Type[ViewType]] = None, if not prop_info.is_xml_attribute: new_key = prop_info.custom_names.get(SerializationType.XML, new_key) + if v is None: + v = prop_info.get_none_value_for_view(view_=view_) if v is None: SubElement(this_e, _namespace_element_name(tag_name=new_key, xmlns=xmlns)) continue diff --git a/tests/fixtures/the-phoenix-project-bookedition-none.json b/tests/fixtures/the-phoenix-project-bookedition-none.json new file mode 100644 index 0000000..4581c82 --- /dev/null +++ b/tests/fixtures/the-phoenix-project-bookedition-none.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "George Spafford", + "Gene Kim", + "Kevin Behr" + ], + "edition": { + "name": "Preview Edition" + }, + "id": "f3758bf0-0ff7-4366-a5e5-c209d4352b2d", + "isbnNumber": "978-1942788294", + "publishDate": "2018-04-16", + "publisher": { + "name": "IT Revolution Press LLC" + }, + "rating": 9.8, + "title": "{J} The Phoenix Project", + "type": "fiction" +} diff --git a/tests/fixtures/the-phoenix-project-bookedition-none.xml b/tests/fixtures/the-phoenix-project-bookedition-none.xml new file mode 100644 index 0000000..7e436eb --- /dev/null +++ b/tests/fixtures/the-phoenix-project-bookedition-none.xml @@ -0,0 +1,14 @@ + + f3758bf0-0ff7-4366-a5e5-c209d4352b2d + {X} The Phoenix Project + Preview Edition + 2018-04-16 + Kevin Behr + George Spafford + Gene Kim + fiction + + IT Revolution Press LLC + + 9.8 + diff --git a/tests/model.py b/tests/model.py index 0d86ce7..8604d1e 100644 --- a/tests/model.py +++ b/tests/model.py @@ -97,6 +97,22 @@ def xml_deserialize(cls, o: str) -> str: return re.sub(r'^\{X} ', '', o) +class BookEditionHelper(BaseHelper): + + @classmethod + def serialize(cls, o: Any) -> Optional[int]: + return o \ + if isinstance(o, int) and o > 0 \ + else None + + @classmethod + def deserialize(cls, o: Any) -> int: + try: + return int(o) + except Exception: + return 1 + + @serializable.serializable_class class Chapter: @@ -170,6 +186,7 @@ def __init__(self, *, number: int, name: str) -> None: @property @serializable.xml_attribute() + @serializable.type_mapping(BookEditionHelper) def number(self) -> int: return self._number @@ -469,6 +486,22 @@ def stock_ids(self) -> Set[StockId]: # endregion ThePhoenixProject_unnormalized +# region ThePhoenixProject_attr_serialized_none + +# a case where an attribute is serialized to `None` and deserialized from it +ThePhoenixProject_attr_serialized_none = Book( + title='The Phoenix Project', + isbn='978-1942788294', + publish_date=date(year=2018, month=4, day=16), + authors=['Gene Kim', 'Kevin Behr', 'George Spafford'], + publisher=Publisher(name='IT Revolution Press LLC'), + edition=BookEdition(number=0, name='Preview Edition'), + id=UUID('f3758bf0-0ff7-4366-a5e5-c209d4352b2d'), + rating=Decimal('9.8') +) + +# endregion ThePhoenixProject_attr_serialized_none + if __name__ == '__main__': tpp_as_xml = ThePhoenixProject.as_xml() # type:ignore[attr-defined] tpp_as_json = ThePhoenixProject.as_json() # type:ignore[attr-defined] diff --git a/tests/test_json.py b/tests/test_json.py index a23549f..0561c63 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -26,11 +26,21 @@ SnakeCasePropertyNameFormatter, ) from tests.base import FIXTURES_DIRECTORY, BaseTestCase -from tests.model import Book, SchemaVersion2, SchemaVersion3, SchemaVersion4, ThePhoenixProject, ThePhoenixProject_v1 +from tests.model import ( + Book, + SchemaVersion2, + SchemaVersion3, + SchemaVersion4, + ThePhoenixProject, + ThePhoenixProject_attr_serialized_none, + ThePhoenixProject_v1, +) class TestJson(BaseTestCase): + # region test_serialize + def test_serialize_tfp_cc(self) -> None: CurrentFormatter.formatter = CamelCasePropertyNameFormatter with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-camel-case.json')) as expected_json: @@ -130,3 +140,8 @@ def test_deserialize_tfp_sc(self) -> None: self.assertEqual(ThePhoenixProject_v1.publisher, book.publisher) self.assertEqual(ThePhoenixProject_v1.chapters, book.chapters) self.assertEqual(ThePhoenixProject_v1.rating, book.rating) + + def test_serialize_attr_none(self) -> None: + CurrentFormatter.formatter = CamelCasePropertyNameFormatter + with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-bookedition-none.json')) as expected_json: + self.assertEqualJson(expected_json.read(), ThePhoenixProject_attr_serialized_none.as_json()) diff --git a/tests/test_xml.py b/tests/test_xml.py index b49d6d3..606d9ec 100644 --- a/tests/test_xml.py +++ b/tests/test_xml.py @@ -36,6 +36,7 @@ SchemaVersion3, SchemaVersion4, ThePhoenixProject, + ThePhoenixProject_attr_serialized_none, ThePhoenixProject_unnormalized, ThePhoenixProject_v1, ) @@ -129,6 +130,11 @@ def test_serialize_unnormalized(self) -> None: with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-camel-case-1-v4.xml')) as expected_xml: self.assertEqualXml(expected_xml.read(), ThePhoenixProject_unnormalized.as_xml(SchemaVersion4)) + def test_serialize_attr_none(self) -> None: + CurrentFormatter.formatter = CamelCasePropertyNameFormatter + with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-bookedition-none.xml')) as expected_xml: + self.assertEqualXml(expected_xml.read(), ThePhoenixProject_attr_serialized_none.as_xml(SchemaVersion4)) + # endregion test_serialize # region test_deserialize