Source code for Musica.Xml.Objectifier

####################################################################################################
#
# Musica - A Music Theory Package for Python
# Copyright (C) 2017 Fabrice Salvaire
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
####################################################################################################

# Fixme:
#   improve formater
#   code dumper, register class

####################################################################################################

import logging
import types
from xml.etree import ElementTree
from xml.etree.ElementTree import Element

####################################################################################################

_module_logger = logging.getLogger(__name__)

####################################################################################################

__all__ = [
    'XmlObjectifierFactory',
    'XmlObjectifierLeaf',
    'XmlObjectifierNode',
    ]

####################################################################################################

class SourceCode:

    ##############################################

    def __init__(self, indentation=4):

        self._buffer = ''
        self._indentation = indentation
        self._level = 0

    ##############################################

    def increment_level(self):
        self._level += 1

    def decrement_level(self):
        self._level -= 1

    @property
    def indentation(self):
        return ' '*(self._indentation * self._level)

    ##############################################

    def __iter__(self):
        return iter(self._buffer.split('\n'))

    ##############################################

    def __str__(self):
        return self._buffer

    ##############################################

    def __iadd__(self, codes):

        self._buffer += codes
        return self

    ##############################################

    def append_line(self, codes):

        self._buffer += self.indentation + codes + '\n'

    ##############################################

    def append_lines(self, codes):

        for line in codes.split('\n'):
            self.append_line(line)

    ##############################################

    def close_block(self, decrement=False):

        if decrement:
            self.decrement_level()
        self.append_line(')')

####################################################################################################

class XmlObjectifierMetaclass(type):

    __classes__ = {}

    _logger = _module_logger.getChild('XmlObjectifierMetaclass')

    ##############################################

    def __new__(meta_cls, class_name, base_classes, namespace):

        print(meta_cls, class_name, base_classes, namespace)

        cls = super().__new__(meta_cls, class_name, base_classes, namespace)
        meta_cls.register(cls)

        return cls

    ##############################################

    def __init__(cls, class_name, base_classes, namespace):

        print(cls, class_name, base_classes, namespace)

        type.__init__(cls, class_name, base_classes, namespace)

    ##############################################

    @classmethod
    def register(meta_cls, cls):

        class_name = cls.__name__
        XmlObjectifierMetaclass._logger.info('Register {} for {}'.format(cls, class_name))
        meta_cls.__classes__[class_name] = cls

    ##############################################

    @classmethod
    def get(meta_cls, name):

        return meta_cls.__classes__[name]

####################################################################################################

