odoo18/addons/account/tools/dict_to_xml.py

102 lines
3.9 KiB
Python

from lxml import etree
def dict_to_xml(node, *, nsmap={}, template=None, render_empty_nodes=False, tag=None, path=None):
""" Helper to render a Python dict as an XML node.
The dict is expected to be of the form:
{
# Special keys:
'_tag': 'tag_name', # '_tag' is rendered as the node's tag
'_text': 'content', # '_text' is rendered as the node's text content
'_dummy': 'dummy_value', # Keys starting with '_' are not rendered
# Simple values are rendered as attributes
'attribute_name': 'attribute_value',
# Dicts are rendered as child nodes
'child_tag': {
'_text': 'content',
'attribute_name': 'attribute_value',
},
# Lists of dicts are also rendered as child nodes
'child_tag': [
{
'_text': 'content',
'attribute_name': 'attribute_value',
},
],
}
:param node: The Python dict to render.
:param nsmap: (optional) A dict of namespaces to be used for rendering the node.
:param template: (optional) A Python dict providing default values and an order of keys for rendering the node.
:param render_empty_nodes: (optional) If True, empty nodes will be rendered in the XML tree.
:param tag: (optional) The tag of the node to render (needed only for recursive calls).
:param path: (optional) The path of the currently rendered node in the XML tree (needed only for recursive calls).
:return: The rendered XML node as an lxml.Element.
"""
def convert_tag_to_lxml_convention(tag):
if ':' in tag:
namespace, local_name = tag.split(':')
if namespace in nsmap:
return etree.QName(nsmap[namespace], local_name).text
return tag
if template is not None:
# Ensure order of keys
node = dict.fromkeys(template) | node
tag = node.get('_tag') or (template or {}).get('_tag', tag)
if tag is None:
raise ValueError(f"No tag was specified for node: {str(node)[:20]}")
if path is None:
path = tag
element = etree.Element(convert_tag_to_lxml_convention(tag), nsmap=nsmap)
# Add attributes
for attr_name, attr_value in node.items():
if not attr_name.startswith('_') and not isinstance(attr_value, (dict, list)) and attr_value is not None and attr_value is not False:
element.set(convert_tag_to_lxml_convention(attr_name), str(attr_value))
# Add text content if present
text = node.get('_text')
if text is not None and text is not False:
element.text = str(text)
# Add child nodes
for child_tag, child in node.items():
if not child_tag.startswith('_') and isinstance(child, (dict, list)):
child_template = (template or {}).get(child_tag)
child_is_empty = True
if isinstance(child, dict):
child = [child]
# child is a list (of dicts)
for sub_child in child:
if sub_child is not None:
child_element = dict_to_xml(
sub_child,
nsmap=nsmap,
template=child_template,
render_empty_nodes=render_empty_nodes,
tag=child_tag,
path=f'{path}/{child_tag}',
)
if child_element is not None:
element.append(child_element)
child_is_empty = False
# Check that all non-empty child nodes are defined in the template
if template is not None and child_tag not in template and not child_is_empty:
raise ValueError(f"The following child node is not defined in the template: {path}/{child_tag}")
if not render_empty_nodes and not element.attrib and not element.text and len(element) == 0:
return None
return element