# -*- coding: utf-8 -*-
"""
Supplemental script which handles data manipulation.
Read an XML file and produce a layout list, which is then used
by the main script :mod:`cardassembler`.
"""
__all__ = ['Blueprint']
__version__ = '1.5.1'
__author__ = 'Martin Brajer'
import xml.etree.ElementTree as ET
def main():
path = ''
if not path:
return
blueprint = Blueprint((path + '\\Blueprint.xml').decode('utf-8'))
layout = blueprint.generate_layout('unique item example')
palette = blueprint.generate_palette('color')
pass
[docs]class Blueprint():
""" Blueprint information handling class.
Can read XML file, produce layout list and palette.
:param file_path: Folder containing XML blueprint
:type file_path: str or None
"""
#: Those tags are always stored in a :class:`list` & have extra treatment
#: in :meth:`_step_in`.
SPECIAL_TAGS = ['next', 'text']
def __init__(self, file_path):
# Dict tree representation of the given XML file.
self.data = self._load(file_path) if file_path is not None else None
[docs] def _load(self, file_path):
""" Load XML file blueprint into a dictionary tree.
:param file_path: Path to the XML file to load
:type file_path: str
:return: Tree structure of cards data
:rtype: dict
"""
root = ET.parse(file_path).getroot()
return self._ElementTree_to_dict(root)
[docs] def _ElementTree_to_dict(self, parent):
""" Translation from :mod:`xml.etree.ElementTree` to
:class:`dict` tree from the given node down.
Tags in :data:`SPECIAL_TAGS` are stored in a :class:`list`.
:param parent: A node of ElementTree
:type parent: :class:`ElementTree.Element`
:return: Dictionary representation of the given tree
:rtype: dict
"""
node = {}
for child in parent:
tag = child.tag
text = child.text
# First condition: "item.text" is None <=> "<item></item>".
# Second condition: is there actual information? Symbols
# other than space and newline. If there is no text (just
# another child), tail is still there (e.g. " \n").
if (text is not None) and (text.strip().replace('\n', '')):
# ElementTree unescapes newline so we're reverting back.
text = text.replace('\\n', '\n')
if child.attrib and 'parse' in child.attrib:
text = self._parse(text, child.attrib['parse'])
if tag in node:
node[tag].append(text)
else:
node[tag] = [text] if tag in self.SPECIAL_TAGS else text
# No text, go down the level.
else:
node[tag] = self._ElementTree_to_dict(child)
return node
[docs] def _parse(self, text, target_type):
""" ElementTree.element.text to various python types.
Input parsed as tuple can have any length. Its elements will be
parsed as :class:`int`
:param text: Text to be parsed
:type text: str
:param target_type: Either "int", "float" or "tuple"
:type target_type: str
:raises ValueError: If target type is not known
:return: Parsed value
:rtype: int or float or tuple
"""
if target_type == 'int':
return int(text)
elif target_type == 'float':
return float(text)
elif target_type == 'tuple':
return tuple(self._parse(item, 'int') for item in
text.replace(' ', '').split(','))
raise ValueError('Unknown "{}" target type!'.format(target_type))
[docs] def generate_layout(self, start_by):
""" Generate card layout given starting position.
Starting position children are sorted alphabetically (name them
accordingly).
:param start_by: Space separated path through data tree leading
to the starting node
:type start_by: str
:return: Layout of the chosen card
:rtype: list
"""
layers = self._step_in({}, start_by)
return [(name, layers[name]) for name in sorted(layers.keys())]
[docs] def _step_in(self, layout, this_step):
""" Browse data guided by the ``next`` tag.
Do not overwrite (first in stays). Further ``next`` tags are served
successively in the order according to the XML file. If there are
multiple ``text`` tags, join them by ``\\n``
:param layout: Input layout
:type layout: dict
:param this_step: Where does this step leads
:type this_step: str
:return: Filled layout
:rtype: dict
"""
next_steps = []
for key, value in self._goto(this_step).items():
# Next is not written into layout. It stores further direction.
if key == 'next':
next_steps.extend(value)
# If lower levels can be reached.
elif isinstance(value, dict):
if key not in layout:
layout[key] = {}
layout[key] = self._step_in(
layout[key], ' '.join((this_step, key)))
# Keys having values from previous levels are NOT changed.
elif key not in layout:
if key == 'text':
value = '\n'.join(value)
layout[key] = value
# Recursively browse all "next" branches.
for next_step in next_steps:
layout = self._step_in(layout, next_step)
return layout
[docs] def _goto(self, next_steps):
""" Find target dict tree node and return its sub tree.
Analogous to successive application of :meth:`dict.get`.
:param next_steps: Space separated key sequence.
:type next_steps: str
:raises KeyError: If one of the given keys doesn't exist.
:return: Sub-tree of the :data:`self.data` dict tree.
:rtype: dict
"""
data = self.data
# Down the rabbit hole!
for next_step in next_steps.split(' '):
if next_step in data:
data = data[next_step]
else:
raise KeyError(
'While browsing the data tree by "{}", keyword "{}"'
'was not found.'.format(next_steps, next_step))
return data
[docs] def generate_palette(self, start_by):
""" Make palette out of colors used by cards.
To be used in another mini plug-in to import palette into Gimp.
:param start_by: Path through the data tree (space separated)
:type start_by: str
:return: Pairs of name and color
:rtype: list
"""
palette = self._harvest_leaves(self._goto(start_by))
palette.sort() # Alphabetically first.
palette.sort(key=lambda x: x[0].count(' '))
return palette
[docs] def _harvest_leaves(self, color_tree):
""" Find the path to the leaves of the given tree, whose tag
is ``color``.
Kinda inverse to :meth:`_goto`.
:param color_tree: Part of the data (dict tree) to look for
colors in
:type color_tree: dict
:return: List colors as :class:`tuple` of space delimited
path and color code
:rtype: list
"""
palette = []
for key, value in color_tree.items():
if isinstance(value, dict):
subpalette = self._harvest_leaves(value)
for subKey, subValue in subpalette:
palette.append(
(' '.join((key, subKey)) if subKey else key, subValue))
elif key == 'color':
palette.append(('', value))
return palette
if __name__ == '__main__':
main()