class XmlObjectifierAbc(metaclass=XmlObjectifierMetaclass):

    _logger = _module_logger.getChild('XmlObjectifierAbc')

    __register_schema__ = False

    ##############################################

    @staticmethod
    def populate_class(namespace):

        namespace['__register_schema__'] = True
        # namespace['__id__'] = 0
        # we must instantiate here so as to update the class and not the base class
        namespace['__attribute_names__'] = set()
        namespace['__attribute_map__'] = {}

    ##############################################

    @classmethod
    def get_id(cls):

        if not hasattr(cls, '__id__'):
            cls.__id__ = 0

        _id = cls.__id__
        cls.__id__ += 1
        # name = cls.__name__.lower()
        name = cls.pythonify_name(cls.__tag__) # Fixme: cache

        return '{}_{}'.format(name, _id)

    ##############################################

    @classmethod
    def register_attribute(cls, name):

        py_name = cls.pythonify_name(name)
        cls.__attribute_names__.add(py_name)
        cls.__attribute_map__[name] = py_name
        cls.__attribute_map__[py_name] = name

        return py_name

    ##############################################

    @classmethod
    def class_to_python(cls):

        class_name = cls.__name__
        base_class = cls.__mro__[1].__name__

        py_code = SourceCode()
        py_code.append_line('class {}({}):'.format(class_name, base_class))
        py_code.increment_level()

        py_code.append_line("__tag__ = '{}'".format(cls.__tag__))

        if cls.__attribute_names__:
            py_code.append_line('__attribute_names__ = (') # python name
            py_code.increment_level()
            for name in sorted(cls.__attribute_names__):
                py_code.append_line("'{}',".format(name))
            py_code.decrement_level()
            py_code.append_line(')')

        attribute_map = [(name, value)
                         for name, value in sorted(cls.__attribute_map__.items(), key=lambda x: x[0])
                         if name != value and name not in cls.__attribute_names__]
        if attribute_map:
            py_code.append_line('__attribute_map__ = {')
            py_code.increment_level()
            for name, value in attribute_map:
                py_code.append_line("'{}':'{}',".format(name, value))
            py_code.decrement_level()
            py_code.append_line('}')

        return py_code

    ##############################################

    @staticmethod
    def pythonify_name(name):

        return name.replace('-', '_')

    ##############################################

    @staticmethod
    def to_python_value(value):

        if isinstance(value, str):
            try:
                if '.' in value or 'e' in value.lower():
                    return float(value)
                else:
                    return int(value)
            except ValueError:
                return value
        else:
            return value

    ##############################################

    @staticmethod
    def value_to_python_code(value):

        py_code = str(value)
        if not isinstance(value, (int, float)):
            py_code = "'" + py_code + "'"
        return py_code

    ##############################################

    def __init__(self, **kwargs):

        self._id = self.get_id()

        for name, value in kwargs.items():
            self.set_attribute(name, value)

    ##############################################

    @property
    def class_name(self):
        return self.__class__.__name__

    @property
    def instance_id(self):
        return self._id

    ##############################################

    def __repr__(self):

        return '{} {}'.format(self.__class__.__name__, self.instance_id)

    ##############################################

    def set_attribute(self, name, value):

        if self.__register_schema__:
            py_name = self.register_attribute(name)
            value = self.to_python_value(value)
            self._logger.info("{} {} = {} {}".format(self.__class__.__name__, name, value, type(value)))
        else:
            py_name = name

        setattr(self, py_name, value)

    ##############################################

    def attributes(self):

        _attrib = {name:getattr(self, name, None)
                  for name in self.__attribute_names__
                  if hasattr(self, name)}
        return {name:value for name, value in _attrib.items() if value is not None}

    ##############################################

    def to_dom(self):

        attributes = {self.__attribute_map__[name]:str(value)
                      for name, value in self.attributes().items()}
        return Element(self.__tag__, attributes)

    ##############################################

    def to_xml(self):

        return ElementTree.tostring(self.to_dom(), encoding='utf-8')

    ##############################################

    def attributes_to_python(self):

        kwarg = self.attributes()
        kwarg_string = ', '.join('{}={}'.format(name, self.value_to_python_code(value))
                                 for name, value in sorted(kwarg.items(), key=lambda x: x[0]))
        return kwarg_string

    ##############################################

    def depth_level(self):
        return 0

    ##############################################

    # def to_python(self, anonymous=False):
    #
    #     py_code = '{}({})'.format(self.class_name, self.attributes_to_python())
    #     if anonymous:
    #         return py_code
    #     else:
    #         return '{} = {}'.format(self.instance_id, py_code)

####################################################################################################

