import xml.etree.ElementTree as ET
from musicxml.util.core import convert_to_xml_class_name, cap_first
from musicxml.xmlelement.exceptions import XMLChildContainerFactoryError, XMLChildContainerWrongElementError, \
XMLChildContainerChoiceHasAnotherChosenChild, XMLChildContainerMaxOccursError
from musicxml.xsd.xsdelement import XSDElement
from musicxml.xsd.xsdindicator import *
from musicxml.xsd.xsdtree import XSDTree
from tree.tree import Tree
def _convert_xsd_child_to_xsd_container(xsd_child):
min_occurrences = xsd_child.get_attributes().get('minOccurs')
max_occurrences = xsd_child.get_attributes().get('maxOccurs')
copied_xsd_child = xsd_child.__deepcopy__()
if min_occurrences is not None:
copied_xsd_child.get_attributes().pop('minOccurs')
if max_occurrences is not None:
copied_xsd_child.get_attributes().pop('maxOccurs')
if xsd_child.tag == 'element':
return XMLChildContainer(content=XSDElement(copied_xsd_child), min_occurrences=min_occurrences,
max_occurrences=max_occurrences)
elif xsd_child.tag == 'sequence':
return XMLChildContainer(content=XSDSequence(copied_xsd_child), min_occurrences=min_occurrences,
max_occurrences=max_occurrences)
elif xsd_child.tag == 'choice':
return XMLChildContainer(content=XSDChoice(copied_xsd_child), min_occurrences=min_occurrences,
max_occurrences=max_occurrences)
elif xsd_child.tag == 'group':
xsd_group_name = 'XSDGroup' + ''.join(
[cap_first(partial) for partial in xsd_child.get_attributes()['ref'].split('-')])
return XMLChildContainer(content=eval(xsd_group_name)(), min_occurrences=min_occurrences,
max_occurrences=max_occurrences)
else:
raise NotImplementedError(xsd_child.tag)
def _check_if_container_requires_elements(xsd_container):
if isinstance(xsd_container.content, XSDSequence):
return _check_if_sequence_requires_elements(xsd_container)
elif isinstance(xsd_container.content, XSDGroup):
return _check_if_group_requires_elements(xsd_container)
elif isinstance(xsd_container.content, XSDChoice):
return _check_if_choice_requires_elements(xsd_container)
else:
raise NotImplementedError(xsd_container)
def _check_if_choice_requires_elements(xsd_container_choice):
element_chosen = False
for child in xsd_container_choice.get_children():
if isinstance(child.content, XSDGroup):
if child.get_children()[0].force_validate:
_check_if_container_requires_elements(child.get_children()[0])
elif child.force_validate:
_check_if_container_requires_elements(child)
else:
if child.min_occurrences == 0:
pass
elif int(child.min_occurrences) == 1:
if isinstance(child.content, XSDElement):
if len(child.content.xml_elements) == 0:
pass
elif len(child.content.xml_elements) == 1:
element_chosen = True
else:
raise NotImplementedError(child)
else:
_check_if_container_requires_elements(child)
else:
raise NotImplementedError(f'child {child} with min_occurrence greater than 1')
if element_chosen:
xsd_container_choice.requirements_fulfilled = True
def _check_if_group_requires_elements(xsd_group_container):
if xsd_group_container.min_occurrences == 0 and not xsd_group_container.get_children()[0].force_validate:
return
return _check_if_sequence_requires_elements(xsd_group_container.get_children()[0])
def _check_if_sequence_requires_elements(xsd_sequence_container):
if xsd_sequence_container.force_validate:
for child in xsd_sequence_container.get_children():
if isinstance(child.content, XSDElement):
if len(child.content.xml_elements) < child.min_occurrences:
child.requirements_fulfilled = False
else:
pass
else:
_check_if_container_requires_elements(child)
def validate_child(ch):
if isinstance(ch.content, XSDElement):
if child.choices_in_reversed_path:
pass
elif len(ch.content.xml_elements) < ch.min_occurrences:
ch.requirements_fulfilled = False
else:
ch.requirements_fulfilled = True
else:
_check_if_container_requires_elements(ch)
if xsd_sequence_container.min_occurrences > 0:
for child in xsd_sequence_container.get_children():
if child.force_validate is True:
_check_if_container_requires_elements(child)
elif child.min_occurrences == 0:
pass
elif child.min_occurrences == 1:
validate_child(child)
else:
raise NotImplementedError(f'child {child} with min_occurrence greater than 1')
class DuplicationXSDSequence(XSDSequence):
sequence_xsd = """
<xs:sequence xmlns:xs="http://www.w3.org/2001/XMLSchema">
</xs:sequence>
"""
def __init__(self):
xsd_tree_ = XSDTree(ET.fromstring(self.sequence_xsd))
super().__init__(xsd_tree_)
[docs]class XMLChildContainer(Tree):
def __init__(self, content, min_occurrences=None, max_occurrences=None, populate_children=True, *args, **kwargs):
super().__init__(*args, **kwargs)
self._content = None
self._chosen_child = None
self._required_element_names = None
self._requirements_fulfilled = None
self.min_occurrences = 1 if min_occurrences is None else int(min_occurrences)
self.max_occurrences = 1 if max_occurrences is None else 'unbounded' if max_occurrences == 'unbounded' else int(
max_occurrences)
self.content = content
self._force_validate = None
self._parent_xml_element = None
if populate_children:
self._populate_children()
# private methods
def _add_duplication_parent(self):
if not self.get_parent():
parent_container = XMLChildContainer(content=DuplicationXSDSequence())
parent_container.add_child(self)
elif not (isinstance(self.get_parent().content, DuplicationXSDSequence)):
parent_container = XMLChildContainer(content=DuplicationXSDSequence())
index = self.get_parent().get_children().index(self)
parent = self.get_parent()
parent.remove(self)
parent.get_children().insert(index, parent_container)
parent_container._parent = parent
parent_container.add_child(self)
if isinstance(parent.content, XSDChoice) and parent.chosen_child == self:
parent.chosen_child = parent_container
else:
pass
@staticmethod
def _check_content_type(val):
types = [XSDSequence, XSDChoice, XSDElement, XSDGroup]
for type_ in types:
if isinstance(val, type_):
return
raise TypeError(val)
def _check_child_to_be_added(self, child):
if not isinstance(child, XMLChildContainer):
raise TypeError
def _check_choices_intelligently(self, xml_element=None):
"""
Check if existing xml elements can be attached to other choice paths in order to fulfill all requirements. Only possible leaves
forwards will be checked. If xml_element is given it will be treated as a new element which is going to be attached to the
container.
"""
if self.get_parent_xml_element():
print(f'_check_choices_intelligently: intelligent choice for {self.get_parent_xml_element()}')
def get_same_name_next_leaves(leaf_):
output = []
index = -1
record = False
for l in self.iterate_leaves():
if l.content.name == leaf_.content.name:
index += 1
if not record:
record = True
else:
output.append((l, index))
return output
def get_sorted_xml_elements(sorted_options_):
unsorted_elements = [el for el in self.get_attached_elements() if el.name != sorted_options_[-1][0]]
output = []
for option in sorted_options_:
selected_elements = [el for el in unsorted_elements if el.name == option[1]]
for el in selected_elements:
unsorted_elements.remove(el)
output.append(el)
output.extend(unsorted_elements)
return output
current_leaves_with_xml_elements = [leaf for leaf in self.iterate_leaves() if leaf.content.xml_elements]
optional_next_leaves = [get_same_name_next_leaves(leaf) for leaf in current_leaves_with_xml_elements if
get_same_name_next_leaves(leaf)]
if optional_next_leaves:
indices = {list_of_leaves[0][0].content.name: [l[1] for l in list_of_leaves] for list_of_leaves in
optional_next_leaves}
sorted_options = sorted([(k, v) for k, v in indices.items()], key=lambda v: -len(v))
sorted_xml_elements = get_sorted_xml_elements(sorted_options)
efficient_xml_element_name, forward_indices = sorted_options.pop()
efficient_xml_elements = [el for el in self.get_attached_elements() if
el.name == efficient_xml_element_name]
for element in efficient_xml_elements:
for forward_index in forward_indices:
copied_container = self._create_empty_copy()
copied_container.add_element(element, forward_index)
try:
for el in sorted_xml_elements:
copied_container.add_element(el, intelligent_choice=False)
if xml_element:
copied_container.add_element(xml_element, intelligent_choice=False)
return copied_container
if not copied_container.check_required_elements():
return copied_container
except XMLChildContainerChoiceHasAnotherChosenChild:
pass
return None
def _create_empty_copy(self):
"""
Creates a copy without attached elements or duplicated nodes
:return: XMLChildContainer
"""
if isinstance(self.content, XSDChoice) or isinstance(self.content, XSDSequence):
copied_content = self.content.__class__(self.content.xsd_tree)
else:
copied_content = eval(self.content.__class__.__name__)()
return XMLChildContainer(copied_content, self.min_occurrences, self.max_occurrences)
def _duplicate_parent_in_path(self):
for node in list(self.reversed_path_to_root())[:-1]:
if node.get_parent().max_occurrences == 'unbounded':
return node.get_parent().duplicate()
return None
def _update_requirements_in_path(self):
if not isinstance(self.content, XSDElement):
raise ValueError
if self.max_is_reached:
self.requirements_fulfilled = True
if self.content.xml_elements:
for node in self.reversed_path_to_root():
if node.get_parent():
if isinstance(node.get_parent().content, XSDChoice):
if node.get_parent().chosen_child:
if node.get_parent().chosen_child != node:
raise XMLChildContainerChoiceHasAnotherChosenChild
else:
break
else:
node.get_parent().chosen_child = node
if node.get_parent().requirements_fulfilled is False:
node.get_parent().requirements_fulfilled = True
break
node.get_parent().requirements_fulfilled = True
elif isinstance(node.get_parent().content, XSDSequence):
if node.get_parent().force_validate:
break
else:
node.get_parent().set_force_validate(node, True)
def _populate_children(self):
for xsd_child in [child for child in self.content.xsd_tree.get_children() if
child.tag != 'annotation' and child.tag != 'complexType']:
container = _convert_xsd_child_to_xsd_container(xsd_child)
self.add_child(container)
def _set_requirements_fulfilled(self):
for node in self.traverse():
if isinstance(node.content, XSDChoice) and node.requirements_fulfilled is None and False not in [
choice.requirements_fulfilled for choice in
node.choices_in_reversed_path] and node.min_occurrences != 0:
for leaf in node.iterate_leaves():
if leaf.content.xml_elements:
node._requirements_fulfilled = True
if node._requirements_fulfilled is None:
for child in node.get_children():
if child.min_occurrences != 0:
node._requirements_fulfilled = False
break
if node._requirements_fulfilled is None:
node._requirements_fulfilled = True
else:
node._requirements_fulfilled = True
# public properties
@property
def compact_repr(self):
"""
:return: A compact representation of :obj:`~content`.
"""
if isinstance(self.content, XSDSequence):
type_ = 'Sequence'
return f"{type_}@minOccurs={self.min_occurrences}@maxOccurs={self.max_occurrences}"
if isinstance(self.content, XSDChoice):
type_ = 'Choice'
output = f"{type_}@minOccurs={self.min_occurrences}@maxOccurs={self.max_occurrences}"
if self.requirements_fulfilled is False:
output += '\n'
output += self.get_indentation() + ' '
output += '!Required!'
return output
if isinstance(self.content, XSDGroup):
type_ = 'Group'
return f"{type_}@name={self.content.name}@minOccurs={self.min_occurrences}@maxOccurs={self.max_occurrences}"
if isinstance(self.content, XSDElement):
type_ = 'Element'
output = f"{type_}@name={self.content.name}@minOccurs={self.min_occurrences}@maxOccurs={self.max_occurrences}"
for xml_element in self.content.xml_elements:
output += '\n'
output += self.get_indentation() + ' '
output += xml_element.__class__.__name__
if self.requirements_fulfilled is False:
output += '\n'
output += self.get_indentation() + ' '
output += '!Required!'
return output
@property
def content(self):
"""
:return: Content of a :obj:`~musicxml.xmlelement.xmlchildcontainer.XMLChildContainer` is its core property. It can be of types: :obj:`~musicxml.xsd.xsdindicator.XSDSequence`, :obj:`~musicxml.xsd.xsdindicator.XSDChoice`, :obj:`~musicxml.xsd.xsdindicator.XSDGroup` or :obj:`~musicxml.xsd.xsdelement.XSDElement` which are used to translate the behaviour of according xsd tags: ``sequence``, ``choice``, ``group`` and ``element``.
"""
return self._content
@content.setter
def content(self, val):
self._check_content_type(val)
self._content = val
self._content.parent_container = self
@property
def choices_in_reversed_path(self):
return [node for node in list(self.reversed_path_to_root())[1:] if isinstance(node.content, XSDChoice)]
@property
def chosen_child(self):
return self._chosen_child
@chosen_child.setter
def chosen_child(self, val):
if not isinstance(self.content, XSDChoice):
raise TypeError
self._chosen_child = val
@property
def force_validate(self):
return self._force_validate
@property
def max_is_reached(self):
if not isinstance(self.content, XSDElement):
raise TypeError
if self.max_occurrences == 'unbounded':
return False
else:
if len(self.content.xml_elements) == self.max_occurrences:
return True
elif len(self.content.xml_elements) > self.max_occurrences:
raise ValueError
else:
return False
@property
def requirements_fulfilled(self):
return self._requirements_fulfilled
@requirements_fulfilled.setter
def requirements_fulfilled(self, val: bool):
if not isinstance(val, bool):
raise TypeError
self._requirements_fulfilled = val
# public methods
[docs] def add_element(self, xml_element, forward=None, intelligent_choice=True):
# resets frozen tree iteration lists like traverse() iterate_leaves() etc.
self._reset_iterators()
if self._requirements_fulfilled is None:
self.check_required_elements()
def select_valid_leaves(leaves):
output = []
choice_with_chosen_child = None
for index, leaf in enumerate(leaves):
for n in leaf.reversed_path_to_root():
if n.get_parent() and isinstance(n.get_parent().content, XSDChoice) and n.get_parent().chosen_child:
choice_with_chosen_child = n.get_parent()
if n == choice_with_chosen_child.chosen_child:
output.append(leaf)
break
if not choice_with_chosen_child:
return leaves
elif not output:
if choice_with_chosen_child.max_occurrences == 1:
if choice_with_chosen_child.up and isinstance(choice_with_chosen_child.up.content, XSDSequence) and \
choice_with_chosen_child.up.max_occurrences == 'unbounded':
return []
else:
return None
else:
return []
else:
return output
if 'XMLElement' not in [cls.__name__ for cls in xml_element.__class__.__mro__]:
raise TypeError(xml_element.__class__)
same_name_leaves = [leaf for leaf in self.iterate_leaves() if leaf.content.name == xml_element.name]
if not same_name_leaves:
raise XMLChildContainerWrongElementError()
selected_same_name_leaves = select_valid_leaves(same_name_leaves)
if selected_same_name_leaves is None:
intelligently_selected = None
if forward is None and intelligent_choice is True:
copy_with_intelligence = self._check_choices_intelligently(xml_element)
if copy_with_intelligence:
for old_child, new_child in zip(self.get_children(), copy_with_intelligence.get_children()):
self.replace_child(old_child, new_child)
return \
[l for l in copy_with_intelligence.iterate_leaves() if xml_element in l.content.xml_elements][0]
if not intelligently_selected:
msg = f"{self} By adding {xml_element.__class__.__name__} to {self.get_parent_xml_element().__class__.__name__}" if \
self.get_parent_xml_element() else f"{self} By adding {xml_element.__class__.__name__}"
raise XMLChildContainerChoiceHasAnotherChosenChild(msg)
else:
self.check_required_elements()
return intelligently_selected
if selected_same_name_leaves == []:
duplicated_parent = same_name_leaves[-1]._duplicate_parent_in_path()
if duplicated_parent:
selected_same_name_leaves = [leaf for leaf in duplicated_parent.iterate_leaves() if
leaf.content.name == xml_element.name and not
leaf.max_is_reached]
if self._parent_xml_element and self.up:
self._parent_xml_element._child_container_tree = self.up
else:
raise XMLChildContainerChoiceHasAnotherChosenChild
if forward is not None:
selected = same_name_leaves[forward]
if selected not in selected_same_name_leaves:
raise XMLChildContainerChoiceHasAnotherChosenChild('Wrong forwarding')
else:
selected_same_name_leaves_max_not_reached = [leaf for leaf in selected_same_name_leaves if
not leaf.max_is_reached]
if not selected_same_name_leaves_max_not_reached:
duplicated_parent = selected_same_name_leaves[-1]._duplicate_parent_in_path()
if duplicated_parent:
selected_same_name_leaves_max_not_reached = [leaf for leaf in duplicated_parent.iterate_leaves() if
leaf.content.name ==
xml_element.name and not leaf.max_is_reached]
if self._parent_xml_element and self.up:
self._parent_xml_element._child_container_tree = self.up
else:
raise XMLChildContainerMaxOccursError()
selected = selected_same_name_leaves_max_not_reached[0]
selected.content.add_xml_element(xml_element)
selected._update_requirements_in_path()
return selected
[docs] def check_required_elements(self, intelligent_choice=False):
if self._requirements_fulfilled is None:
self._set_requirements_fulfilled()
_check_if_container_requires_elements(self)
requirements_exist = False
for node in self.traverse():
if node.requirements_fulfilled is False:
requirements_exist = True
if requirements_exist and intelligent_choice:
if isinstance(self.content, XSDChoice):
return requirements_exist
copy_with_intelligence = self._check_choices_intelligently()
if copy_with_intelligence:
for old_child, new_child in zip(self.get_children(), copy_with_intelligence.get_children()):
self.replace_child(old_child, new_child)
return False
return requirements_exist
check_requirements = check_required_elements
[docs] def duplicate(self):
if not isinstance(self.content, XSDSequence) and not isinstance(self.content, XSDChoice) and not isinstance(
self.content, XSDGroup):
raise TypeError(self.content)
if self.max_occurrences != 'unbounded':
raise ValueError
self._add_duplication_parent()
copied_self = self._create_empty_copy()
copied_self._parent = self.get_parent()
self.get_parent().add_child(copied_self)
return copied_self
[docs] def get_attached_elements(self):
output = []
for leaf in self.iterate_leaves():
output.extend(leaf.content.xml_elements)
return output
[docs] def get_leaves(self, function=None):
if isinstance(self.content, XSDElement):
if function:
return function(self)
else:
return self.content
elif isinstance(self.content, XSDGroup):
return self.get_children()[0].get_leaves(function=function)
elif isinstance(self.content, XSDSequence) or isinstance(self.content, XSDChoice):
output = [node.get_leaves(function=function) for node in self.get_children() if
node.get_leaves(function=function)]
try:
if not output or set(output) == {None}:
return None
except TypeError:
pass
if len(output) == 1:
return output[0]
return output if isinstance(self.content, XSDSequence) else tuple(output)
else:
raise NotImplementedError
[docs] def get_parent_xml_element(self):
"""
:return: :obj:`~musicxml.xmlelement.xmlelement.XMLElement` which child container is attached to.
"""
return self._parent_xml_element
[docs] def get_required_element_names(self, intelligent_choice=False):
def func(leaf):
if leaf.requirements_fulfilled is False:
return convert_to_xml_class_name(leaf.content.name)
elif leaf.min_occurrences != 0 and False in [choice.requirements_fulfilled for choice in
leaf.choices_in_reversed_path]:
if isinstance(leaf.get_parent().content,
XSDSequence) and leaf.get_parent().min_occurrences == 0 and not leaf.get_parent(
).force_validate:
pass
else:
return convert_to_xml_class_name(leaf.content.name)
self.check_required_elements(intelligent_choice)
return self.get_leaves(func)
[docs] def set_force_validate(self, node, val):
self._force_validate = val
for child in [ch for ch in self.get_children() if ch != node]:
for n in child.traverse():
if isinstance(n.content, XSDChoice):
if n.min_occurrences != 0 and not n.chosen_child and [ch for ch in n.get_children() if
ch.min_occurrences != 0]:
n.requirements_fulfilled = False
break
if isinstance(n.content, XSDSequence) and n.min_occurrences == 0:
break
if isinstance(n.content, XSDSequence) and n.min_occurrences != 0 and not (
isinstance(n.get_parent().content, XSDGroup) and
n.get_parent().min_occurrences == 0):
n._force_validate = val
def __repr__(self):
return f"XMLChildContainer:{self.compact_repr} {self.get_coordinates_in_tree()}"
def __copy__(self):
copied = self.__class__(content=self.content.__copy__(), min_occurrences=self.min_occurrences,
max_occurrences=self.max_occurrences,
populate_children=False)
for child in self.get_children():
copied.add_child(child.__copy__())
return copied
class XMLChildContainerFactory:
def __init__(self, complex_type):
self._child_container = None
self._create_child_container(complex_type)
def _create_child_container(self, complex_type):
if 'XSDComplexType' not in [cls.__name__ for cls in complex_type.__mro__]:
raise TypeError
if not complex_type.get_xsd_indicator():
raise XMLChildContainerFactoryError(f'complex_type {complex_type} has no xsd_indicator.')
child_container = XMLChildContainer(*complex_type.get_xsd_indicator())
self._child_container = child_container
def get_child_container(self):
return self._child_container