Skip to content

Commit

Permalink
Merge pull request #144 from dapper91/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
dapper91 authored Nov 26, 2023
2 parents c1ae049 + 7f26d33 commit ad0ef53
Show file tree
Hide file tree
Showing 8 changed files with 257 additions and 4 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ Changelog
=========


2.5.0 (2023-11-26)
------------------

- adjacent sub-elements support added. See https://github.com/dapper91/pydantic-xml/pull/143.


2.4.0 (2023-11-06)
------------------

Expand Down
70 changes: 70 additions & 0 deletions docs/source/pages/data-binding/homogeneous.rst
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,73 @@ Field of a mapping homogeneous collection type is bound to sub-elements attribut
:lines: 2-
:start-after: json-start
:end-before: json-end


Adjacent sub-elements
*********************

Some xml documents contain a list of adjacent elements related to each other.
To group such elements a homogeneous collection of heterogeneous ones may be used:

.. grid:: 2
:gutter: 2

.. grid-item-card:: Model

.. literalinclude:: ../../../../examples/snippets/homogeneous_tuples.py
:language: python
:start-after: model-start
:end-before: model-end

.. grid-item-card:: Document

.. tab-set::

.. tab-item:: XML

.. literalinclude:: ../../../../examples/snippets/homogeneous_tuples.py
:language: xml
:lines: 2-
:start-after: xml-start
:end-before: xml-end

.. tab-item:: JSON

.. literalinclude:: ../../../../examples/snippets/homogeneous_tuples.py
:language: json
:lines: 2-
:start-after: json-start
:end-before: json-end


To group sub-elements with different tags it is necessary to declare a sub-model for each one:

.. grid:: 2
:gutter: 2

.. grid-item-card:: Model

.. literalinclude:: ../../../../examples/snippets/homogeneous_models_tuples.py
:language: python
:start-after: model-start
:end-before: model-end

.. grid-item-card:: Document

.. tab-set::

.. tab-item:: XML

.. literalinclude:: ../../../../examples/snippets/homogeneous_models_tuples.py
:language: xml
:lines: 2-
:start-after: xml-start
:end-before: xml-end

.. tab-item:: JSON

.. literalinclude:: ../../../../examples/snippets/homogeneous_models_tuples.py
:language: json
:lines: 2-
:start-after: json-start
:end-before: json-end
60 changes: 60 additions & 0 deletions examples/snippets/homogeneous_models_tuples.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from typing import List, Optional, Tuple

from pydantic_xml import BaseXmlModel, RootXmlModel, attr


# [model-start]
class Product(BaseXmlModel, tag='product'):
status: str = attr()
title: str


class Launch(RootXmlModel[int], tag='launched'):
pass


class Products(RootXmlModel):
root: List[Tuple[Product, Optional[Launch]]]
# [model-end]


# [xml-start]
xml_doc = '''
<Products>
<product status="running">Several launch vehicles</product>
<launched>2013</launched>
<product status="running">Starlink</product>
<launched>2019</launched>
<product status="development">Starship</product>
</Products>
''' # [xml-end]

# [json-start]
json_doc = '''
[
[
{
"title": "Several launch vehicles",
"status": "running"
},
2013
],
[
{
"title": "Starlink",
"status": "running"
},
2019
],
[
{
"title": "Starship",
"status": "development"
},
null
]
]
''' # [json-end]

products = Products.from_xml(xml_doc)
assert products == Products.model_validate_json(json_doc)
43 changes: 43 additions & 0 deletions examples/snippets/homogeneous_tuples.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from typing import List, Optional, Tuple

from pydantic_xml import RootXmlModel, element


# [model-start]
class Products(RootXmlModel):
root: List[Tuple[str, Optional[int]]] = element(tag='info')
# [model-end]


# [xml-start]
xml_doc = '''
<Products>
<info type="status">running</info>
<info type="launched">2013</info>
<info type="status">running</info>
<info type="launched">2019</info>
<info type="status">development</info>
<info type="launched"></info>
</Products>
''' # [xml-end]

