Source code for pdpy_lib.encoding.xmlbuilder

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# **************************************************************************** #
# This file is part of the pdpy project: https://github.com/pdpy-org
# Copyright (C) 2022 Fede Camara Halac
# **************************************************************************** #
""" XML Converter Class """

from xml.etree.ElementTree import ElementTree
from xml.etree.ElementTree import Element
from xml.etree.ElementTree import parse as __xparse__
from .xmltagconvert import XmlTagConvert
from ..utilities.utils import log

__all__ = [ 'XmlBuilder' ]

[docs]class XmlBuilder(XmlTagConvert): """ XML Converter Class """ def __init__(self): super().__init__() def __tree__(self, root, autoindent=True): tree = ElementTree(root) if autoindent: from xml.etree.ElementTree import indent as __indent__ __indent__(tree, space=' ', level=0) # return tostring(self.__root__, encoding='unicode', method='xml') return tree def __element__(self, scope=None, tag=None, text=None, attrib=None): """ Returns an XML element for this object """ __pdpy__ = None __tag__ = None # protect against empty string on the tag for 'empty' objects if tag == '': tag = None if tag is not None and isinstance(tag, str): # we have a tag, so the element will have a # different name than the PdPy class name # we will need to put the PdPy class name # in the '__pdpy__' attribute # convert the tag to xml-friendly format __tag__ = super().to_xml_tag(tag) if scope is not None and hasattr(scope, '__pdpy__'): # we have a scope, so we know the PdPy class name # we can create the element with the PdPy class name # and omit the '__pdpy__' attribute # get the pdpy name for the scope __pdpy__ = getattr(scope, '__pdpy__') if __tag__ is not None and __pdpy__ is not None: # we have both a tag and a scope # store the class name in the attributes # update the attrib dict if it's there if attrib is not None: attrib.update({'pdpy':__pdpy__}) else: attrib = {'pdpy':__pdpy__} elif __tag__ is not None and __pdpy__ is None: # we have a tag, but no scope # we don't update the attrib dict pass elif __pdpy__ is not None and __tag__ is None: # we have a scope, but no tag # we don't update the attrib dict # but we need to set the tag to the PdPy class name __tag__ = str(__pdpy__).lower() else: # we have neither a tag nor a scope raise AttributeError("Either tag or scope must be present") # log(1, f"""element # tag: {__tag__} # text: {text} # attrib: {attrib} # """) # create the element if super().isvalid(__tag__): element = Element(__tag__) else: element = Element(str(__pdpy__).lower()) if attrib is not None: element.attrib.update(attrib) # store the text of the element as a string if text is not None: element.text = str(text) return element def __subelement__(self, parent, child, **kwargs): """ Create a sub element (child) of a parent element (parent) """ if not isinstance(child, Element): if isinstance(child, str): child = self.__element__(tag=child, **kwargs) else: child = self.__element__(scope=child, **kwargs) parent.append(child) def __update_element__(self, parent, scope, attrib): """ Updates an element's attributes Description ----------- This method is used to update an element with pdpy attributes. The element is updated with the attributes of the scope. Parameters ----------- parent : `Element` The element to update scope : `object` The PdPy object handle (aka, `self`) to update the element with attrib : `list` or `tuple` of `str` The list of attributes to update the element with """ # print(parent, scope, attrib) def _parseattrib(a): for e in a: if hasattr(scope, e): attr = getattr(scope, e) if hasattr(attr, '__xml__'): self.__subelement__(parent, getattr(scope,e).__xml__(e)) else: if type(attr) in (list, tuple): for a in attr: if hasattr(a, '__xml__'): self.__subelement__(parent, a.__xml__(e)) else: self.__subelement__(parent, e, text=a) else: self.__subelement__(parent, e, text=attr) if attrib is not None: # log(0, 'Updating attrib:', attrib) if type(attrib) in (list, tuple): _parseattrib(attrib) elif isinstance(attrib, str): _parseattrib([attrib]) else: # print("----- DICT ATTIRB: ",attrib) # for k,v in attrib.items(): # parent.attrib.update({k:v}) pass else: # log(1, "MISSING ATTRIBUTES:",parent, scope, attrib) pass def __update_attrib__(self, element, key, value): element.attrib.update({key: str(value)}) def __xml__(self, scope=None, tag=None, attrib=None): # print("base",scope, tag, attrib) x = self.__element__(scope=scope, tag=tag) self.__update_element__(x, scope, attrib) return x def __tag_strip__(self, tag): strip_ns_tag = tag split_array = tag.split('}') if len(split_array) > 1: strip_ns_tag = split_array[1] tag = strip_ns_tag return tag def __elem_to_obj__(self, elem): """Convert an Element into an object """ elem_tag = self.__tag_strip__(elem.tag) pdpy_tag = elem.attrib['pdpy'] if 'pdpy' in elem.attrib else None if elem_tag == 'scalar' and pdpy_tag != 'Array': return self.__n__.__get__(name='Scalar')(xml=elem) if elem_tag == 'data': return {'data':[self.__n__.__get__(name='Data')(xml=c) for c in elem]} if elem_tag == 'comments': return [self.__n__.__get__(name='Comment')(xml=c) for c in elem] if elem_tag == 'comment': return self.__n__.__get__(name='Comment')(xml=elem) # print('>'*80) # log(1, f"Starting __elem_to_obj__ for {elem_tag}") # the class cls = self.__n__.__get__(name=getattr(elem.attrib, 'pdpy', None), tag=elem_tag) # print(cls) is_list = False # Handle list-type tags, we need to create lists for these: if elem_tag in ('nodes', # any node 'edges', # connections 'args', # objects 'targets', # messages 'messages', # messages # 'data', # data # 'symbols', # scalars # 'texts', # scalars # 'arrays', # scalars ): is_list = True d = [] # a list of PdData objects else: d = {} # our usual dictionary # handle header and data type loading h = elem.attrib['header'] if 'header' in elem.attrib else None h_type = str if h and h in ('set','saved') else self.__num__ # d will be a PdData type if header was found if h is not None: d.update({'__pdpy__':'PdData'}) d.update({'data':[]}) d.update({'header':h}) def _prnt(*argv): print(" ".join(argv)) log(1, "elem_to_obj\nELEM: "+elem+"\nLENGTH: "+str(len(list(elem)))+"\nTAG: "+str(elem_tag)+ "\nHEAD: "+ str(h)+ "\nHEAD TYPE: "+ str(h_type)+ "\nCLS: "+ str(cls)) # _prnt('') # loop over subelements to merge them ---------------------------------- for subelem in elem: subelem_tag = self.__tag_strip__(subelem.tag) # recurse into 'v' # print('->'*40) # log(1, f"-- BEGIN recursion __elem_to_obj__ for {elem_tag}") # log(1, f"""subelement loop # SUBELEMENT: {subelem} # TAG: {subelem_tag} # """) v = self.__elem_to_obj__(subelem) # log(1, f"-- END recursion __elem_to_obj__ for {elem_tag}") # print('<-'*40) if 'pdpy' in subelem.attrib: # sub_cls = self.__n__.__get__(name=getattr(subelem.attrib, 'pdpy', None), subelem_tag=subelem_tag) # print("Found subclass",sub_cls) if is_list: d.append(v) else: d.update({subelem_tag: v}) else: # print(d, subelem_tag, v) if not isinstance(d, dict) and is_list: # _prnt("Found list", subelem_tag, '-->', v) # drop the keys of the <msg> tag and keep values only if isinstance(v, dict): d += v.values() else: # these are proper xml Element objects, not dicts d.append(v) else: if type(v) not in (dict, list, str, int, float, bool): # log(1, 'not a dict', subelem_tag, v) d.update({subelem_tag: v}) else: if 'text' in v: # a pd comment is a string # log(1, 'found comment', subelem_tag, v) d.update({'text':[t for t in [v['text']]]}) else: if h is None: if isinstance(v, list): d.update({subelem_tag:v}) # log(1, 'found anything', subelem_tag, v, d) # for e in v: # d.update({subelem_tag:e}) else: for kk,vv in v.items(): d.update({kk:vv}) else: # log(1, '--------> found header', subelem_tag, v) for _,vv in v.items(): d['data'].append(h_type(vv)) # end subelement loop ----------------------------------------------- text = elem.text if elem.text is not None else '' # tail = elem.tail # print('text:',repr(text)) if '\n' in text: # ignore trailing whitespace text = text.strip() # if text != ' ' and not all([t==' ' for t in text]): # text = text.strip() # if tail: # tail = tail.strip() # if tail: # d['#tail'] = tail # print("FOUND TAIL", tail) if d: # use #text element if other attributes exist if text: d["#text"] = text else: # text is the value if no attributes d = text or None if isinstance(cls, str) and not 'pdpy' in elem.attrib: if 'arg' == elem_tag: return d else: return {elem_tag: d} elif d is None: cls = self.__n__.__get__(name=elem_tag) if not isinstance(cls, str): # print("FOUND CLASS", cls) # log(1, f"Ending __elem_to_obj__ for {elem_tag}") # print('<'*80) return cls() else: _prnt("DICT IS", d) else: try: # log(1,'UPDATING:',d) if 'pdpy' in elem.attrib and isinstance(d, dict): cls = self.__n__.__get__(name=elem.attrib['pdpy']) c = cls(json=d) elif isinstance(d, list): c = d else: # _prnt(f"updating: {d}") cls = self.__n__.__get__(name=d.get('__pdpy__',None), tag=elem_tag) if isinstance(cls, type): c = cls(json=d) else: log(2,"Unknown JSON object:", d) # print('-'*80) # c.__dumps__() except Exception as e: log(2, e) log(2, cls, d) c = None finally: # log(1, f"Ending __elem_to_obj__ for {elem_tag}") # print('<'*80) return c def __xmlparse__(self, xml): """ Parse an XML file into an ElementTree object. """ # PdPyXMLParser(self, xml) return __xparse__(xml)