fabiommendes/sidekick

View on GitHub
sidekick-tree/sidekick/tree/node_base.py

Summary

Maintainability
A
2 hrs
Test Coverage
import operator as op

from .node_iterators import NodeOrLeafIteratorMixin
from ...typing import Optional, Sequence, Callable


class NodeOrLeaf(NodeOrLeafIteratorMixin):
    """
    Abstract base class for leaf or node types.
    """

    __slots__ = ("_parent", "_attrs")
    _parent: Optional["Node"]
    _children: Sequence["NodeOrLeaf"]
    _pretty_printer: Callable[..., str] = None

    #: True if sub-tree cannot have children (a Leaf instance)
    is_leaf = False

    #: True for Nodes with no children.
    has_children = property(lambda self: len(self._children) == 0)

    #: Expose mapping of attributes
    attrs = property(lambda self: self._attrs)

    @attrs.setter
    def attrs(self, value):
        self._attrs.clear()
        self._attrs.update(value)

    #
    # Parent node information
    #

    #: Base node in tree.
    @property
    def root(self):
        root: NodeOrLeaf = self
        while root._parent is not None:
            root = root._parent
        return root

    #: Parent and their parents.
    ancestors = property(lambda self: self.path[:-1])

    #: Parent Node.
    parent = property(op.attrgetter("_parent"))

    @parent.setter
    def parent(self, value):
        if value is None:
            self._parent = None
        elif not isinstance(value, Node):
            raise TypeError(f"Parent node {value!r} is not of type 'Node'.")
        elif value is not self._parent:
            has_loop = (
                self.is_ancestor_of(value)
                or value.is_ancestor_of(self)
                or self is value
            )
            if has_loop:
                msg = f"Setting parent to {value} would create a dependency loop"
                raise ValueError(msg)
            self.detach()
            value.children.append(self)

    @property
    def path(self):
        """Path from root to node"""
        path = []
        node = self
        while node:
            path.append(node)
            node = node._parent
        return tuple(reversed(path))

    #
    # Children and sibling nodes
    #

    #: List of children nodes
    children = property(lambda self: self._children)

    #: Tuple of leaf nodes.
    leaves = property(lambda self: tuple(filter(lambda c: c.is_leaf, self.iter())))

    #: Children and children of children.
    descendants = property(lambda self: tuple(self.iter_descendants()))

    def _sibling(self, delta):
        if self._parent is not None:
            siblings = self._parent.children
            for idx, sibling in enumerate(siblings):
                if self is sibling:
                    try:
                        new_idx = idx + delta
                        if new_idx < 0:
                            return None
                        return siblings[new_idx]
                    except IndexError:
                        break
        return None

    #: Left siblings or None
    left_sibling = property(lambda self: self._sibling(-1))

    #: Right siblings or None
    right_sibling = property(lambda self: self._sibling(+1))

    @property
    def siblings(self):
        """
        Tuple of nodes with the same parent.
        """
        parent = self._parent
        if parent is None:
            return ()
        else:
            parent: NodeOrLeaf
            generation = parent._children
            return tuple(node for node in generation if node is not self)

    #
    # Properties of node or tree
    #

    #: Number of edges on the longest path to a leaf `Node`.
    height = property(
        lambda self: max((c.height for c in self._children), default=0) + 1
    )

    #: Number of edges to the root `Node`.
    depth = property(lambda self: len(self.path) - 1)

    #: True if node is in the base of tree.
    is_root = property(lambda self: self._parent is None)

    #: True if element defines arbitrary meta-data
    has_attrs = property(lambda self: bool(self._attrs))

    def __eq__(self, other):
        if self.__class__ is other.__class__:
            raise NotImplementedError("must be implemented in subclass")
        return NotImplemented

    #
    # Tree shaping
    #
    def detach(self) -> "NodeOrLeaf":
        """
        Detach itself from tree.

        This method returns self, so it can be chained.
        """
        parent = self._parent
        if parent is not None:
            parent.discard_child(self)
        return self

    def is_ancestor_of(self, node) -> bool:
        """
        Check if node is an ancestor of argument.
        """
        return any(self is ancestor for ancestor in node.iter_ancestors())

    def copy(self) -> "NodeOrLeaf":
        """
        Create a copy of tree.
        """
        raise NotImplementedError

    #
    # Tree Rendering
    #
    def __repr__(self):
        return self._repr()

    def _repr(self, children=True):
        data = self._repr_attrs()
        return f"{self.__class__.__name__}({data})"

    def _repr_as_child(self):
        return self._repr()

    def _repr_attrs(self):
        return ", ".join(f"{k}={v!r}" for k, v in self._attrs.items())

    def _repr_node(self):
        """
        Simplified representation used to show element as node in a pretty
        printed tree.

        This method never show children.
        """
        return self._repr(children=False)

    def pretty(self, style="line", renderer=None) -> str:
        """
        Pretty-printed representation of tree.

        Args:
            style:
                One of 'ascii', 'line', 'rounded', or 'double'. It can also
                be a 3-string tuple with the (vertical, horizontal, end) prefixes
                for each rendered line.
            renderer:
                A function that renders row omitting its children.
        """
        return self._pretty_printer(self, tree_style=style, node_renderer=renderer)


from .node_classes import Node  # noqa: E402