Hrabal/TemPy

View on GitHub
tempy/modifier.py

Summary

Maintainability
C
1 day
Test Coverage
# -*- coding: utf-8 -*-
# @author: Federico Cerchiari <federicocerchiari@gmail.com>
"""Classes for DOM building"""
try:
    from collections import Iterable
except ImportError:
    from collections.abc import Iterable
from copy import copy

from .bases import TempyClass
from .tools import content_receiver
from .exceptions import TagError, WrongArgsError, DOMModByKeyError, DOMModByIndexError


class BaseDOMModifier(TempyClass):
    def _insert(self, dom_group, idx=None, prepend=False, name=None):
        """Inserts a DOMGroup inside this element.
        If provided at the given index, if prepend at the start of the childs list, by default at the end.
        If the child is a DOMElement, correctly links the child.
        If the DOMGroup have a name, an attribute containing the child is created in this instance.
        """
        if dom_group is None:
            return
        if idx and idx < 0:
            idx = 0
        if prepend:
            idx = 0
        if idx is None:
            idx = -1
        if not isinstance(dom_group, Iterable) or isinstance(dom_group, (TempyClass, str)):
            dom_group = [dom_group]
        for i_group, elem in enumerate(dom_group):
            if elem is not None:
                # Element insertion in this DOMElement childs
                if idx == -1:
                    self.childs.append(elem)
                else:
                    self.childs.insert(idx + i_group, elem)
                # Managing child attributes if needed
                if hasattr(elem, "parent"):
                    elem.parent = self
                if name:
                    setattr(self, name, elem)

    @content_receiver()
    def __call__(self, _, child, name=None):
        """Calling the object will add the given parameters as childs"""
        self._insert(child, name=name)

    def clone(self):
        """Returns a deepcopy of this element."""
        return copy(self)


class SiblingsManager(BaseDOMModifier):
    @content_receiver()
    def after(self, i, sibling, name=None):
        """Adds siblings after the current tag."""
        self.parent._insert(sibling, idx=self._own_index + 1 + i, name=name)
        return self

    @content_receiver(reverse=True)
    def before(self, i, sibling, name=None):
        """Adds siblings before the current tag."""
        self.parent._insert(sibling, idx=self._own_index - i, name=name)
        return self


class DOMNihil(SiblingsManager):
    def replace_with(self, other):
        """Replace this element with the given DOMElement."""
        self.after(other)
        self.parent.pop(self._own_index)
        return other

    def remove(self):
        """Detach this element from his father."""
        if self._own_index is not None and self.parent:
            self.parent.pop(self._own_index)
        return self

    def _detach_childs(self, idx_from=None, idx_to=None):
        """Moves all the childs to a new father"""
        idx_from = idx_from or 0
        idx_to = idx_to or len(self.childs)
        removed = self.childs[idx_from:idx_to]
        for child in removed:
            if issubclass(child.__class__, TempyClass):
                child.parent = None
        self.childs[idx_from:idx_to] = []
        return removed

    def move_childs(self, new_father, idx_from=None, idx_to=None):
        removed = self._detach_childs(idx_from=idx_from, idx_to=idx_to)
        new_father(removed)
        return self

    def move(self, new_father, idx=None, prepend=None, name=None):
        """Moves this element from his father to the given one."""
        self.parent.pop(self._own_index)
        new_father._insert(self, idx=idx, prepend=prepend, name=name)
        return self

    def pop(self, arg=None):
        """Removes the child at given position or by name (or name iterator).
            if no argument is given removes the last."""
        if arg is None:
            arg = len(self.childs) - 1
        if isinstance(arg, int):
            try:
                result = self.childs.pop(arg)
            except IndexError:
                raise DOMModByIndexError(self, "Given index invalid.")
            if isinstance(result, TempyClass):
                result.parent = None
        else:
            result = []
            if isinstance(arg, str):
                arg = [arg]
            for x in arg:
                try:
                    result.append(getattr(self, x))
                except AttributeError:
                    raise DOMModByKeyError(self, "Given search key invalid. No child found.")
            for x in result:
                self.childs.remove(x)
                if isinstance(x, TempyClass):
                    x.parent = False
        return result

    def empty(self):
        """Remove all this tag's childs."""
        self._detach_childs()
        return self