[docs]class XmlObjectifierNode(XmlObjectifierAbc): _logger = _module_logger.getChild('XmlObjectifierNode') __is_root__ = False ##############################################
[docs] @staticmethod def populate_class(namespace): XmlObjectifierAbc.populate_class(namespace) # namespace['__is_root__'] = False namespace['__child_names__'] = set()
##############################################
[docs] @classmethod def register_child(cls, name): if name not in cls.__child_names__: cls.__child_names__.add(name) # cls._add_getter(name) # Fixme: getter name setattr(cls, name, property(lambda self: self._get_child_by_name(name)))
##############################################
[docs] @classmethod def is_container(cls): return cls.__child_names__ and not cls.__attribute_names__
############################################## # @classmethod # def _add_getter(cls, name): ##############################################
[docs] @classmethod def class_to_python(cls): py_code = super().class_to_python() if cls.__is_root__: py_code.append_line('__is_root__ = True') py_code.append_line('__child_names__ = (') py_code.increment_level() for name in sorted(cls.__child_names__): py_code.append_line("'{}',".format(name)) py_code.decrement_level() py_code.append_line(')') return py_code
############################################## def __init__(self, *args, **kwargs): super().__init__(**kwargs) self._childs = [] self._child_map = {name:[] for name in self.__child_names__} for child in args: self.append(child) for child in kwargs.get('childs', ()): self.append(child) ##############################################
[docs] def __iter__(self): return iter(self._childs)
##############################################
[docs] def _get_child_by_name(self, name): return self._child_map[name]
##############################################
[docs] def _append_child(self, child): self._childs.append(child) name = child.__class__.__name__ if name not in self._child_map: self._child_map[name] = [] self._child_map[name].append(child) if self.__register_schema__: self.register_child(name)
##############################################
[docs] def append(self, *childs): for child in childs: self._append_child(child)
##############################################
[docs] def childs_are_leaf(self): for cls_name in self._child_map.keys(): if issubclass(XmlObjectifierMetaclass.get(cls_name), XmlObjectifierNode): return False return True
##############################################
[docs] def depth_level(self): depth_level = 0 for child in self._childs: depth_level = max(depth_level, child.depth_level() +1) return depth_level
##############################################
[docs] def to_dom(self): element = super().to_dom() for child in self._childs: element.append(child.to_dom()) return element
##############################################
[docs] def to_python(self, anonymous=False): # py_code = super().to_python() py_code = SourceCode() if not anonymous: py_code += self.instance_id + ' = ' py_code += self.class_name + '(' attributes = self.attributes_to_python() if attributes: py_code += attributes if self.depth_level() < 4: # self.childs_are_leaf() if attributes: py_code += ',' py_code += '\n' py_code.increment_level() if attributes: # not self.is_container py_code.append_line('childs=(') py_code.increment_level() for child in self._childs: py_code.append_lines(child.to_python(True).rstrip() + ',') if attributes: py_code.close_block(decrement=True) py_code.close_block(decrement=True) else: py_code.close_block() template = '{}.append({})\n' for child in self._childs: if isinstance(child, XmlObjectifierLeaf): py_code += template.format(self.instance_id, child.to_python(True)) else: py_code += child.to_python() py_code += template.format(self.instance_id, child.instance_id) return str(py_code)
##############################################
[docs] def to_tree(self): return ElementTree.ElementTree(self.to_dom())
##############################################
[docs] def write_xml(self, path, doctype=None): dom = self.to_tree() with open(path, 'wb') as fh: fh.write('<?xml version="1.0" encoding="UTF-8"?>'.encode('utf-8')) if doctype is not None: fh.write(doctype.encode('utf-8')) dom.write(fh, encoding='utf-8', xml_declaration=False)
# dom.write(path, encoding='utf-8', xml_declaration=True) ####################################################################################################
[docs]class XmlObjectifierLeaf(XmlObjectifierAbc): _logger = _module_logger.getChild('XmlObjectifierLeaf') ##############################################
[docs] @staticmethod def populate_class(namespace): XmlObjectifierAbc.populate_class(namespace)
############################################## def __init__(self, text=None, **kwargs): super().__init__(**kwargs) self._text = text ############################################## @property def text(self): return self._text @text.setter def text(self, value): self._text = value ##############################################
[docs] def to_dom(self): element = super().to_dom() if self._text is not None: text = str(self._text) if text: element.text = text return element
##############################################
[docs] def to_python(self, anonymous=False): # py_code = super().to_python() args = [] if self._text is not None: text = str(self._text) if not isinstance(self._text, (int, float)): text = "'" + text + "'" # py_code += '{}.text = {}\n'.format(self.instance_id, text) args.append(text) attributes = self.attributes_to_python() if attributes: args.append(attributes) py_code = '{}({})'.format(self.class_name, ', '.join(args)) if anonymous: return py_code else: return '{} = {}\n'.format(self.instance_id, py_code)
####################################################################################################
[docs]class XmlObjectifierFactory: _logger = _module_logger.getChild('XmlObjectifierFactory') ############################################## def __init__(self, doctype=None, node_hints=()): self._doctype = doctype self._node_hints = tuple(node_hints) self._classes = {} self._root_cls = None ##############################################
[docs] def _get_classe(self, element): name = element.tag class_name = ''.join([part.title() for part in name.split('-')]) if class_name not in self._classes: is_node = len(element) > 0 cls = self._build_class(class_name, name, is_node) else: cls = self._classes[class_name] return cls
##############################################
[docs] def _build_class(self, class_name, name, is_node): # Fixme: # if element.text is not None: if is_node or class_name in self._node_hints: base_class = XmlObjectifierNode else: base_class = XmlObjectifierLeaf self._logger.info('Create class {} {}'.format(class_name, base_class)) cls = types.new_class( class_name, bases=(base_class,), kwds=None, exec_body=XmlObjectifierNode.populate_class ) cls.__tag__ = name self._classes[class_name] = cls if self._root_cls is None: self._root_cls = cls cls.__is_root__ = True return cls
##############################################
[docs] def _cast_to_node(self, py_element): # Fixme: hack ! # old element are not recasted !!! cls = py_element.__class__ class_name = cls.__name__ self._logger.warning('{} must be reacasted to Node'.format(class_name)) del self._classes[class_name] new_cls = self._build_class(class_name, cls.__tag__, True) new_cls.__attribute_names__ = cls.__attribute_names__ new_cls.__attribute_map__ = cls.__attribute_map__ return new_cls(** py_element.attributes())
##############################################
[docs] def __getitem__(self, element): return self._get_classe(element)
##############################################
[docs] def __iter__(self): return iter(self._classes.values())
##############################################
[docs] def parse(self, file_path): tree = ElementTree.parse(file_path) root = tree.getroot() return self._process_element(root)
##############################################
[docs] def _process_element(self, element): self._logger.info('{0.tag} {0.text}'.format(element)) element_cls = self[element] py_element = element_cls() for name, value in element.attrib.items(): py_element.set_attribute(name, value) if element.text is not None: py_element.text = XmlObjectifierAbc.to_python_value(element.text) for subelement in element: py_subelement = self._process_element(subelement) if not hasattr(py_element, 'append'): py_element = self._cast_to_node(py_element) py_element.append(py_subelement) return py_element