Skip to content

Commit 6e88b73

Browse files
authored
Merge pull request #205 from dapper91/dev
- exclude_none and exclude_unset serialization flags support added.
2 parents ce20508 + 3b81aeb commit 6e88b73

File tree

19 files changed

+353
-36
lines changed

19 files changed

+353
-36
lines changed

CHANGELOG.rst

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
Changelog
22
=========
33

4+
2.12.0 (2024-08-24)
5+
-------------------
6+
7+
- exclude_none and exclude_unset serialization flags support added. See https://github.com/dapper91/pydantic-xml/pull/204.
8+
9+
410
2.11.0 (2024-05-11)
5-
------------------
11+
-------------------
612

713
- named tuple support added. See https://github.com/dapper91/pydantic-xml/issues/172
814

915

1016
2.10.0 (2024-05-09)
11-
------------------
17+
-------------------
1218

1319
- dynamic model creation support added. See https://pydantic-xml.readthedocs.io/en/latest/pages/misc.html#dynamic-model-creation
1420

docs/source/pages/misc.rst

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,33 @@ for a particular model during its declaration as illustrated in the following ex
9797
:start-after: xml-start
9898
:end-before: xml-end
9999

100+
It is also possible to exclude ``None`` values:
101+
102+
.. literalinclude:: ../../../examples/snippets/exclude_none.py
103+
:language: python
104+
:start-after: model-start
105+
:end-before: model-end
106+
107+
.. literalinclude:: ../../../examples/snippets/exclude_none.py
108+
:language: xml
109+
:lines: 2-
110+
:start-after: xml-start
111+
:end-before: xml-end
112+
113+
114+
... or unset values:
115+
116+
.. literalinclude:: ../../../examples/snippets/exclude_unset.py
117+
:language: python
118+
:start-after: model-start
119+
:end-before: model-end
120+
121+
.. literalinclude:: ../../../examples/snippets/exclude_unset.py
122+
:language: xml
123+
:lines: 2-
124+
:start-after: xml-start
125+
:end-before: xml-end
126+
100127

101128
Default namespace
102129
~~~~~~~~~~~~~~~~~

examples/snippets/exclude_none.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from typing import Literal, Optional
2+
from xml.etree.ElementTree import canonicalize
3+
4+
from pydantic_xml import BaseXmlModel, element
5+
6+
7+
# [model-start]
8+
class Product(BaseXmlModel, tag='Product'):
9+
title: Optional[str] = element(tag='Title', default=None)
10+
status: Optional[Literal['running', 'development']] = element(tag='Status', default=None)
11+
launched: Optional[int] = element(tag='Launched', default=None)
12+
13+
14+
product = Product(title="Starlink", status=None)
15+
xml = product.to_xml(exclude_none=True)
16+
# [model-end]
17+
18+
19+
# [xml-start]
20+
xml_doc = '''
21+
<Product>
22+
<Title>Starlink</Title>
23+
</Product>
24+
''' # [xml-end]
25+
26+
assert canonicalize(xml, strip_text=True) == canonicalize(xml_doc, strip_text=True)

examples/snippets/exclude_unset.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from typing import Literal, Optional
2+
from xml.etree.ElementTree import canonicalize
3+
4+
from pydantic_xml import BaseXmlModel, element
5+
6+
7+
# [model-start]
8+
class Product(BaseXmlModel, tag='Product'):
9+
title: Optional[str] = element(tag='Title', default=None)
10+
status: Optional[Literal['running', 'development']] = element(tag='Status', default=None)
11+
launched: Optional[int] = element(tag='Launched', default=None)
12+
13+
14+
product = Product(title="Starlink", status=None)
15+
xml = product.to_xml(exclude_unset=True)
16+
# [model-end]
17+
18+
19+
# [xml-start]
20+
xml_doc = '''
21+
<Product>
22+
<Title>Starlink</Title>
23+
<Status />
24+
</Product>
25+
''' # [xml-end]
26+
27+
assert canonicalize(xml, strip_text=True) == canonicalize(xml_doc, strip_text=True)

pydantic_xml/model.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -469,11 +469,15 @@ def from_xml(cls: Type[ModelT], source: Union[str, bytes], context: Optional[Dic
469469

470470
return cls.from_xml_tree(etree.fromstring(source), context=context)
471471

472-
def to_xml_tree(self, *, skip_empty: bool = False) -> etree.Element:
472+
def to_xml_tree(
473+
self, *, skip_empty: bool = False, exclude_none: bool = False, exclude_unset: bool = False,
474+
) -> etree.Element:
473475
"""
474476
Serializes the object to an xml tree.
475477
476478
:param skip_empty: skip empty elements (elements without sub-elements, attributes and text, Nones)
479+
:param exclude_none: exclude `None` values
480+
:param exclude_unset: exclude values that haven't been explicitly set
477481
:return: object xml representation
478482
"""
479483

@@ -485,21 +489,31 @@ def to_xml_tree(self, *, skip_empty: bool = False) -> etree.Element:
485489
self,
486490
by_alias=False,
487491
fallback=lambda obj: obj if not isinstance(obj, ElementT) else None, # for raw fields support
488-
), skip_empty=skip_empty,
492+
),
493+
skip_empty=skip_empty,
494+
exclude_none=exclude_none,
495+
exclude_unset=exclude_unset,
489496
)
490497