class OperatorsModifier(DOMNihil):
    def __add__(self, other):
        """Addition produces a copy of the left operator, containig the right operator as a child."""
        return self.clone()(other)

    def __iadd__(self, other):
        """In-place addition adds the right operand as left's child"""
        return self(other)

    def __sub__(self, other):
        """Subtraction produces a copy of the left operator, with the right operator removed from left.childs."""
        if other not in self:
            raise ValueError("%s is not in %s" % (other, self))
        ret = self.clone()
        ret.pop(other._own_index)
        return ret

    def __isub__(self, other):
        """removes the child."""
        if other not in self:
            raise ValueError("%s is not in %s" % (other, self))
        return self.pop(other._own_index)

    def __mul__(self, n):
        """Returns a list of clones."""
        if not isinstance(n, int):
            raise TypeError
        if n < 0:
            raise ValueError("Negative multiplication not permitted.")
        return [self.clone() for i in range(n)]

    def __imul__(self, n):
        """Clones the element n times."""
        if not self.parent:
            return self * n
        if n == 0:
            self.parent.pop(self._own_index)
            return self
        return self.after(self * (n - 1))


class DOMFather(OperatorsModifier):
    @content_receiver(reverse=True)
    def prepend(self, _, child, name=None):
        """Adds childs to this tag, starting from the first position."""
        self._insert(child, prepend=True, name=name)
        return self

    def prepend_to(self, father):
        """Adds this tag to a father, at the beginning."""
        father.prepend(self)
        return self

    @content_receiver()
    def append(self, _, child, name=None):
        """Adds childs to this tag, after the current existing childs."""
        self._insert(child, name=name)
        return self

    def append_to(self, father):
        """Adds this tag to a parent, after the current existing childs."""
        father.append(self)
        return self


class DOMWrapper(DOMFather):
    def wrap(self, other):
        """Wraps this element inside another empty tag."""
        if other.childs:
            raise TagError(self, "Wrapping in a non empty Tag is forbidden.")
        if self.parent:
            self.before(other)
            self.parent.pop(self._own_index)
        other.append(self)
        return self

    def wrap_many(self, *args, strict=False):
        """Wraps different copies of this element inside all empty tags
        listed in params or param's (non-empty) iterators.
        Returns list of copies of this element wrapped inside args
        or None if not succeeded, in the same order and same structure,
        i.e. args = (Div(), (Div())) -> value = (A(...), (A(...)))
        If on some args it must raise TagError, it will only if strict is True,
        otherwise it will do nothing with them and return Nones on their positions"""
        for arg in args:
            is_elem = arg and isinstance(arg, TempyClass)
            is_elem_iter = (
                not is_elem and arg and isinstance(arg, Iterable) and isinstance(iter(arg).__next__(), TempyClass)
            )
            if not (is_elem or is_elem_iter):
                raise WrongArgsError(self, "%s is not DOMElement nor iterable of DOMElements" % arg)
        wcopies = []
        failures = []

        def wrap_next(tag, idx):
            nonlocal wcopies, failures
            next_copy = self.__copy__()
            try:
                return next_copy.wrap(tag)
            except TagError:
                failures.append(idx)
                return next_copy

        for arg_idx, arg in enumerate(args):
            if isinstance(arg, TempyClass):
                wcopies.append(wrap_next(arg, (arg_idx, -1)))
            else:
                iter_wcopies = []
                for iter_idx, t in enumerate(arg):
                    iter_wcopies.append(wrap_next(t, (arg_idx, iter_idx)))
                wcopies.append(type(arg)(iter_wcopies))

        if failures and strict:
            fail_repr = ', '.join(map(lambda i: str(i[0]) if i[1] == -1 else "[%s - %s]" % i, failures))
            raise TagError(self, "Wrapping in a non empty Tag is forbidden, failed on arguments %s" % fail_repr)
        return wcopies

    def wrap_inner(self, other):
        self.move_childs(other)
        self(other)
        return self


class DOMModifier(DOMWrapper):
    pass