Skip to content

Commit 405b3bb

Browse files
authored
Merge pull request #262 from dapper91/dev
- annotated form of field serializer/validator support added.
2 parents ed885d3 + 32e205c commit 405b3bb

File tree

10 files changed

+114
-19
lines changed

10 files changed

+114
-19
lines changed

CHANGELOG.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
Changelog
22
=========
33

4+
5+
2.17.0 (2025-05-18)
6+
-------------------
7+
8+
- feat: annotated form of field serializer/validator support added. See https://github.com/dapper91/pydantic-xml/pull/261.
9+
10+
411
2.16.0 (2025-04-20)
512
-------------------
613

docs/source/pages/misc.rst

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,23 @@ The following example illustrate how to serialize ``xs:list`` element:
5454

5555
*model.py:*
5656

57-
.. literalinclude:: ../../../examples/xml-serialization/model.py
57+
.. literalinclude:: ../../../examples/xml-serialization-decorator/model.py
5858
:language: python
5959

6060
*doc.xml:*
6161

62-
.. literalinclude:: ../../../examples/xml-serialization/doc.xml
62+
.. literalinclude:: ../../../examples/xml-serialization-decorator/doc.xml
6363
:language: xml
6464

6565

66+
``pydantic-xml`` also supports the ``Annotated`` typing form to attach metadata to an annotation:
67+
68+
*model.py:*
69+
70+
.. literalinclude:: ../../../examples/xml-serialization-annotation/model.py
71+
:language: python
72+
73+
6674
Optional type encoding
6775
~~~~~~~~~~~~~~~~~~~~~~
6876

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import pathlib
2+
from typing import Annotated, List, Type
3+
from xml.etree.ElementTree import canonicalize
4+
5+
import pydantic_xml as pxml
6+
from pydantic_xml.element import XmlElementReader, XmlElementWriter
7+
8+
9+
def validate_space_separated_list(
10+
cls: Type[pxml.BaseXmlModel],
11+
element: XmlElementReader,
12+
field_name: str,
13+
) -> List[float]:
14+
if element := element.pop_element(field_name, search_mode=cls.__xml_search_mode__):
15+
return list(map(float, element.pop_text().split()))
16+
17+
return []
18+
19+
20+
def serialize_space_separated_list(
21+
model: pxml.BaseXmlModel,
22+
element: XmlElementWriter,
23+
value: List[float],
24+
field_name: str,
25+
) -> None:
26+
sub_element = element.make_element(tag=field_name, nsmap=None)
27+
sub_element.set_text(' '.join(map(str, value)))
28+
29+
element.append_element(sub_element)
30+
31+
32+
SpaceSeparatedValueList = Annotated[
33+
List[float],
34+
pxml.XmlFieldValidator(validate_space_separated_list),
35+
pxml.XmlFieldSerializer(serialize_space_separated_list),
36+
]
37+
38+
39+
class Plot(pxml.BaseXmlModel):
40+
x: SpaceSeparatedValueList = pxml.element()
41+
y: SpaceSeparatedValueList = pxml.element()
42+
43+
44+
xml_doc = pathlib.Path('./doc.xml').read_text()
45+
plot = Plot.from_xml(xml_doc)
46+
47+
assert canonicalize(plot.to_xml(), strip_text=True) == canonicalize(xml_doc, strip_text=True)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<Plot>
2+
<x>0.0 1.0 2.0 3.0 4.0 5.0</x>
3+
<y>0.0 3.2 5.4 4.1 2.0 -1.2</y>
4+
</Plot>

pydantic_xml/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
from . import config, errors, model
66
from .errors import ModelError, ParsingError
7-
from .model import BaseXmlModel, RootXmlModel, attr, computed_attr, computed_element, create_model, element, wrapped
8-
from .model import xml_field_serializer, xml_field_validator
7+
from .model import BaseXmlModel, RootXmlModel, XmlFieldSerializer, XmlFieldValidator, attr, computed_attr
8+
from .model import computed_element, create_model, element, wrapped, xml_field_serializer, xml_field_validator
99

1010
__all__ = (
1111
'BaseXmlModel',
@@ -22,4 +22,6 @@
2222
'model',
2323
'xml_field_serializer',
2424
'xml_field_validator',
25+
'XmlFieldValidator',
26+
'XmlFieldSerializer',
2527
)

