Skip to content

Commit c12b065

Browse files
committed
Add inline lists support for Attributes.
Added new tests and updated old tests as this addition is a regression/backwards incompatibility.
1 parent 17a2d71 commit c12b065

File tree

3 files changed

+64
-5
lines changed

3 files changed

+64
-5
lines changed

pydantic_xml/serializers.py

+35-3
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,37 @@ def serialize(
581581
def deserialize(self, element: etree.Element) -> Optional[List[Any]]:
582582
return [value for value in element.text.split()]
583583

584+
class AttributeSerializer(Serializer):
585+
def __init__(
586+
self, model: Type['pxml.BaseXmlModel'], model_field: pd.fields.ModelField, ctx: Serializer.Context,
587+
):
588+
assert len(model_field.sub_fields) == 1
589+
if issubclass(model_field.type_, pxml.BaseXmlModel):
590+
raise errors.ModelFieldError(
591+
model.__name__, model_field.name, "Inline list value should be of scalar type",
592+
)
593+
594+
ns_attrs = model.__xml_ns_attrs__
595+
name = ctx.entity_name or model_field.alias
596+
ns = ctx.entity_ns or (ctx.parent_ns if ns_attrs else None)
597+
nsmap = ctx.parent_nsmap
598+
599+
self.attr_name = QName.from_alias(tag=name, ns=ns, nsmap=nsmap, is_attr=True).uri
600+
601+
602+
def serialize(
603+
self, element: etree.Element, value: Any, *, encoder: XmlEncoder, skip_empty: bool = False,
604+
) -> Optional[etree.Element]:
605+
if value is None or skip_empty and len(value) == 0:
606+
return element
607+
608+
encoded = " ".join(encoder.encode(val) for val in value)
609+
element.set(self.attr_name, encoded)
610+
return element
611+
612+
def deserialize(self, element: etree.Element) -> Optional[List[Any]]:
613+
return [value for value in element.get(self.attr_name).split()]
614+
584615
class ElementSerializer(Serializer):
585616
def __init__(
586617
self, model: Type['pxml.BaseXmlModel'], model_field: pd.fields.ModelField, ctx: Serializer.Context,
@@ -658,9 +689,10 @@ def build(
658689
elif field_location is Location.MISSING:
659690
return cls.TextSerializer(model, model_field, ctx)
660691
elif field_location is Location.ATTRIBUTE:
661-
raise errors.ModelFieldError(
662-
model.__name__, model_field.name, "attributes of collection type are not supported",
663-
)
692+
return cls.AttributeSerializer(model, model_field, ctx)
693+
# raise errors.ModelFieldError(
694+
# model.__name__, model_field.name, "attributes of collection type are not supported",
695+
# )
664696
else:
665697
raise AssertionError("unreachable")
666698

tests/test_homogeneous_collections.py

+28-1
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,37 @@ class RootModel(BaseXmlModel, tag="model"):
140140
assert_xml_equal(actual_xml, xml)
141141

142142

143+
def test_attr_list_extraction():
144+
class RootModel(BaseXmlModel, tag="model"):
145+
values: List[float] = attr()
146+
147+
xml = '''
148+
<model values="3.14 -1.0 300.0"/>
149+
'''
150+
# This will fail if scientific notation is used
151+
# i.e. if 300 is replaced with 3e2 or 300, the deserializer
152+
# will always use the standard notation with the added `.0`.
153+
# While this behaviour fails the tests, it shouldn't
154+
# matter in practice.
155+
156+
actual_obj = RootModel.from_xml(xml)
157+
expected_obj = RootModel(
158+
values = [3.14, -1.0, 3e2]
159+
)
160+
161+
assert actual_obj == expected_obj
162+
163+
actual_xml = actual_obj.to_xml()
164+
assert_xml_equal(actual_xml, xml)
165+
166+
143167
def test_homogeneous_definition_errors():
144168
with pytest.raises(errors.ModelFieldError):
169+
class SubModel(BaseXmlModel):
170+
text: str
171+
145172
class TestModel(BaseXmlModel):
146-
attr1: List[int] = attr()
173+
attr1: List[SubModel] = attr()
147174

148175
with pytest.raises(errors.ModelFieldError):
149176
class TestModel(BaseXmlModel):

tests/test_misc.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class TestSubModel(BaseXmlModel, tag='model'):
3939

4040
class TestModel(BaseXmlModel, tag='model'):
4141
model: TestSubModel
42-
list: List[TestSubModel] = []
42+
list: List[TestSubModel] = element(default=[])
4343
tuple: Optional[Tuple[TestSubModel, TestSubModel]] = None
4444
attrs: Dict[str, str] = {}
4545
wrapped: Optional[str] = wrapped('envelope')

0 commit comments

Comments
 (0)