Skip to content

Commit ad0ef53

Browse files
authored
Merge pull request #144 from dapper91/dev
Dev
2 parents c1ae049 + 7f26d33 commit ad0ef53

File tree

8 files changed

+257
-4
lines changed

8 files changed

+257
-4
lines changed

CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ Changelog
22
=========
33

44

5+
2.5.0 (2023-11-26)
6+
------------------
7+
8+
- adjacent sub-elements support added. See https://github.com/dapper91/pydantic-xml/pull/143.
9+
10+
511
2.4.0 (2023-11-06)
612
------------------
713

docs/source/pages/data-binding/homogeneous.rst

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,73 @@ Field of a mapping homogeneous collection type is bound to sub-elements attribut
120120
:lines: 2-
121121
:start-after: json-start
122122
:end-before: json-end
123+
124+
125+
Adjacent sub-elements
126+
*********************
127+
128+
Some xml documents contain a list of adjacent elements related to each other.
129+
To group such elements a homogeneous collection of heterogeneous ones may be used:
130+
131+
.. grid:: 2
132+
:gutter: 2
133+
134+
.. grid-item-card:: Model
135+
136+
.. literalinclude:: ../../../../examples/snippets/homogeneous_tuples.py
137+
:language: python
138+
:start-after: model-start
139+
:end-before: model-end
140+
141+
.. grid-item-card:: Document
142+
143+
.. tab-set::
144+
145+
.. tab-item:: XML
146+
147+
.. literalinclude:: ../../../../examples/snippets/homogeneous_tuples.py
148+
:language: xml
149+
:lines: 2-
150+
:start-after: xml-start
151+
:end-before: xml-end
152+
153+
.. tab-item:: JSON
154+
155+
.. literalinclude:: ../../../../examples/snippets/homogeneous_tuples.py
156+
:language: json
157+
:lines: 2-
158+
:start-after: json-start
159+
:end-before: json-end
160+
161+
162+
To group sub-elements with different tags it is necessary to declare a sub-model for each one:
163+
164+
.. grid:: 2
165+
:gutter: 2
166+
167+
.. grid-item-card:: Model
168+
169+
.. literalinclude:: ../../../../examples/snippets/homogeneous_models_tuples.py
170+
:language: python
171+
:start-after: model-start
172+
:end-before: model-end
173+
174+
.. grid-item-card:: Document
175+
176+
.. tab-set::
177+
178+
.. tab-item:: XML
179+
180+
.. literalinclude:: ../../../../examples/snippets/homogeneous_models_tuples.py
181+
:language: xml
182+
:lines: 2-
183+
:start-after: xml-start
184+
:end-before: xml-end
185+
186+
.. tab-item:: JSON
187+
188+
.. literalinclude:: ../../../../examples/snippets/homogeneous_models_tuples.py
189+
:language: json
190+
:lines: 2-
191+
:start-after: json-start
192+
:end-before: json-end
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from typing import List, Optional, Tuple
2+
3+
from pydantic_xml import BaseXmlModel, RootXmlModel, attr
4+
5+
6+
# [model-start]
7+
class Product(BaseXmlModel, tag='product'):
8+
status: str = attr()
9+
title: str
10+
11+
12+
class Launch(RootXmlModel[int], tag='launched'):
13+
pass
14+
15+
16+
class Products(RootXmlModel):
17+
root: List[Tuple[Product, Optional[Launch]]]
18+
# [model-end]
19+
20+
21+
# [xml-start]
22+
xml_doc = '''
23+
<Products>
24+
<product status="running">Several launch vehicles</product>
25+
<launched>2013</launched>
26+
<product status="running">Starlink</product>
27+
<launched>2019</launched>
28+
<product status="development">Starship</product>
29+
</Products>
30+
''' # [xml-end]
31+
32+
# [json-start]
33+
json_doc = '''
34+
[
35+
[
36+
{
37+
"title": "Several launch vehicles",
38+
"status": "running"
39+
},
40+
2013
41+
],
42+
[
43+
{
44+
"title": "Starlink",
45+
"status": "running"
46+
},
47+
2019
48+
],
49+
[
50+
{
51+
"title": "Starship",
52+
"status": "development"
53+
},
54+
null
55+
]
56+
]
57+
''' # [json-end]
58+
59+
products = Products.from_xml(xml_doc)
60+
assert products == Products.model_validate_json(json_doc)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from typing import List, Optional, Tuple
2+
3+
from pydantic_xml import RootXmlModel, element
4+
5+
6+
# [model-start]
7+
class Products(RootXmlModel):
8+
root: List[Tuple[str, Optional[int]]] = element(tag='info')
9+
# [model-end]
10+
11+
12+
# [xml-start]
13+
xml_doc = '''
14+
<Products>
15+
<info type="status">running</info>
16+
<info type="launched">2013</info>
17+
<info type="status">running</info>
18+
<info type="launched">2019</info>
19+
<info type="status">development</info>
20+
<info type="launched"></info>
21+
</Products>
22+
''' # [xml-end]
23+
24+
# [json-start]
25+
json_doc = '''
26+
[
27+
[
28+
"running",
29+
2013
30+
],
31+
[
32+
"running",
33+
2019
34+
],
35+
[
36+
"development",
37+
null
38+
]
39+
]
40+
''' # [json-end]
41+
42+
products = Products.from_xml(xml_doc)
43+
assert products == Products.model_validate_json(json_doc)