pydantic_xml/model.py

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
'computed_element',
2727
'xml_field_serializer',
2828
'xml_field_validator',
29+
'XmlFieldSerializer',
30+
'XmlFieldValidator',
2931
'BaseXmlModel',
3032
'RootXmlModel',
3133
)
@@ -355,6 +357,16 @@ def wrapper(func: SerializerFuncT) -> SerializerFuncT:
355357
return wrapper
356358

357359

360+
@dc.dataclass(frozen=True)
361+
class XmlFieldValidator:
362+
func: ValidatorFunc
363+
364+
365+
@dc.dataclass(frozen=True)
366+
class XmlFieldSerializer:
367+
func: SerializerFunc
368+
369+
358370
@te.dataclass_transform(kw_only_default=True, field_specifiers=(attr, element, wrapped, pd.Field))
359371
class XmlModelMeta(ModelMetaclass):
360372
"""
@@ -374,8 +386,32 @@ def __new__(
374386
if not is_abstract:
375387
cls.__build_serializer__()
376388

389+
cls._collect_xml_field_serializers_validators(cls)
390+
377391
return cls
378392

393+
@classmethod
394+
def _collect_xml_field_serializers_validators(mcls, cls: Type['BaseXmlModel']) -> None:
395+
for field_name, field_info in cls.model_fields.items():
396+
for metadatum in field_info.metadata:
397+
if isinstance(metadatum, XmlFieldValidator):
398+
cls.__xml_field_validators__[field_name] = metadatum.func
399+
if isinstance(metadatum, XmlFieldSerializer):
400+
cls.__xml_field_serializers__[field_name] = metadatum.func
401+
402+
# find custom validators/serializers in all defined attributes
403+
# though we want to skip any BaseModel attributes, as these can never be field
404+
# serializers/validators, and getting certain pydantic fields
405+
# may cause recursion errors for recursive / self-referential models
406+
for attr_name in set(dir(cls)) - set(dir(BaseModel)):
407+
if func := getattr(cls, attr_name, None):
408+
if fields := getattr(func, '__xml_field_serializer__', None):
409+
for field in fields:
410+
cls.__xml_field_serializers__[field] = func
411+
if fields := getattr(func, '__xml_field_validator__', None):
412+
for field in fields:
413+
cls.__xml_field_validators__[field] = func
414+
379415

380416
ModelT = TypeVar('ModelT', bound='BaseXmlModel')
381417

@@ -435,19 +471,6 @@ def __init_subclass__(
435471
cls.__xml_field_serializers__ = {}
436472
cls.__xml_field_validators__ = {}
437473

438-
# find custom validators/serializers in all defined attributes
439-
# though we want to skip any Base(Xml)Model attributes, as these can never be field
440-
# serializers/validators, and getting certain pydantic fields, like __pydantic_post_init__
441-
# may cause recursion errors for recursive / self-referential models
442-
for attr_name in set(dir(cls)) - set(dir(BaseXmlModel)):
443-
if func := getattr(cls, attr_name, None):
444-
if fields := getattr(func, '__xml_field_serializer__', None):
445-
for field in fields:
446-
cls.__xml_field_serializers__[field] = func
447-
if fields := getattr(func, '__xml_field_validator__', None):
448-
for field in fields:
449-
cls.__xml_field_validators__[field] = func
450-
451474
@classmethod
452475
def __build_serializer__(cls) -> None:
453476
if cls is BaseXmlModel:

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.16.0"
3+
version = "2.17.0"
44
description = "pydantic xml extension"
55
authors = ["Dmitry Pershin <[email protected]>"]
66
license = "Unlicense"

tests/test_examples.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ def test_snippets_py39(snippet: Path):
3737
'generic-model',
3838
'quickstart',
3939
'self-ref-model',
40-
'xml-serialization',
40+
'xml-serialization-decorator',
41+
pytest.param(
42+
'xml-serialization-annotation',
43+
marks=pytest.mark.skipif(sys.version_info < (3, 9), reason="requires python 3.9 and above"),
44+
),
4145
],
4246
)
4347
def example_dir(request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch):

0 commit comments

Comments
 (0)