# [json-start]
json_doc = '''
[
[
"running",
2013
],
[
"running",
2019
],
[
"development",
null
]
]
''' # [json-end]

products = Products.from_xml(xml_doc)
assert products == Products.model_validate_json(json_doc)
6 changes: 5 additions & 1 deletion pydantic_xml/serializers/factories/heterogeneous.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,14 @@ def deserialize(
if element is None:
return None

return [
result = [
serializer.deserialize(element, context=context)
for serializer in self._inner_serializers
]
if all((value is None for value in result)):
return None
else:
return result


def from_core_schema(schema: pcs.TuplePositionalSchema, ctx: Serializer.Context) -> Serializer:
Expand Down
7 changes: 6 additions & 1 deletion pydantic_xml/serializers/factories/homogeneous.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,17 @@ def from_core_schema(schema: HomogeneousCollectionTypeSchema, ctx: Serializer.Co
SchemaTypeFamily.TYPED_MAPPING,
SchemaTypeFamily.UNION,
SchemaTypeFamily.IS_INSTANCE,
SchemaTypeFamily.HETEROGENEOUS_COLLECTION,
):
raise errors.ModelFieldError(
ctx.model_name, ctx.field_name, "collection item must be of primitive, model, mapping or union type",
)

if items_type_family not in (SchemaTypeFamily.MODEL, SchemaTypeFamily.UNION) and ctx.entity_location is None:
if items_type_family not in (
SchemaTypeFamily.MODEL,
SchemaTypeFamily.UNION,
SchemaTypeFamily.HETEROGENEOUS_COLLECTION,
) and ctx.entity_location is None:
raise errors.ModelFieldError(ctx.model_name, ctx.field_name, "entity name is not provided")

if ctx.entity_location is EntityLocation.ELEMENT:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "pydantic-xml"
version = "2.4.0"
version = "2.5.0"
description = "pydantic xml extension"
authors = ["Dmitry Pershin <[email protected]>"]
license = "Unlicense"
Expand Down
67 changes: 66 additions & 1 deletion tests/test_homogeneous_collections.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Dict, List, Set, Tuple
from typing import Dict, List, Optional, Set, Tuple

import pytest
from helpers import assert_xml_equal
Expand Down Expand Up @@ -121,6 +121,71 @@ class RootModel(BaseXmlModel, tag='model'):
assert_xml_equal(actual_xml, xml)


def test_list_of_tuples_extraction():
class RootModel(BaseXmlModel, tag='model'):
elements: List[Tuple[str, Optional[int]]] = element(tag='element')

xml = '''
<model>
<element>text1</element>
<element>1</element>
<element>text2</element>
<element></element>
<element>text3</element>
<element>3</element>
</model>
'''

actual_obj = RootModel.from_xml(xml)
expected_obj = RootModel(
elements=[
('text1', 1),
('text2', None),
('text3', 3),
],
)

assert actual_obj == expected_obj

actual_xml = actual_obj.to_xml()
assert_xml_equal(actual_xml, xml)


def test_list_of_tuples_of_models_extraction():
class SubModel1(RootXmlModel[str], tag='text'):
pass

class SubModel2(RootXmlModel[int], tag='number'):
pass

class RootModel(BaseXmlModel, tag='model'):
elements: List[Tuple[SubModel1, Optional[SubModel2]]]

xml = '''
<model>
<text>text1</text>
<number>1</number>
<text>text2</text>
<text>text3</text>
<number>3</number>
</model>
'''

actual_obj = RootModel.from_xml(xml)
expected_obj = RootModel(
elements=[
(SubModel1('text1'), SubModel2(1)),
(SubModel1('text2'), None),
(SubModel1('text3'), SubModel2(3)),
],
)

assert actual_obj == expected_obj

actual_xml = actual_obj.to_xml()
assert_xml_equal(actual_xml, xml)


def test_root_list_of_submodels_extraction():
class TestSubModel(BaseXmlModel, tag='model2'):
text: int
Expand Down

0 comments on commit ad0ef53

Please sign in to comment.