from dataclasses import dataclass
from dataclasses import field
from dataclasses import is_dataclass
from typing import Any
from typing import Optional
from lxml.etree import cleanup_namespaces
from lxml.etree import Element
from lxml.etree import QName
from lxml.etree import SubElement
from lxml.etree import tostring
from xsdata.formats.bindings import AbstractSerializer
from xsdata.formats.converters import to_xml
from xsdata.formats.dataclass.mixins import ModelInspect
from xsdata.formats.dataclass.models import AnyElement
from xsdata.formats.dataclass.models import AnyText
from xsdata.formats.dataclass.models import Namespaces
from xsdata.models.enums import Namespace
from xsdata.models.inspect import ClassMeta
from xsdata.models.inspect import ClassVar
XSI_NIL_QNAME = QName(Namespace.XSI.uri, "nil")
[docs]@dataclass
class XmlSerializer(AbstractSerializer, ModelInspect):
"""
:ivar xml_declaration: Add xml declaration
:ivar encoding: Result text encoding
:ivar pretty_print: Enable pretty output
"""
xml_declaration: bool = field(default=True)
encoding: str = field(default="UTF-8")
pretty_print: bool = field(default=False)
[docs] def render(self, obj: Any, namespaces: Optional[Namespaces] = None) -> str:
"""
Convert the given object tree to xml string.
Optionally provide a namespaces instance with a predefined list
of namespace uris and prefixes.
"""
tree = self.render_tree(obj, namespaces)
return tostring(
tree,
xml_declaration=self.xml_declaration,
encoding=self.encoding,
pretty_print=self.pretty_print,
).decode()
[docs] def render_tree(self, obj: Any, namespaces: Optional[Namespaces] = None) -> Element:
"""
Convert a dataclass instance to a nested Element structure.
Optionally provide a namespaces instance with a predefined list
of namespace uris and prefixes.
"""
meta = self.class_meta(obj.__class__)
namespaces = namespaces or Namespaces()
namespaces.register()
namespaces.add(meta.namespace)
root = self.render_node(obj, Element(meta.qname), namespaces)
cleanup_namespaces(
root, top_nsmap=namespaces.ns_map, keep_ns_prefixes=namespaces.prefixes
)
return root
[docs] def render_node(self, obj, parent, namespaces: Namespaces) -> Element:
"""Recursively traverse the given dataclass instance fields and build
the lxml Element structure."""
if not is_dataclass(obj):
self.set_text(parent, obj)
return parent
meta = self.class_meta(obj.__class__, QName(parent).namespace)
for var, value in self.next_value(meta, obj):
if value is not None:
if not var.is_any_element and not var.is_any_attribute:
namespaces.add(var.namespace)
if var.is_attribute:
self.set_attribute(parent, var.qname, value)
elif var.is_any_attribute:
self.set_attributes(parent, value)
elif var.is_text:
self.set_text(parent, value)
else:
self.render_sub_nodes(parent, value, var, namespaces)
self.set_nil_attribute(parent, meta.nillable, namespaces)
return parent
[docs] def render_sub_nodes(
self, parent, values: Any, var: ClassVar, namespaces: Namespaces
):
if not isinstance(values, list):
values = [values]
is_wildcard = var.is_any_element
for value in values:
if value is None:
continue
elif not is_wildcard:
sub_element = SubElement(parent, var.qname)
self.render_node(value, sub_element, namespaces)
self.set_nil_attribute(sub_element, var.nillable, namespaces)
elif isinstance(value, str):
if parent.text:
self.set_tail(parent, value)
else:
self.set_text(parent, value)
elif isinstance(value, AnyText):
namespaces.add_all(value.nsmap)
self.set_text(parent, value.text)
self.set_attributes(parent, value.attributes)
elif isinstance(value, AnyElement) and value.qname:
qname = QName(value.qname)
namespaces.add(qname.namespace)
sub_element = SubElement(parent, qname)
self.set_text(sub_element, value.text)
self.set_tail(sub_element, value.tail)
self.set_attributes(sub_element, value.attributes)
for child in value.children:
self.render_sub_nodes(sub_element, child, var, namespaces)
self.set_nil_attribute(parent, var.nillable, namespaces)
elif isinstance(value, AnyElement) and not value.qname:
for child in value.children:
self.render_sub_nodes(parent, child, var, namespaces)
else:
sub_element = SubElement(parent, value.qname)
self.render_node(value, sub_element, namespaces)
self.set_nil_attribute(sub_element, var.nillable, namespaces)
[docs] @classmethod
def set_attribute(cls, parent: Element, key: Any, value: Any):
if key != XSI_NIL_QNAME or (not parent.text and len(parent) == 0):
parent.set(to_xml(key), to_xml(value))
[docs] @classmethod
def set_attributes(cls, parent: Element, values: Any):
for key, value in values.items():
cls.set_attribute(parent, key, value)
[docs] @classmethod
def set_text(cls, parent: Element, value: Any):
value = to_xml(value)
if value is not None and len(value) == 0:
value = None
parent.text = value
[docs] @classmethod
def set_tail(cls, parent: Element, value: Any):
parent.tail = to_xml(value)
[docs] @classmethod
def set_nil_attribute(
cls, element: Element, nillable: bool, namespaces: Namespaces
):
if nillable and element.text is None and len(element) == 0:
namespaces.add(Namespace.XSI.uri, Namespace.XSI.prefix)
element.set(XSI_NIL_QNAME, "true")
[docs] @classmethod
def next_value(cls, meta: ClassMeta, obj: Any):
index = 0
attrs = list(meta.vars.values())
stop = len(attrs)
while index < stop:
var = attrs[index]
if not var.sequential:
yield var, getattr(obj, var.name)
index += 1
continue
end = next(
(i for i in range(index + 1, stop) if not attrs[i].sequential), stop
)
sequence = attrs[index:end]
index = end
j = 0
rolling = True
while rolling:
rolling = False
for var in sequence:
values = getattr(obj, var.name)
if j < len(values):
rolling = True
yield var, values[j]
j += 1