import json
from dataclasses import asdict
from dataclasses import dataclass
from dataclasses import field
from dataclasses import is_dataclass
from enum import Enum
from typing import Any
from typing import Callable
from typing import Dict
from typing import Optional
from typing import Set
from typing import Tuple
from typing import Type
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.dataclass.mixins import ClassVar
from xsdata.formats.dataclass.mixins import ModelInspect
from xsdata.formats.dataclass.models import AnyElement
from xsdata.formats.mixins import AbstractSerializer
from xsdata.models.enums import Namespace
[docs]def filter_none(x: Tuple):
return dict((k, v) for k, v in x if v is not None)
[docs]class DictFactory:
FILTER_NONE = filter_none
[docs]class JsonEncoder(json.JSONEncoder):
[docs] def default(self, obj):
if isinstance(obj, Enum):
return obj.value
return super(JsonEncoder, self).default(obj)
[docs]@dataclass
class DictSerializer(AbstractSerializer):
dict_factory: Callable = field(default=dict)
[docs] def render(self, obj: object) -> Dict:
"""Convert the given object tree to dictionary with primitive
values."""
return asdict(obj, dict_factory=self.dict_factory)
[docs]@dataclass
class JsonSerializer(AbstractSerializer):
"""
:ivar dict_factory: Callable to generate dictionary
:ivar encoder: Value encoder
:ivar indent: Pretty print indent
"""
dict_factory: Callable = field(default=dict)
encoder: Type[json.JSONEncoder] = field(default=JsonEncoder)
indent: Optional[int] = field(default=None)
[docs] def render(self, obj: object) -> str:
"""Convert the given object tree to json string."""
return json.dumps(
asdict(obj, dict_factory=self.dict_factory),
cls=self.encoder,
indent=self.indent,
)
[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: object) -> str:
"""Convert the given object tree to xml string."""
tree = self.render_tree(obj)
return tostring(
tree,
xml_declaration=self.xml_declaration,
encoding=self.encoding,
pretty_print=self.pretty_print,
).decode()
[docs] def render_tree(self, obj: object, namespace: Optional[str] = None) -> Element:
"""
Convert a dataclass instance to a nested Element structure.
If the instance class is generated from the xsdata cli the root
element's name will be auto assigned otherwise it will default
to the class name.
:raise TypeError: If the instance is not a dataclass
"""
meta = self.class_meta(obj.__class__, namespace)
namespaces = set()
if meta.namespace:
namespaces.add(meta.namespace)
root = self.render_node(obj, Element(meta.qname), namespaces)
nsmap = {f"ns{index}": ns for index, ns in enumerate(sorted(namespaces))}
cleanup_namespaces(root, top_nsmap=nsmap)
return root
[docs] def render_node(self, obj, parent, namespaces) -> Element:
"""Recursively traverse the given dataclass instance fields and build
the lxml Element structure."""
if not is_dataclass(obj):
parent.text = self.render_value(obj)
return parent
meta = self.class_meta(obj.__class__, QName(parent).namespace)
for var in meta.vars.values():
value = getattr(obj, var.name)
if value is not None:
if var.namespace:
namespaces.add(var.namespace)
if var.is_attribute:
self.set_attribute(parent, value, var)
elif var.is_any_attribute:
self.set_attributes(parent, value)
elif var.is_any_element:
self.set_any_children(parent, value, namespaces)
elif var.is_text:
self.set_text(parent, value)
else:
self.set_children(parent, value, var, namespaces)
return parent
[docs] def set_attribute(self, parent: Element, value: Any, var: ClassVar):
parent.set(var.qname, self.render_value(value))
[docs] def set_attributes(self, parent: Element, value: Any):
for qname, value in value.items():
parent.set(qname, self.render_value(value))
[docs] def set_text(self, parent: Element, value: Any):
parent.text = self.render_value(value)
[docs] def set_children(
self, parent: Element, value: Any, var: ClassVar, namespaces: Set[str]
):
value = value if isinstance(value, list) else [value]
for val in value:
sub_element = SubElement(parent, var.qname)
self.render_node(val, sub_element, namespaces)
self.set_nil_attribute(var, sub_element, namespaces)
[docs] @classmethod
def set_any_children(cls, parent: Element, value: Any, namespaces: Set[str]):
value = value if isinstance(value, list) else [value]
for val in value:
if isinstance(val, str):
if parent.text:
parent.tail = val
else:
parent.text = val
elif isinstance(val, AnyElement):
qname = QName(val.qname)
namespace = qname.namespace
if namespace:
namespaces.add(namespace)
sub_element = SubElement(parent, qname)
sub_element.text = val.text
sub_element.tail = val.tail
for child in val.children:
cls.set_any_children(sub_element, child, namespaces)
[docs] @staticmethod
def set_nil_attribute(var: ClassVar, element: Element, namespaces: Set[str]):
if var.is_nillable and element.text is None and len(element) == 0:
namespaces.add(Namespace.INSTANCE)
qname = QName(Namespace.INSTANCE, "nil")
element.set(qname, "true")
[docs] @staticmethod
def render_value(value) -> str:
if isinstance(value, bool):
return "true" if value else "false"
if isinstance(value, Enum):
return str(value.value)
return str(value)