pydantic_xml/serializers/factories/heterogeneous.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,14 @@ def deserialize(
5151
if element is None:
5252
return None
5353

54-
return [
54+
result = [
5555
serializer.deserialize(element, context=context)
5656
for serializer in self._inner_serializers
5757
]
58+
if all((value is None for value in result)):
59+
return None
60+
else:
61+
return result
5862

5963

6064
def from_core_schema(schema: pcs.TuplePositionalSchema, ctx: Serializer.Context) -> Serializer:

pydantic_xml/serializers/factories/homogeneous.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,17 @@ def from_core_schema(schema: HomogeneousCollectionTypeSchema, ctx: Serializer.Co
7575
SchemaTypeFamily.TYPED_MAPPING,
7676
SchemaTypeFamily.UNION,
7777
SchemaTypeFamily.IS_INSTANCE,
78+
SchemaTypeFamily.HETEROGENEOUS_COLLECTION,
7879
):
7980
raise errors.ModelFieldError(
8081
ctx.model_name, ctx.field_name, "collection item must be of primitive, model, mapping or union type",
8182
)
8283

83-
if items_type_family not in (SchemaTypeFamily.MODEL, SchemaTypeFamily.UNION) and ctx.entity_location is None:
84+
if items_type_family not in (
85+
SchemaTypeFamily.MODEL,
86+
SchemaTypeFamily.UNION,
87+
SchemaTypeFamily.HETEROGENEOUS_COLLECTION,
88+
) and ctx.entity_location is None:
8489
raise errors.ModelFieldError(ctx.model_name, ctx.field_name, "entity name is not provided")
8590

8691
if ctx.entity_location is EntityLocation.ELEMENT:

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "pydantic-xml"
3-
version = "2.4.0"
3+
version = "2.5.0"
44
description = "pydantic xml extension"
55
authors = ["Dmitry Pershin <dapper1291@gmail.com>"]
66
license = "Unlicense"

tests/test_homogeneous_collections.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Dict, List, Set, Tuple
1+
from typing import Dict, List, Optional, Set, Tuple
22

33
import pytest
44
from helpers import assert_xml_equal
@@ -121,6 +121,71 @@ class RootModel(BaseXmlModel, tag='model'):
121121
assert_xml_equal(actual_xml, xml)
122122

123123

124+
def test_list_of_tuples_extraction():
125+
class RootModel(BaseXmlModel, tag='model'):
126+
elements: List[Tuple[str, Optional[int]]] = element(tag='element')
127+
128+
xml = '''
129+
<model>
130+
<element>text1</element>
131+
<element>1</element>
132+
<element>text2</element>
133+
<element></element>
134+
<element>text3</element>
135+
<element>3</element>
136+
</model>
137+
'''
138+
139+
actual_obj = RootModel.from_xml(xml)
140+
expected_obj = RootModel(
141+
elements=[
142+
('text1', 1),
143+
('text2', None),
144+
('text3', 3),
145+
],
146+
)
147+
148+
assert actual_obj == expected_obj
149+
150+
actual_xml = actual_obj.to_xml()
151+
assert_xml_equal(actual_xml, xml)
152+
153+
154+
def test_list_of_tuples_of_models_extraction():
155+
class SubModel1(RootXmlModel[str], tag='text'):
156+
pass
157+
158+
class SubModel2(RootXmlModel[int], tag='number'):
159+
pass
160+
161+
class RootModel(BaseXmlModel, tag='model'):
162+
elements: List[Tuple[SubModel1, Optional[SubModel2]]]
163+
164+
xml = '''
165+
<model>
166+
<text>text1</text>
167+
<number>1</number>
168+
<text>text2</text>
169+
<text>text3</text>
170+
<number>3</number>
171+
</model>
172+
'''
173+
174+
actual_obj = RootModel.from_xml(xml)
175+
expected_obj = RootModel(
176+
elements=[
177+
(SubModel1('text1'), SubModel2(1)),
178+
(SubModel1('text2'), None),
179+
(SubModel1('text3'), SubModel2(3)),
180+
],
181+
)
182+
183+
assert actual_obj == expected_obj
184+
185+
actual_xml = actual_obj.to_xml()
186+
assert_xml_equal(actual_xml, xml)
187+
188+
124189
def test_root_list_of_submodels_extraction():
125190
class TestSubModel(BaseXmlModel, tag='model2'):
126191
text: int

0 commit comments

Comments
 (0)