491498
return root.to_native()
492499

493-
def to_xml(self, *, skip_empty: bool = False, **kwargs: Any) -> Union[str, bytes]:
500+
def to_xml(
501+
self, *, skip_empty: bool = False, exclude_none: bool = False, exclude_unset: bool = False, **kwargs: Any,
502+
) -> Union[str, bytes]:
494503
"""
495504
Serializes the object to an xml string.
496505
497506
:param skip_empty: skip empty elements (elements without sub-elements, attributes and text, Nones)
507+
:param exclude_none: exclude `None` values
508+
:param exclude_unset: exclude values that haven't been explicitly set
498509
:param kwargs: additional xml serialization arguments
499510
:return: object xml representation
500511
"""
501512

502-
return etree.tostring(self.to_xml_tree(skip_empty=skip_empty), **kwargs)
513+
return etree.tostring(
514+
self.to_xml_tree(skip_empty=skip_empty, exclude_none=exclude_none, exclude_unset=exclude_unset),
515+
**kwargs,
516+
)
503517

504518

505519
@te.dataclass_transform(kw_only_default=True, field_specifiers=(attr, element, wrapped, pd.Field))

pydantic_xml/serializers/factories/heterogeneous.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,14 @@ def __init__(self, model_name: str, computed: bool, inner_serializers: Tuple[Ser
2626
self._inner_serializers = inner_serializers
2727

2828
def serialize(
29-
self, element: XmlElementWriter, value: List[Any], encoded: List[Any], *, skip_empty: bool = False,
29+
self,
30+
element: XmlElementWriter,
31+
value: List[Any],
32+
encoded: List[Any],
33+
*,
34+
skip_empty: bool = False,
35+
exclude_none: bool = False,
36+
exclude_unset: bool = False,
3037
) -> Optional[XmlElementWriter]:
3138
if value is None:
3239
return element
@@ -38,7 +45,9 @@ def serialize(
3845
raise errors.SerializationError("value length is incorrect")
3946

4047
for serializer, val, enc in zip(self._inner_serializers, value, encoded):
41-
serializer.serialize(element, val, enc, skip_empty=skip_empty)
48+
serializer.serialize(
49+
element, val, enc, skip_empty=skip_empty, exclude_none=exclude_none, exclude_unset=exclude_unset,
50+
)
4251

4352
return element
4453

pydantic_xml/serializers/factories/homogeneous.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,14 @@ def __init__(self, model_name: str, computed: bool, inner_serializer: Serializer
3838
self._inner_serializer = inner_serializer
3939

4040
def serialize(
41-
self, element: XmlElementWriter, value: List[Any], encoded: List[Any], *, skip_empty: bool = False,
41+
self,
42+
element: XmlElementWriter,
43+
value: List[Any],
44+
encoded: List[Any],
45+
*,
46+
skip_empty: bool = False,
47+
exclude_none: bool = False,
48+
exclude_unset: bool = False,
4249
) -> Optional[XmlElementWriter]:
4350
if value is None:
4451
return element
@@ -50,7 +57,9 @@ def serialize(
5057
if skip_empty and val is None:
5158
continue
5259

53-
self._inner_serializer.serialize(element, val, enc, skip_empty=skip_empty)
60+
self._inner_serializer.serialize(
61+
element, val, enc, skip_empty=skip_empty, exclude_none=exclude_none, exclude_unset=exclude_unset,
62+
)
5463

5564
return element
5665

pydantic_xml/serializers/factories/mapping.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ def serialize(
3232
encoded: Dict[str, Any],
3333
*,
3434
skip_empty: bool = False,
35+
exclude_none: bool = False,
36+
exclude_unset: bool = False,
3537
) -> Optional[XmlElementWriter]:
3638
if value is None:
3739
return element
@@ -100,12 +102,16 @@ def serialize(
100102
encoded: Dict[str, Any],
101103
*,
102104
skip_empty: bool = False,
105+
exclude_none: bool = False,
106+
exclude_unset: bool = False,
103107
) -> Optional[XmlElementWriter]:
104108
if skip_empty and len(value) == 0:
105109
return element
106110

107111
sub_element = element.make_element(self._element_name, nsmap=self._nsmap)
108-
super().serialize(sub_element, value, encoded, skip_empty=skip_empty)
112+
super().serialize(
113+
sub_element, value, encoded, skip_empty=skip_empty, exclude_none=exclude_none, exclude_unset=exclude_unset,
114+
)
109115
if skip_empty and sub_element.is_empty():
110116
return None
111117
else:

pydantic_xml/serializers/factories/model.py

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ def serialize(
158158
encoded: Dict[str, Any],
159159
*,
160160
skip_empty: bool = False,
161+
exclude_none: bool = False,
162+
exclude_unset: bool = False,
161163
) -> Optional[XmlElementWriter]:
162164
if value is None:
163165
return None
@@ -166,10 +168,17 @@ def serialize(
166168
skip_empty = self._model.__xml_skip_empty__
167169

168170
for field_name, field_serializer in self._field_serializers.items():
169-
if field_name not in self._fields_serialization_exclude:
170-
field_serializer.serialize(
171-
element, getattr(value, field_name), encoded[field_name], skip_empty=skip_empty,
172-
)
171+
if field_name in self._fields_serialization_exclude:
172+
continue
173+
if exclude_unset and field_name not in value.__pydantic_fields_set__:
174+
continue
175+
176+
field_serializer.serialize(
177+
element, getattr(value, field_name), encoded[field_name],
178+
skip_empty=skip_empty,
179+
exclude_none=exclude_none,
180+
exclude_unset=exclude_unset,
181+
)
173182

174183
return element
175184

@@ -269,14 +278,24 @@ def serialize(
269278
encoded: Dict[str, Any],
270279
*,
271280
skip_empty: bool = False,
281+
exclude_none: bool = False,
282+
exclude_unset: bool = False,
272283
) -> Optional[XmlElementWriter]:
273284
if value is None:
274285
return None
275286

287+
if exclude_unset and 'root' not in value.__pydantic_fields_set__:
288+
return None
289+
276290
if self._model.__xml_skip_empty__ is not None:
277291
skip_empty = self._model.__xml_skip_empty__
278292

279-
self._root_serializer.serialize(element, getattr(value, 'root'), encoded, skip_empty=skip_empty)
293+
self._root_serializer.serialize(
294+
element, getattr(value, 'root'), encoded,
295+
skip_empty=skip_empty,
296+
exclude_none=exclude_none,
297+
exclude_unset=exclude_unset,
298+
)
280299

281300
return element
282301

@@ -362,9 +381,14 @@ def serialize(
362381
encoded: Dict[str, Any],
363382
*,
364383
skip_empty: bool = False,
384+
exclude_none: bool = False,
385+
exclude_unset: bool = False,
365386
) -> Optional[XmlElementWriter]:
366387
assert self._model.__xml_serializer__ is not None, f"model {self._model.__name__} is partially initialized"
367388

389+
if exclude_none and value is None:
390+
return None
391+
368392
if self._nillable and value is None:
369393
sub_element = element.make_element(self._element_name, nsmap=self._nsmap)
370394
make_element_nill(sub_element)
@@ -375,7 +399,9 @@ def serialize(
375399
return None
376400

377401
sub_element = element.make_element(self._element_name, nsmap=self._nsmap)
378-
self._model.__xml_serializer__.serialize(sub_element, value, encoded, skip_empty=skip_empty)
402+
self._model.__xml_serializer__.serialize(
403+
sub_element, value, encoded, skip_empty=skip_empty, exclude_none=exclude_none, exclude_unset=exclude_unset,
404+
)
379405
if skip_empty and sub_element.is_empty():
380406
return None
381407
else:

pydantic_xml/serializers/factories/named_tuple.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,18 @@ def __init__(self, model_name: str, computed: bool, inner_serializers: Tuple[Ser
2626
self._inner_serializer = heterogeneous.ElementSerializer(model_name, computed, inner_serializers)
2727

2828
def serialize(
29-
self, element: XmlElementWriter, value: List[Any], encoded: List[Any], *, skip_empty: bool = False,
29+
self,
30+
element: XmlElementWriter,
31+
value: List[Any],
32+
encoded: List[Any],
33+
*,
34+
skip_empty: bool = False,
35+
exclude_none: bool = False,
36+
exclude_unset: bool = False,
3037
) -> Optional[XmlElementWriter]:
31-
return self._inner_serializer.serialize(element, value, encoded, skip_empty=skip_empty)
38+
return self._inner_serializer.serialize(
39+
element, value, encoded, skip_empty=skip_empty, exclude_none=exclude_none, exclude_unset=exclude_unset,
40+
)
3241

3342
def deserialize(
3443
self,

0 commit comments

Comments
 (0)