import json
from dataclasses import asdict, dataclass, field
from enum import Enum
from typing import Callable, Dict, Optional, Tuple, Type
from lxml.etree import Element, QName, SubElement, cleanup_namespaces, tostring
from xsdata.formats.dataclass.mixins import ModelInspect
from xsdata.formats.mixins import AbstractSerializer
[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 EnumEncoder(json.JSONEncoder):
[docs] def default(self, obj):
if isinstance(obj, Enum):
return obj.value
return super(EnumEncoder, 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):
"""
:param dict_factory: Callable to generate dictionary
:param encoder: Value encoder
:param indent: Pretty print indent
"""
dict_factory: Callable = field(default=dict)
encoder: Type[json.JSONEncoder] = field(default=EnumEncoder)
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):
"""
:param xml_declaration: Add xml declaration
:param encoding: Result text encoding
:param pretty_print: Enable pretty output
"""
xml_declaration: bool = field(default=True)
encoding: str = field(default="UTF-8")
pretty_print: bool = field(default=False)
ns_list: list = field(init=False, default_factory=list)
[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) -> 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
"""
if not self.is_dataclass(obj):
raise TypeError(f"Object {obj} is not a dataclass.")
meta = self.class_meta(obj.__class__)
qname = self.render_tag(meta.name, meta.namespace)
root = self.render_node(obj, Element(qname))
cleanup_namespaces(
root,
top_nsmap={
None if index == 0 else f"ns{index}": namespace
for index, namespace in enumerate(self.ns_list)
},
)
return root
[docs] def render_node(self, obj, parent) -> Element:
"""Recursively traverse the given dataclass instance fields and build
the lxml Element structure."""
if not self.is_dataclass(obj):
parent.text = self.render_value(obj)
return parent
for f in self.fields(obj.__class__):
value = getattr(obj, f.name)
if not value:
continue
elif f.is_attribute:
parent.set(f.local_name, self.render_value(value))
else:
value = value if type(value) is list else [value]
if f.namespace:
qname = self.render_tag(f.local_name, f.namespace)
elif parent.prefix:
qname = self.render_tag(
f.local_name, parent.nsmap[parent.prefix]
)
else:
qname = f.local_name
for val in value:
sub_element = SubElement(parent, qname)
self.render_node(val, sub_element)
return parent
[docs] def render_tag(self, name, namespace=None) -> QName:
if namespace and namespace not in self.ns_list:
self.ns_list.append(namespace)
return QName(namespace, name)
[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)