Skip to content

Commit

Permalink
XmlSerializer: integrate XmlElements
Browse files Browse the repository at this point in the history
  • Loading branch information
tefra committed Oct 25, 2020
1 parent 1fcf610 commit 091330f
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 7 deletions.
84 changes: 83 additions & 1 deletion tests/formats/dataclass/serializers/test_xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@
from xsdata.exceptions import SerializerError
from xsdata.exceptions import XmlContextError
from xsdata.formats.dataclass.models.elements import XmlElement
from xsdata.formats.dataclass.models.elements import XmlElements
from xsdata.formats.dataclass.models.elements import XmlText
from xsdata.formats.dataclass.models.elements import XmlVar
from xsdata.formats.dataclass.models.elements import XmlWildcard
from xsdata.formats.dataclass.models.generics import AnyElement
from xsdata.formats.dataclass.models.generics import DerivedElement
from xsdata.formats.dataclass.serializers import XmlSerializer
from xsdata.formats.dataclass.serializers.mixins import XmlWriter
from xsdata.formats.dataclass.serializers.mixins import XmlWriterEvent
from xsdata.models.enums import QNames

Expand Down Expand Up @@ -279,6 +279,88 @@ def test_write_element_with_nillable_true(self):
self.assertIsInstance(result, Generator)
self.assertEqual(expected, list(result))

def test_write_choice_with_derived_primitive_value(self):
var = XmlElements(
name="compound",
qname="compound",
choices=[XmlElement(qname="a", name="a", types=[int])],
)
value = DerivedElement(qname="a", value=1)
expected = [
(XmlWriterEvent.START, "a"),
(XmlWriterEvent.DATA, 1),
(XmlWriterEvent.END, "a"),
]

result = self.serializer.write_value(value, var, "xsdata")
self.assertIsInstance(result, Generator)
self.assertEqual(expected, list(result))

def test_write_choice_with_derived_dataclass(self):
var = XmlElements(
name="compound",
qname="compound",
choices=[XmlElement(qname="a", name="a", types=[int])],
)
value = DerivedElement(qname="a", value=A("foo"))
expected = [
(XmlWriterEvent.START, "a"),
(XmlWriterEvent.ATTR, "a0", "foo"),
(XmlWriterEvent.END, "a"),
]

result = self.serializer.write_value(value, var, "xsdata")
self.assertIsInstance(result, Generator)
self.assertEqual(expected, list(result))

def test_write_choice_with_generic_object(self):
var = XmlElements(
name="compound",
qname="compound",
choices=[XmlElement(qname="a", name="a", types=[int])],
)
value = AnyElement(qname="a", text="1")
expected = [
(XmlWriterEvent.START, "a"),
(XmlWriterEvent.DATA, "1"),
(XmlWriterEvent.END, "a"),
(XmlWriterEvent.DATA, None),
]

result = self.serializer.write_value(value, var, "xsdata")
self.assertIsInstance(result, Generator)
self.assertEqual(expected, list(result))

def test_write_choice_with_raw_value(self):
var = XmlElements(
name="compound",
qname="compound",
choices=[XmlElement(qname="a", name="a", types=[int])],
)
expected = [
(XmlWriterEvent.START, "a"),
(XmlWriterEvent.DATA, 1),
(XmlWriterEvent.END, "a"),
]

result = self.serializer.write_value(1, var, "xsdata")
self.assertIsInstance(result, Generator)
self.assertEqual(expected, list(result))

def test_write_choice_when_no_matching_choice_exists(self):
var = XmlElements(
name="compound",
qname="compound",
choices=[XmlElement(qname="a", name="a", types=[float])],
)

with self.assertRaises(SerializerError) as cm:
result = self.serializer.write_value(1, var, "xsdata")
next(result)

msg = "XmlElements undefined choice: `compound` for `<class 'int'>`"
self.assertEqual(msg, str(cm.exception))

