Source code for ptr.element

"""PTR Elements module."""

from copy import deepcopy
from datetime import datetime, timedelta
from functools import wraps
from pathlib import Path
from xml.dom import getDOMImplementation

from .datetime.parser import dt, td


XML = getDOMImplementation().createDocument(None, None, None)


class FrozenElementError(Exception):
    """Frozen element error."""


def frozen_lock(func):
    """Freezing decorator."""
    @wraps(func)
    def wrap(self, *args, **kwargs):
        """Wrapper function."""
        if self.is_frozen():
            raise FrozenElementError(f'<{self.tag}> cannot be changed.')
        return func(self, *args, **kwargs)
    return wrap


[docs]class Element: """Abstract PTR element object. Parameters ---------- tag: str Element tag name. *elements: any Text/numerical values or list of children elements. description: str or list, optional Element description, put as a xml-comment on top of the element. **attrs: str Element attributes. """ INDENT = ' ' LIST_SEP = ' ' def __init__(self, tag, *elements, description=None, **attrs): self.tag = tag self.description = description self.attrs = attrs for element in elements: self.append(element) def __str__(self): s = '' for desc in self.xml_desc: s += desc.toprettyxml(indent=self.INDENT) s += self.xml.toprettyxml(indent=self.INDENT) return s.strip() def __repr__(self): return str(self) def __eq__(self, other): return str(self) == str(other) def __add__(self, other): return self.append(other) def __len__(self): return len(self._els) def __iter__(self): return iter(self._els) def __getitem__(self, key): if isinstance(key, int): return self._els[key] elements = [] for el in self: if isinstance(el, Element): if el.tag == key: elements.append(el) elif key in el: children = el[key] if isinstance(children, list): elements.extend(children) else: elements.append(children) if not elements: raise KeyError(key) return elements[0] if len(elements) == 1 else elements def __setitem__(self, key, value): self[key].set_value(value) def __contains__(self, key): for el in self: if isinstance(el, Element) and (el.tag == key or key in el): return True return False def __delitem__(self, key): self.pop(key) @property def tag(self): """Element tag name.""" return self._tag @tag.setter def tag(self, tag): """Element tag name setter.""" self._tag = tag self._els = [] self._attrs = {} self._desc = [] @property def value(self): """Element parsed value.""" if len(self) != 1 or isinstance(self[0], Element): raise ValueError('Only single value can be retrieved with `.value`') return self._value_parser(self[0]) def _value_parser(self, value): """Value parser.""" if self.LIST_SEP in str(value): return [ self._value_parser(v.strip()) for v in str(value).split(self.LIST_SEP) if v.strip() ] if str(value).isdecimal(): return int(value) for func in [float, dt, td]: try: return func(value) except (ValueError, TypeError): pass return value
[docs] def set_value(self, value): """Set element value.""" if len(self) != 1 or isinstance(self[0], Element): raise ValueError('Only single value can be edited.') self._els[0] = value return self
[docs] def freeze(self, status=True): """Freeze the element.""" setattr(self, 'frozen', status)
[docs] def is_frozen(self): """Check if the element is frozen.""" return getattr(self, 'frozen', False)
[docs] @frozen_lock def append(self, element): """Append a new element or text/numeric value.""" if len(self) == 1 and not isinstance(self[0], Element): raise ValueError(f'<{self.tag}> already content the value ' f'`{self[0]}` impossible to add `{element}`.') if isinstance(element, dict): for tag, value in element.items(): self.append(Element(tag, value)) return self if self and not isinstance(element, Element): raise ValueError( f'<{self.tag}> already content {len(self)} `ptr.Element`, ' f'impossible to add `{element}`.') if element is not None: self._els.append(element) return self
@property def description(self): """Element description.""" return self._desc @description.setter def description(self, desc): """Element description setter.""" if not desc: self._desc = [] elif isinstance(desc, (list, tuple)): self._desc.extend(d for d in desc) else: self._desc.append(desc) @property def xml_desc(self): """Element XML formatted description.""" return ( XML.createComment(f' {desc.replace("-", "–").strip()} ') for desc in self.description ) @property def xml(self): """Element XML object.""" xml = XML.createElement(self.tag) # Add element attributes for key, value in self.attrs.items(): if value is not None: xml.setAttribute(key, str(value)) for element in self: if isinstance(element, Element): # Add child description if present for desc in element.xml_desc: xml.appendChild(desc) child = element.xml else: child = XML.createTextNode(f' {self._fmt_el(element)} ') xml.appendChild(child) return xml def _fmt_el(self, element) -> str: """Format element as XML text string.""" if not isinstance(element, str) and hasattr(element, '__iter__'): return self.LIST_SEP.join([ self._fmt_el(el) for el in element ]) if isinstance(element, datetime): if element.microsecond == 0: return element.isoformat() # Round value to milliseconds t = element + timedelta(microseconds=500) return t.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] # trunk last 3 digits return str(element).strip()
[docs] def index(self, tag) -> int: """Locate tag element index in the elements list. Parameters ---------- tag: str Tag element name to search in the Element. Returns ------- int Index of the tag element requested. Raises ------ ValueError If the tag element was not found in the Element. Note ---- If multiple tag elements are present only the index of the first tag element found is returned. Only the top level children are explored. """ for i, el in enumerate(self): if el.tag == tag: return i raise ValueError(f'`{tag}` is not present in {self}')
[docs] @frozen_lock def pop(self, key=-1): """Pop child element(s) by index or key. Parameters ---------- key: int or str Element index or Element tag name to remove. By default the last element is removed. Returns ------- Element or list(Element, ...) Popped element(s). Raises ------ IndexError If the provided index is out of range. KeyError If not element match the provided key. TypeError If the key provided is invalid. Note ---- When removed by tag name, if multiple child elements match the provided key, they will all be removed and returned. """ if isinstance(key, int): return self._els.pop(key) if isinstance(key, str): if key not in self: raise KeyError(f'`{key}` is not in `{self.__class__.__name__}`.') els, ipop = [], [] for i, el in enumerate(self._els): if isinstance(el, Element): if el.tag == key: els.append(el) # Store the element to remove ipop.append(i) # and its index elif key in el: p = el.pop(key) if isinstance(p, list): els.extend(p) else: els.append(p) for i in reversed(ipop): self._els.pop(i) # Remove the els elements here return els[0] if len(els) == 1 else els raise TypeError( f'Only `int` and `str` are accepted (`{type(key).__name__}` provided).')
[docs] @frozen_lock def insert(self, index, element): """Insert element before a given index.""" self.append(element) # Check if the element can be append el = self.pop() # Remove the last added element self._els.insert(index, el) # Insert the element at the right position
[docs] def save(self, filename, overwrite=False): """Save the element into a file. Parameters ---------- filename: str or pathlib.Path Output filename. overwrite: bool, optional Overwrite the file if already exists (default: False). Returns ------- pathlib.Path Output file location. """ fname = Path(filename) if fname.exists() and not overwrite: raise FileExistsError(filename) fname.write_text(str(self) + '\n', encoding='utf-8') return fname
[docs] def copy(self): """Make a deep copy of the element.""" return deepcopy(self)