From 6ba882551d14be109343e1894a9601885f29c847 Mon Sep 17 00:00:00 2001 From: Jacob Nilsson Date: Tue, 14 Feb 2023 21:07:57 +0100 Subject: [PATCH 1/7] Rebase branch to be up-to-date with dev. Fixed review comments. --- examples/snippets/model_template.py | 2 +- examples/snippets/union_models.py | 2 +- examples/snippets/union_primitives.py | 4 +- .../serializers/factories/homogeneous.py | 82 +++++++++++++++++-- tests/test_homogeneous_collections.py | 24 +++++- tests/test_misc.py | 2 +- 6 files changed, 102 insertions(+), 14 deletions(-) diff --git a/examples/snippets/model_template.py b/examples/snippets/model_template.py index b9a4ee8..8b6a0cc 100644 --- a/examples/snippets/model_template.py +++ b/examples/snippets/model_template.py @@ -27,7 +27,7 @@ class Config: class Vehicles(BaseXmlModel, tag='vehicles'): - items: List[Union[Car, Airplane]] + items: List[Union[Car, Airplane]] = element() # [model-end] diff --git a/examples/snippets/union_models.py b/examples/snippets/union_models.py index 791f2da..573a0ac 100644 --- a/examples/snippets/union_models.py +++ b/examples/snippets/union_models.py @@ -18,7 +18,7 @@ class MouseEvent(Event, tag='mouse'): class Log(BaseXmlModel, tag='log'): - events: List[Union[KeyboardEvent, MouseEvent]] + events: List[Union[KeyboardEvent, MouseEvent]] = element() # [model-end] diff --git a/examples/snippets/union_primitives.py b/examples/snippets/union_primitives.py index 434efdc..f4356fa 100644 --- a/examples/snippets/union_primitives.py +++ b/examples/snippets/union_primitives.py @@ -1,7 +1,7 @@ import datetime as dt from typing import List, Optional, Union -from pydantic_xml import BaseXmlModel, attr +from pydantic_xml import BaseXmlModel, attr, element # [model-start] @@ -11,7 +11,7 @@ class Message(BaseXmlModel, tag='Message'): class Messages(BaseXmlModel): - messages: List[Message] + messages: List[Message] = element() # [model-end] diff --git a/pydantic_xml/serializers/factories/homogeneous.py b/pydantic_xml/serializers/factories/homogeneous.py index 09b9eac..eaf3101 100644 --- a/pydantic_xml/serializers/factories/homogeneous.py +++ b/pydantic_xml/serializers/factories/homogeneous.py @@ -1,6 +1,7 @@ import dataclasses as dc from copy import deepcopy -from typing import Any, List, Optional, Type +from inspect import isclass +from typing import Any, Collection, List, Optional, Type import pydantic as pd @@ -17,6 +18,79 @@ class HomogeneousSerializerFactory: Homogeneous collection type serializer factory. """ + class TextSerializer(Serializer): + def __init__( + self, model: Type['pxml.BaseXmlModel'], model_field: pd.fields.ModelField, ctx: Serializer.Context, + ): + assert model_field.sub_fields and len(model_field.sub_fields) == 1 + if ( + isclass(model_field.type_) and issubclass(model_field.type_, pxml.BaseXmlModel) or + issubclass(model_field.type_, tuple) + ): + raise errors.ModelFieldError( + model.__name__, model_field.name, "Inline list value should be of scalar type", + ) + + def serialize( + self, element: XmlElementWriter, value: Collection[Any], *, encoder: XmlEncoder, + skip_empty: bool = False, + ) -> Optional[XmlElementWriter]: + if value is None or skip_empty and len(value) == 0: + return element + + encoded = " ".join(encoder.encode(val) for val in value) + element.set_text(encoded) + return element + + def deserialize(self, element: Optional[XmlElementReader]) -> Optional[List[Any]]: + if element is None: + return None + + text = element.pop_text() + + if text is None: + return [] + + return [value for value in text.split()] + + class AttributeSerializer(Serializer): + def __init__( + self, model: Type['pxml.BaseXmlModel'], model_field: pd.fields.ModelField, ctx: Serializer.Context, + ): + assert model_field.sub_fields and len(model_field.sub_fields) == 1 + if issubclass(model_field.type_, pxml.BaseXmlModel): + raise errors.ModelFieldError( + model.__name__, model_field.name, "Inline list value should be of scalar type", + ) + + name, ns, nsmap = self._get_entity_info(model_field) + + assert name is not None, "attr must be name" + + self.attr_name = QName.from_alias(tag=name, ns=ns, nsmap=nsmap, is_attr=True).uri + + def serialize( + self, element: XmlElementWriter, value: Collection[Any], *, encoder: XmlEncoder, + skip_empty: bool = False, + ) -> Optional[XmlElementWriter]: + if value is None or skip_empty and len(value) == 0: + return element + + encoded = " ".join(encoder.encode(val) for val in value) + element.set_attribute(self.attr_name, encoded) + return element + + def deserialize(self, element: Optional[XmlElementReader]) -> Optional[List[Any]]: + if element is None: + return None + + attribute = element.pop_attrib(self.attr_name) + + if attribute is None: + return [] + + return [value for value in attribute.split()] + class ElementSerializer(Serializer): def __init__( self, model: Type['pxml.BaseXmlModel'], model_field: pd.fields.ModelField, ctx: Serializer.Context, @@ -103,10 +177,8 @@ def build( if field_location is Location.ELEMENT: return cls.ElementSerializer(model, model_field, ctx) elif field_location is Location.MISSING: - return cls.ElementSerializer(model, model_field, ctx) + return cls.TextSerializer(model, model_field, ctx) elif field_location is Location.ATTRIBUTE: - raise errors.ModelFieldError( - model.__name__, model_field.name, "attributes of collection type are not supported", - ) + return cls.AttributeSerializer(model, model_field, ctx) else: raise AssertionError("unreachable") diff --git a/tests/test_homogeneous_collections.py b/tests/test_homogeneous_collections.py index cb0b61e..ae60a48 100644 --- a/tests/test_homogeneous_collections.py +++ b/tests/test_homogeneous_collections.py @@ -121,11 +121,27 @@ class RootModel(BaseXmlModel, tag='model'): assert_xml_equal(actual_xml, xml) -def test_homogeneous_definition_errors(): - with pytest.raises(errors.ModelFieldError): - class TestModel(BaseXmlModel): - attr1: List[int] = attr() +def test_text_list_extraction(): + class RootModel(BaseXmlModel, tag="model"): + values: List[int] + xml = ''' + 1 2 70 -34 + ''' + + actual_obj = RootModel.from_xml(xml) + expected_obj = RootModel( + values = [1, 2, 70, -34], + ) + + assert actual_obj == expected_obj + + actual_xml = actual_obj.to_xml() + assert_xml_equal(actual_xml, xml) + + +def test_homogeneous_definition_errors(): + breakpoint() with pytest.raises(errors.ModelFieldError): class TestModel(BaseXmlModel): attr1: List[Tuple[int, ...]] diff --git a/tests/test_misc.py b/tests/test_misc.py index cbab977..30e7003 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -46,7 +46,7 @@ class TestSubModel(BaseXmlModel, tag='model'): class TestModel(BaseXmlModel, tag='model'): model: TestSubModel - list: List[TestSubModel] = [] + list: List[TestSubModel] = element(default=[]) tuple: Optional[Tuple[TestSubModel, TestSubModel]] = None attrs: Dict[str, str] = {} wrapped: Optional[str] = wrapped('envelope') From d6c038c968d173b0bc637901a5d066f05b9ae0f7 Mon Sep 17 00:00:00 2001 From: Jacob Nilsson Date: Tue, 14 Feb 2023 21:11:34 +0100 Subject: [PATCH 2/7] Fix merge conflict. --- tests/test_homogeneous_collections.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_homogeneous_collections.py b/tests/test_homogeneous_collections.py index ae60a48..a0f528c 100644 --- a/tests/test_homogeneous_collections.py +++ b/tests/test_homogeneous_collections.py @@ -172,3 +172,10 @@ class TestSubModel(BaseXmlModel): class TestModel(BaseXmlModel): __root__: List[TestSubModel] + + with pytest.raises(errors.ModelFieldError): + class TestSubModel(BaseXmlModel): + attr: int + + class TestModel(BaseXmlModel): + text: List[TestSubModel] From d075694157d1cf8b4f1c99744ad88158f915dfa5 Mon Sep 17 00:00:00 2001 From: Jacob Nilsson Date: Mon, 23 Jan 2023 21:44:45 +0100 Subject: [PATCH 3/7] Add inline lists support for Attributes. Added new tests and updated old tests as this addition is a regression/backwards incompatibility. --- tests/test_homogeneous_collections.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/test_homogeneous_collections.py b/tests/test_homogeneous_collections.py index a0f528c..b1df2f5 100644 --- a/tests/test_homogeneous_collections.py +++ b/tests/test_homogeneous_collections.py @@ -140,8 +140,31 @@ class RootModel(BaseXmlModel, tag="model"): assert_xml_equal(actual_xml, xml) +def test_attr_list_extraction(): + class RootModel(BaseXmlModel, tag="model"): + values: List[float] = attr() + + xml = ''' + + ''' + # This will fail if scientific notation is used + # i.e. if 300 is replaced with 3e2 or 300, the deserializer + # will always use the standard notation with the added `.0`. + # While this behaviour fails the tests, it shouldn't + # matter in practice. + + actual_obj = RootModel.from_xml(xml) + expected_obj = RootModel( + values = [3.14, -1.0, 3e2] + ) + + assert actual_obj == expected_obj + + actual_xml = actual_obj.to_xml() + assert_xml_equal(actual_xml, xml) + + def test_homogeneous_definition_errors(): - breakpoint() with pytest.raises(errors.ModelFieldError): class TestModel(BaseXmlModel): attr1: List[Tuple[int, ...]] @@ -179,3 +202,4 @@ class TestSubModel(BaseXmlModel): class TestModel(BaseXmlModel): text: List[TestSubModel] + From 6d8f38cb624cfefdc6b13aa543b96a10aad7d2ea Mon Sep 17 00:00:00 2001 From: Jacob Nilsson Date: Fri, 3 Feb 2023 11:41:43 +0100 Subject: [PATCH 4/7] Add tests for homogeneous tuple. --- tests/test_homogeneous_collections.py | 43 ++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/tests/test_homogeneous_collections.py b/tests/test_homogeneous_collections.py index b1df2f5..a984676 100644 --- a/tests/test_homogeneous_collections.py +++ b/tests/test_homogeneous_collections.py @@ -140,6 +140,23 @@ class RootModel(BaseXmlModel, tag="model"): assert_xml_equal(actual_xml, xml) +def test_text_tuple_extraction(): + class RootModel(BaseXmlModel, tag="model"): + values: Tuple[int, ...] + + xml = ''' + 1 2 70 -34 + ''' + + actual_obj = RootModel.from_xml(xml) + expected_obj = RootModel( + values=[1, 2, 70, -34] + ) + + actual_xml = actual_obj.to_xml() + assert_xml_equal(actual_xml, xml) + + def test_attr_list_extraction(): class RootModel(BaseXmlModel, tag="model"): values: List[float] = attr() @@ -155,7 +172,31 @@ class RootModel(BaseXmlModel, tag="model"): actual_obj = RootModel.from_xml(xml) expected_obj = RootModel( - values = [3.14, -1.0, 3e2] + values=[3.14, -1.0, 3e2] + ) + + assert actual_obj == expected_obj + + actual_xml = actual_obj.to_xml() + assert_xml_equal(actual_xml, xml) + + +def test_attr_tuple_extraction(): + class RootModel(BaseXmlModel, tag="model"): + values: List[float] = attr() + + xml = ''' + + ''' + # This will fail if scientific notation is used + # i.e. if 300 is replaced with 3e2 or 300, the deserializer + # will always use the standard notation with the added `.0`. + # While this behaviour fails the tests, it shouldn't + # matter in practice. + + actual_obj = RootModel.from_xml(xml) + expected_obj = RootModel( + values=[3.14, -1.0, 3e2] ) assert actual_obj == expected_obj From f21f0b8fc89e5a5889424a138fad3c292f007cac Mon Sep 17 00:00:00 2001 From: Jacob Nilsson Date: Tue, 14 Feb 2023 19:12:17 +0100 Subject: [PATCH 5/7] Fix formatting errors. --- .pre-commit-config.yaml | 2 +- tests/test_homogeneous_collections.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7742ade..19127cd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -63,7 +63,7 @@ repos: - --multi-line=9 - --project=pydantic_xml - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.991 + rev: v1.0.0 hooks: - id: mypy stages: diff --git a/tests/test_homogeneous_collections.py b/tests/test_homogeneous_collections.py index a984676..682fc97 100644 --- a/tests/test_homogeneous_collections.py +++ b/tests/test_homogeneous_collections.py @@ -150,7 +150,7 @@ class RootModel(BaseXmlModel, tag="model"): actual_obj = RootModel.from_xml(xml) expected_obj = RootModel( - values=[1, 2, 70, -34] + values=[1, 2, 70, -34], ) actual_xml = actual_obj.to_xml() @@ -172,7 +172,7 @@ class RootModel(BaseXmlModel, tag="model"): actual_obj = RootModel.from_xml(xml) expected_obj = RootModel( - values=[3.14, -1.0, 3e2] + values=[3.14, -1.0, 3e2], ) assert actual_obj == expected_obj @@ -196,7 +196,7 @@ class RootModel(BaseXmlModel, tag="model"): actual_obj = RootModel.from_xml(xml) expected_obj = RootModel( - values=[3.14, -1.0, 3e2] + values=[3.14, -1.0, 3e2], ) assert actual_obj == expected_obj From 4ca2f74d1107b3d877906fe86d1292a837353783 Mon Sep 17 00:00:00 2001 From: Jacob Nilsson Date: Tue, 14 Feb 2023 21:32:43 +0100 Subject: [PATCH 6/7] Fix name not being set/extracted by _entity_get_info. --- pydantic_xml/serializers/factories/homogeneous.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pydantic_xml/serializers/factories/homogeneous.py b/pydantic_xml/serializers/factories/homogeneous.py index eaf3101..3180b0f 100644 --- a/pydantic_xml/serializers/factories/homogeneous.py +++ b/pydantic_xml/serializers/factories/homogeneous.py @@ -63,7 +63,9 @@ def __init__( model.__name__, model_field.name, "Inline list value should be of scalar type", ) - name, ns, nsmap = self._get_entity_info(model_field) + _, ns, nsmap = self._get_entity_info(model_field) + + name = model_field.name assert name is not None, "attr must be name" From 09159f50c18ae044f082c3425913b99dcdff4a22 Mon Sep 17 00:00:00 2001 From: Jacob Nilsson Date: Mon, 6 Mar 2023 10:36:27 +0100 Subject: [PATCH 7/7] Fix review comments. --- pydantic_xml/serializers/factories/homogeneous.py | 11 ++++------- tests/test_homogeneous_collections.py | 7 ++++--- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/pydantic_xml/serializers/factories/homogeneous.py b/pydantic_xml/serializers/factories/homogeneous.py index 3180b0f..77177dd 100644 --- a/pydantic_xml/serializers/factories/homogeneous.py +++ b/pydantic_xml/serializers/factories/homogeneous.py @@ -1,6 +1,5 @@ import dataclasses as dc from copy import deepcopy -from inspect import isclass from typing import Any, Collection, List, Optional, Type import pydantic as pd @@ -9,7 +8,7 @@ from pydantic_xml import errors from pydantic_xml.element import XmlElementReader, XmlElementWriter from pydantic_xml.serializers.encoder import XmlEncoder -from pydantic_xml.serializers.serializer import Location, PydanticShapeType, Serializer +from pydantic_xml.serializers.serializer import Location, PydanticShapeType, Serializer, is_xml_model from pydantic_xml.utils import QName, merge_nsmaps @@ -24,7 +23,7 @@ def __init__( ): assert model_field.sub_fields and len(model_field.sub_fields) == 1 if ( - isclass(model_field.type_) and issubclass(model_field.type_, pxml.BaseXmlModel) or + is_xml_model(model_field.type_) or issubclass(model_field.type_, tuple) ): raise errors.ModelFieldError( @@ -49,7 +48,7 @@ def deserialize(self, element: Optional[XmlElementReader]) -> Optional[List[Any] text = element.pop_text() if text is None: - return [] + return None return [value for value in text.split()] @@ -65,9 +64,7 @@ def __init__( _, ns, nsmap = self._get_entity_info(model_field) - name = model_field.name - - assert name is not None, "attr must be name" + name = model_field.alias self.attr_name = QName.from_alias(tag=name, ns=ns, nsmap=nsmap, is_attr=True).uri diff --git a/tests/test_homogeneous_collections.py b/tests/test_homogeneous_collections.py index 682fc97..4e94e9b 100644 --- a/tests/test_homogeneous_collections.py +++ b/tests/test_homogeneous_collections.py @@ -153,6 +153,8 @@ class RootModel(BaseXmlModel, tag="model"): values=[1, 2, 70, -34], ) + assert actual_obj == expected_obj + actual_xml = actual_obj.to_xml() assert_xml_equal(actual_xml, xml) @@ -183,7 +185,7 @@ class RootModel(BaseXmlModel, tag="model"): def test_attr_tuple_extraction(): class RootModel(BaseXmlModel, tag="model"): - values: List[float] = attr() + values: Tuple[float, ...] = attr() xml = ''' @@ -196,7 +198,7 @@ class RootModel(BaseXmlModel, tag="model"): actual_obj = RootModel.from_xml(xml) expected_obj = RootModel( - values=[3.14, -1.0, 3e2], + values=(3.14, -1.0, 3e2), ) assert actual_obj == expected_obj @@ -243,4 +245,3 @@ class TestSubModel(BaseXmlModel): class TestModel(BaseXmlModel): text: List[TestSubModel] -