def test_write_value_with_list_value(self):
var = XmlElement(qname="a", name="a", list_element=True)
value = [True, False]
Expand Down
14 changes: 13 additions & 1 deletion xsdata/formats/dataclass/models/elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ def find_choice(self, qname: str) -> Optional["XmlVar"]:
"""Match and return a choice field by its qualified name."""
return None

def find_choice_typed(self, tp: Type) -> Optional["XmlVar"]:
"""Match and return a choice field by its types."""
return None


@dataclass(frozen=True)
class XmlElement(XmlVar):
Expand Down Expand Up @@ -147,13 +151,21 @@ def matches(self, qname: str) -> bool:
return self.find_choice(qname) is not None

def find_choice(self, qname: str) -> Optional[XmlVar]:
"""Find a choice field for the given qualified name."""
"""Match and return a choice field by its qualified name."""
for choice in self.choices:
if choice.matches(qname):
return choice

return None

def find_choice_typed(self, tp: Type) -> Optional["XmlVar"]:
"""Match and return a choice field by its types."""
for choice in self.choices:
if tp in choice.types:
return choice

return None


@dataclass(frozen=True)
class XmlWildcard(XmlVar):
Expand Down
40 changes: 35 additions & 5 deletions xsdata/formats/dataclass/serializers/xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@
from xsdata.formats.dataclass.serializers.writers import LxmlEventWriter
from xsdata.models.enums import QNames
from xsdata.utils import namespaces
from xsdata.utils.constants import EMPTY_MAP
from xsdata.utils.namespaces import split_qname


NoneStr = Optional[str]
EMPTY_MAP: Dict = {}


@dataclass
Expand Down Expand Up @@ -141,6 +141,8 @@ def write_value(self, value: Any, var: XmlVar, namespace: NoneStr) -> Generator:
yield from self.write_any_type(value, var, namespace)
elif var.dataclass:
yield from self.write_xsi_type(value, var, namespace)
elif var.is_elements:
yield from self.write_choice(value, var, namespace)
elif var.is_element:
yield from self.write_element(value, var, namespace)
else:
Expand Down Expand Up @@ -225,10 +227,33 @@ def xsi_type(self, var: XmlVar, value: Any, namespace: NoneStr) -> Optional[QNam
f"{value.__class__.__name__} is not derived from {clazz.__name__}"
)

@classmethod
def write_data(cls, value: Any, var: XmlVar, namespace: NoneStr) -> Generator:
"""Produce a data event for the given value."""
yield XmlWriterEvent.DATA, value
def write_choice(self, value: Any, var: XmlVar, namespace: NoneStr) -> Generator:
"""
Produce an events stream for the given value of a compound elements
field.
The value can be anything as long as we can match the qualified
name or its type to a choice.
"""
choice = None
if isinstance(value, DerivedElement):
choice = var.find_choice(value.qname)
value = value.value
func = self.write_xsi_type if is_dataclass(value) else self.write_element
elif isinstance(value, AnyElement) and value.qname:
choice = var.find_choice(value.qname)
func = self.write_any_type
else:
tp = type(value)
choice = var.find_choice_typed(tp)
func = self.write_value

if not choice:
raise SerializerError(
f"XmlElements undefined choice: `{var.name}` for `{type(value)}`"
)

yield from func(value, choice, namespace)

@classmethod
def write_element(cls, value: Any, var: XmlVar, namespace: NoneStr) -> Generator:
Expand All @@ -241,6 +266,11 @@ def write_element(cls, value: Any, var: XmlVar, namespace: NoneStr) -> Generator
yield XmlWriterEvent.DATA, value
yield XmlWriterEvent.END, var.qname

@classmethod
def write_data(cls, value: Any, var: XmlVar, namespace: NoneStr) -> Generator:
"""Produce a data event for the given value."""
yield XmlWriterEvent.DATA, value

@classmethod
def next_value(cls, obj: Any, meta: XmlMeta):
"""
Expand Down

0 comments on commit 091330f

Please sign in to comment.