697 lines
21 KiB
Python
697 lines
21 KiB
Python
import copy
|
|
from typing import Any, Dict, Iterable, List
|
|
|
|
from bigtree.utils.exceptions import CorruptedTreeError, LoopError, TreeError
|
|
from bigtree.utils.iterators import preorder_iter
|
|
|
|
|
|
class BaseNode:
|
|
"""
|
|
BaseNode extends any Python class to a tree node.
|
|
Nodes can have attributes if they are initialized from `Node`, *dictionary*, or *pandas DataFrame*.
|
|
|
|
Nodes can be linked to each other with `parent` and `children` setter methods,
|
|
or using bitshift operator with the convention `parent_node >> child_node` or `child_node << parent_node`.
|
|
|
|
>>> from bigtree import Node, print_tree
|
|
>>> root = Node("a", age=90)
|
|
>>> b = Node("b", age=65)
|
|
>>> c = Node("c", age=60)
|
|
>>> d = Node("d", age=40)
|
|
>>> root.children = [b, c]
|
|
>>> d.parent = b
|
|
>>> print_tree(root, attr_list=["age"])
|
|
a [age=90]
|
|
├── b [age=65]
|
|
│ └── d [age=40]
|
|
└── c [age=60]
|
|
|
|
>>> from bigtree import Node
|
|
>>> root = Node("a", age=90)
|
|
>>> b = Node("b", age=65)
|
|
>>> c = Node("c", age=60)
|
|
>>> d = Node("d", age=40)
|
|
>>> root >> b
|
|
>>> root >> c
|
|
>>> d << b
|
|
>>> print_tree(root, attr_list=["age"])
|
|
a [age=90]
|
|
├── b [age=65]
|
|
│ └── d [age=40]
|
|
└── c [age=60]
|
|
|
|
Directly passing `parent` argument.
|
|
|
|
>>> from bigtree import Node
|
|
>>> root = Node("a")
|
|
>>> b = Node("b", parent=root)
|
|
>>> c = Node("c", parent=root)
|
|
>>> d = Node("d", parent=b)
|
|
|
|
Directly passing `children` argument.
|
|
|
|
>>> from bigtree import Node
|
|
>>> d = Node("d")
|
|
>>> c = Node("c")
|
|
>>> b = Node("b", children=[d])
|
|
>>> a = Node("a", children=[b, c])
|
|
|
|
**BaseNode Creation**
|
|
|
|
Node can be created by instantiating a `BaseNode` class or by using a *dictionary*.
|
|
If node is created with dictionary, all keys of dictionary will be stored as class attributes.
|
|
|
|
>>> from bigtree import Node
|
|
>>> root = Node.from_dict({"name": "a", "age": 90})
|
|
|
|
**BaseNode Attributes**
|
|
|
|
These are node attributes that have getter and/or setter methods.
|
|
|
|
Get and set other `BaseNode`
|
|
|
|
1. ``parent``: Get/set parent node
|
|
2. ``children``: Get/set child nodes
|
|
|
|
Get other `BaseNode`
|
|
|
|
1. ``ancestors``: Get ancestors of node excluding self, iterator
|
|
2. ``descendants``: Get descendants of node excluding self, iterator
|
|
3. ``leaves``: Get all leaf node(s) from self, iterator
|
|
4. ``siblings``: Get siblings of self
|
|
5. ``left_sibling``: Get sibling left of self
|
|
6. ``right_sibling``: Get sibling right of self
|
|
|
|
Get `BaseNode` configuration
|
|
|
|
1. ``node_path``: Get tuple of nodes from root
|
|
2. ``is_root``: Get indicator if self is root node
|
|
3. ``is_leaf``: Get indicator if self is leaf node
|
|
4. ``root``: Get root node of tree
|
|
5. ``depth``: Get depth of self
|
|
6. ``max_depth``: Get maximum depth from root to leaf node
|
|
|
|
**BaseNode Methods**
|
|
|
|
These are methods available to be performed on `BaseNode`.
|
|
|
|
Constructor methods
|
|
|
|
1. ``from_dict()``: Create BaseNode from dictionary
|
|
|
|
`BaseNode` methods
|
|
|
|
1. ``describe()``: Get node information sorted by attributes, returns list of tuples
|
|
2. ``get_attr(attr_name: str)``: Get value of node attribute
|
|
3. ``set_attrs(attrs: dict)``: Set node attribute name(s) and value(s)
|
|
4. ``go_to(node: BaseNode)``: Get a path from own node to another node from same tree
|
|
5. ``copy()``: Deep copy BaseNode
|
|
6. ``sort()``: Sort child nodes
|
|
|
|
----
|
|
|
|
"""
|
|
|
|
def __init__(self, parent=None, children: List = None, **kwargs):
|
|
self.__parent = None
|
|
self.__children = []
|
|
if children is None:
|
|
children = []
|
|
self.parent = parent
|
|
self.children = children
|
|
if "parents" in kwargs:
|
|
raise ValueError(
|
|
"Attempting to set `parents` attribute, do you mean `parent`?"
|
|
)
|
|
self.__dict__.update(**kwargs)
|
|
|
|
@property
|
|
def parent(self):
|
|
"""Get parent node
|
|
|
|
Returns:
|
|
(Self)
|
|
"""
|
|
return self.__parent
|
|
|
|
@staticmethod
|
|
def __check_parent_type(new_parent):
|
|
"""Check parent type
|
|
|
|
Args:
|
|
new_parent (Self): parent node
|
|
"""
|
|
if not (isinstance(new_parent, BaseNode) or new_parent is None):
|
|
raise TypeError(
|
|
f"Expect input to be BaseNode type or NoneType, received input type {type(new_parent)}"
|
|
)
|
|
|
|
def __check_parent_loop(self, new_parent):
|
|
"""Check parent type
|
|
|
|
Args:
|
|
new_parent (Self): parent node
|
|
"""
|
|
if new_parent is not None:
|
|
if new_parent is self:
|
|
raise LoopError("Error setting parent: Node cannot be parent of itself")
|
|
if any(
|
|
ancestor is self
|
|
for ancestor in new_parent.ancestors
|
|
if new_parent.ancestors
|
|
):
|
|
raise LoopError(
|
|
"Error setting parent: Node cannot be ancestor of itself"
|
|
)
|
|
|
|
@parent.setter
|
|
def parent(self, new_parent):
|
|
"""Set parent node
|
|
|
|
Args:
|
|
new_parent (Self): parent node
|
|
"""
|
|
self.__check_parent_type(new_parent)
|
|
self.__check_parent_loop(new_parent)
|
|
|
|
current_parent = self.parent
|
|
current_child_idx = None
|
|
|
|
# Assign new parent - rollback if error
|
|
self.__pre_assign_parent(new_parent)
|
|
try:
|
|
# Remove self from old parent
|
|
if current_parent is not None:
|
|
if not any(
|
|
child is self for child in current_parent.children
|
|
): # pragma: no cover
|
|
raise CorruptedTreeError(
|
|
"Error setting parent: Node does not exist as children of its parent"
|
|
)
|
|
current_child_idx = current_parent.__children.index(self)
|
|
current_parent.__children.remove(self)
|
|
|
|
# Assign self to new parent
|
|
self.__parent = new_parent
|
|
if new_parent is not None:
|
|
new_parent.__children.append(self)
|
|
|
|
self.__post_assign_parent(new_parent)
|
|
|
|
except Exception as exc_info:
|
|
# Remove self from new parent
|
|
if new_parent is not None:
|
|
new_parent.__children.remove(self)
|
|
|
|
# Reassign self to old parent
|
|
self.__parent = current_parent
|
|
if current_child_idx is not None:
|
|
current_parent.__children.insert(current_child_idx, self)
|
|
raise TreeError(exc_info)
|
|
|
|
def __pre_assign_parent(self, new_parent):
|
|
"""Custom method to check before attaching parent
|
|
Can be overriden with `_BaseNode__pre_assign_parent()`
|
|
|
|
Args:
|
|
new_parent (Self): new parent to be added
|
|
"""
|
|
pass
|
|
|
|
def __post_assign_parent(self, new_parent):
|
|
"""Custom method to check after attaching parent
|
|
Can be overriden with `_BaseNode__post_assign_parent()`
|
|
|
|
Args:
|
|
new_parent (Self): new parent to be added
|
|
"""
|
|
pass
|
|
|
|
@property
|
|
def parents(self) -> None:
|
|
"""Do not allow `parents` attribute to be accessed"""
|
|
raise ValueError(
|
|
"Attempting to access `parents` attribute, do you mean `parent`?"
|
|
)
|
|
|
|
@parents.setter
|
|
def parents(self, new_parent):
|
|
"""Do not allow `parents` attribute to be set
|
|
|
|
Args:
|
|
new_parent (Self): parent node
|
|
"""
|
|
raise ValueError("Attempting to set `parents` attribute, do you mean `parent`?")
|
|
|
|
@property
|
|
def children(self) -> Iterable:
|
|
"""Get child nodes
|
|
|
|
Returns:
|
|
(Iterable[Self])
|
|
"""
|
|
return tuple(self.__children)
|
|
|
|
def __check_children_type(self, new_children: List):
|
|
"""Check child type
|
|
|
|
Args:
|
|
new_children (List[Self]): child node
|
|
"""
|
|
if not isinstance(new_children, list):
|
|
raise TypeError(
|
|
f"Children input should be list type, received input type {type(new_children)}"
|
|
)
|
|
|
|
def __check_children_loop(self, new_children: List):
|
|
"""Check child loop
|
|
|
|
Args:
|
|
new_children (List[Self]): child node
|
|
"""
|
|
seen_children = []
|
|
for new_child in new_children:
|
|
# Check type
|
|
if not isinstance(new_child, BaseNode):
|
|
raise TypeError(
|
|
f"Expect input to be BaseNode type, received input type {type(new_child)}"
|
|
)
|
|
|
|
# Check for loop and tree structure
|
|
if new_child is self:
|
|
raise LoopError("Error setting child: Node cannot be child of itself")
|
|
if any(child is new_child for child in self.ancestors):
|
|
raise LoopError(
|
|
"Error setting child: Node cannot be ancestors of itself"
|
|
)
|
|
|
|
# Check for duplicate children
|
|
if id(new_child) in seen_children:
|
|
raise TreeError(
|
|
"Error setting child: Node cannot be added multiple times as a child"
|
|
)
|
|
else:
|
|
seen_children.append(id(new_child))
|
|
|
|
@children.setter
|
|
def children(self, new_children: List):
|
|
"""Set child nodes
|
|
|
|
Args:
|
|
new_children (List[Self]): child node
|
|
"""
|
|
self.__check_children_type(new_children)
|
|
self.__check_children_loop(new_children)
|
|
|
|
current_new_children = {
|
|
new_child: (new_child.parent.__children.index(new_child), new_child.parent)
|
|
for new_child in new_children
|
|
if new_child.parent is not None
|
|
}
|
|
current_new_orphan = [
|
|
new_child for new_child in new_children if new_child.parent is None
|
|
]
|
|
current_children = list(self.children)
|
|
|
|
# Assign new children - rollback if error
|
|
self.__pre_assign_children(new_children)
|
|
try:
|
|
# Remove old children from self
|
|
del self.children
|
|
|
|
# Assign new children to self
|
|
self.__children = new_children
|
|
for new_child in new_children:
|
|
if new_child.parent:
|
|
new_child.parent.__children.remove(new_child)
|
|
new_child.__parent = self
|
|
self.__post_assign_children(new_children)
|
|
except Exception as exc_info:
|
|
# Reassign new children to their original parent
|
|
for child, idx_parent in current_new_children.items():
|
|
child_idx, parent = idx_parent
|
|
child.__parent = parent
|
|
parent.__children.insert(child_idx, child)
|
|
for child in current_new_orphan:
|
|
child.__parent = None
|
|
|
|
# Reassign old children to self
|
|
self.__children = current_children
|
|
for child in current_children:
|
|
child.__parent = self
|
|
raise TreeError(exc_info)
|
|
|
|
@children.deleter
|
|
def children(self):
|
|
"""Delete child node(s)"""
|
|
for child in self.children:
|
|
child.parent.__children.remove(child)
|
|
child.__parent = None
|
|
|
|
def __pre_assign_children(self, new_children: List):
|
|
"""Custom method to check before attaching children
|
|
Can be overriden with `_BaseNode__pre_assign_children()`
|
|
|
|
Args:
|
|
new_children (List[Self]): new children to be added
|
|
"""
|
|
pass
|
|
|
|
def __post_assign_children(self, new_children: List):
|
|
"""Custom method to check after attaching children
|
|
Can be overriden with `_BaseNode__post_assign_children()`
|
|
|
|
Args:
|
|
new_children (List[Self]): new children to be added
|
|
"""
|
|
pass
|
|
|
|
@property
|
|
def ancestors(self) -> Iterable:
|
|
"""Get iterator to yield all ancestors of self, does not include self
|
|
|
|
Returns:
|
|
(Iterable[Self])
|
|
"""
|
|
node = self.parent
|
|
while node is not None:
|
|
yield node
|
|
node = node.parent
|
|
|
|
@property
|
|
def descendants(self) -> Iterable:
|
|
"""Get iterator to yield all descendants of self, does not include self
|
|
|
|
Returns:
|
|
(Iterable[Self])
|
|
"""
|
|
yield from preorder_iter(self, filter_condition=lambda _node: _node != self)
|
|
|
|
@property
|
|
def leaves(self) -> Iterable:
|
|
"""Get iterator to yield all leaf nodes from self
|
|
|
|
Returns:
|
|
(Iterable[Self])
|
|
"""
|
|
yield from preorder_iter(self, filter_condition=lambda _node: _node.is_leaf)
|
|
|
|
@property
|
|
def siblings(self) -> Iterable:
|
|
"""Get siblings of self
|
|
|
|
Returns:
|
|
(Iterable[Self])
|
|
"""
|
|
if self.is_root:
|
|
return ()
|
|
return tuple(child for child in self.parent.children if child is not self)
|
|
|
|
@property
|
|
def left_sibling(self):
|
|
"""Get sibling left of self
|
|
|
|
Returns:
|
|
(Self)
|
|
"""
|
|
if self.parent:
|
|
children = self.parent.children
|
|
child_idx = children.index(self)
|
|
if child_idx:
|
|
return self.parent.children[child_idx - 1]
|
|
return None
|
|
|
|
@property
|
|
def right_sibling(self):
|
|
"""Get sibling right of self
|
|
|
|
Returns:
|
|
(Self)
|
|
"""
|
|
if self.parent:
|
|
children = self.parent.children
|
|
child_idx = children.index(self)
|
|
if child_idx + 1 < len(children):
|
|
return self.parent.children[child_idx + 1]
|
|
return None
|
|
|
|
@property
|
|
def node_path(self) -> Iterable:
|
|
"""Get tuple of nodes starting from root
|
|
|
|
Returns:
|
|
(Iterable[Self])
|
|
"""
|
|
if self.is_root:
|
|
return [self]
|
|
return tuple(list(self.parent.node_path) + [self])
|
|
|
|
@property
|
|
def is_root(self) -> bool:
|
|
"""Get indicator if self is root node
|
|
|
|
Returns:
|
|
(bool)
|
|
"""
|
|
return self.parent is None
|
|
|
|
@property
|
|
def is_leaf(self) -> bool:
|
|
"""Get indicator if self is leaf node
|
|
|
|
Returns:
|
|
(bool)
|
|
"""
|
|
return not len(list(self.children))
|
|
|
|
@property
|
|
def root(self):
|
|
"""Get root node of tree
|
|
|
|
Returns:
|
|
(Self)
|
|
"""
|
|
if self.is_root:
|
|
return self
|
|
return self.parent.root
|
|
|
|
@property
|
|
def depth(self) -> int:
|
|
"""Get depth of self, indexing starts from 1
|
|
|
|
Returns:
|
|
(int)
|
|
"""
|
|
if self.is_root:
|
|
return 1
|
|
return self.parent.depth + 1
|
|
|
|
@property
|
|
def max_depth(self) -> int:
|
|
"""Get maximum depth from root to leaf node
|
|
|
|
Returns:
|
|
(int)
|
|
"""
|
|
return max(node.depth for node in list(preorder_iter(self.root)))
|
|
|
|
@classmethod
|
|
def from_dict(cls, input_dict: Dict[str, Any]):
|
|
"""Construct node from dictionary, all keys of dictionary will be stored as class attributes
|
|
Input dictionary must have key `name` if not `Node` will not have any name
|
|
|
|
>>> from bigtree import Node
|
|
>>> a = Node.from_dict({"name": "a", "age": 90})
|
|
|
|
Args:
|
|
input_dict (Dict[str, Any]): dictionary with node information, key: attribute name, value: attribute value
|
|
|
|
Returns:
|
|
(Self)
|
|
"""
|
|
return cls(**input_dict)
|
|
|
|
def describe(self, exclude_attributes: List[str] = [], exclude_prefix: str = ""):
|
|
"""Get node information sorted by attribute name, returns list of tuples
|
|
|
|
>>> from bigtree.node.node import Node
|
|
>>> a = Node('a', age=90)
|
|
>>> a.describe()
|
|
[('_BaseNode__children', []), ('_BaseNode__parent', None), ('_sep', '/'), ('age', 90), ('name', 'a')]
|
|
>>> a.describe(exclude_prefix="_")
|
|
[('age', 90), ('name', 'a')]
|
|
>>> a.describe(exclude_prefix="_", exclude_attributes=["name"])
|
|
[('age', 90)]
|
|
|
|
Args:
|
|
exclude_attributes (List[str]): list of attributes to exclude
|
|
exclude_prefix (str): prefix of attributes to exclude
|
|
|
|
Returns:
|
|
(List[str])
|
|
"""
|
|
return [
|
|
item
|
|
for item in sorted(self.__dict__.items(), key=lambda item: item[0])
|
|
if (item[0] not in exclude_attributes)
|
|
and (not len(exclude_prefix) or not item[0].startswith(exclude_prefix))
|
|
]
|
|
|
|
def get_attr(self, attr_name: str) -> Any:
|
|
"""Get value of node attribute
|
|
Returns None if attribute name does not exist
|
|
|
|
>>> from bigtree.node.node import Node
|
|
>>> a = Node('a', age=90)
|
|
>>> a.get_attr("age")
|
|
90
|
|
|
|
Args:
|
|
attr_name (str): attribute name
|
|
|
|
Returns:
|
|
(Any)
|
|
"""
|
|
try:
|
|
return self.__getattribute__(attr_name)
|
|
except AttributeError:
|
|
return None
|
|
|
|
def set_attrs(self, attrs: Dict[str, Any]):
|
|
"""Set node attributes
|
|
|
|
>>> from bigtree.node.node import Node
|
|
>>> a = Node('a')
|
|
>>> a.set_attrs({"age": 90})
|
|
>>> a
|
|
Node(/a, age=90)
|
|
|
|
Args:
|
|
attrs (Dict[str, Any]): attribute dictionary,
|
|
key: attribute name, value: attribute value
|
|
"""
|
|
self.__dict__.update(attrs)
|
|
|
|
def go_to(self, node) -> Iterable:
|
|
"""Get path from current node to specified node from same tree
|
|
|
|
>>> from bigtree import Node, print_tree
|
|
>>> a = Node(name="a")
|
|
>>> b = Node(name="b", parent=a)
|
|
>>> c = Node(name="c", parent=a)
|
|
>>> d = Node(name="d", parent=b)
|
|
>>> e = Node(name="e", parent=b)
|
|
>>> f = Node(name="f", parent=c)
|
|
>>> g = Node(name="g", parent=e)
|
|
>>> h = Node(name="h", parent=e)
|
|
>>> print_tree(a)
|
|
a
|
|
├── b
|
|
│ ├── d
|
|
│ └── e
|
|
│ ├── g
|
|
│ └── h
|
|
└── c
|
|
└── f
|
|
>>> d.go_to(d)
|
|
[Node(/a/b/d, )]
|
|
>>> d.go_to(g)
|
|
[Node(/a/b/d, ), Node(/a/b, ), Node(/a/b/e, ), Node(/a/b/e/g, )]
|
|
>>> d.go_to(f)
|
|
[Node(/a/b/d, ), Node(/a/b, ), Node(/a, ), Node(/a/c, ), Node(/a/c/f, )]
|
|
|
|
Args:
|
|
node (Self): node to travel to from current node, inclusive of start and end node
|
|
|
|
Returns:
|
|
(Iterable)
|
|
"""
|
|
if not isinstance(node, BaseNode):
|
|
raise TypeError(
|
|
f"Expect node to be BaseNode type, received input type {type(node)}"
|
|
)
|
|
if self.root != node.root:
|
|
raise TreeError(
|
|
f"Nodes are not from the same tree. Check {self} and {node}"
|
|
)
|
|
if self == node:
|
|
return [self]
|
|
self_path = [self] + list(self.ancestors)
|
|
node_path = ([node] + list(node.ancestors))[::-1]
|
|
common_nodes = set(self_path).intersection(set(node_path))
|
|
self_min_index, min_common_node = sorted(
|
|
[(self_path.index(_node), _node) for _node in common_nodes]
|
|
)[0]
|
|
node_min_index = node_path.index(min_common_node)
|
|
return self_path[:self_min_index] + node_path[node_min_index:]
|
|
|
|
def copy(self):
|
|
"""Deep copy self; clone BaseNode
|
|
|
|
>>> from bigtree.node.node import Node
|
|
>>> a = Node('a')
|
|
>>> a_copy = a.copy()
|
|
|
|
Returns:
|
|
(Self)
|
|
"""
|
|
return copy.deepcopy(self)
|
|
|
|
def sort(self, **kwargs):
|
|
"""Sort children, possible keyword arguments include ``key=lambda node: node.name``, ``reverse=True``
|
|
|
|
>>> from bigtree import Node, print_tree
|
|
>>> a = Node('a')
|
|
>>> c = Node("c", parent=a)
|
|
>>> b = Node("b", parent=a)
|
|
>>> print_tree(a)
|
|
a
|
|
├── c
|
|
└── b
|
|
>>> a.sort(key=lambda node: node.name)
|
|
>>> print_tree(a)
|
|
a
|
|
├── b
|
|
└── c
|
|
"""
|
|
children = list(self.children)
|
|
children.sort(**kwargs)
|
|
self.__children = children
|
|
|
|
def __copy__(self):
|
|
"""Shallow copy self
|
|
|
|
>>> import copy
|
|
>>> from bigtree.node.node import Node
|
|
>>> a = Node('a')
|
|
>>> a_copy = copy.deepcopy(a)
|
|
|
|
Returns:
|
|
(Self)
|
|
"""
|
|
obj = type(self).__new__(self.__class__)
|
|
obj.__dict__.update(self.__dict__)
|
|
return obj
|
|
|
|
def __repr__(self):
|
|
class_name = self.__class__.__name__
|
|
node_dict = self.describe(exclude_prefix="_")
|
|
node_description = ", ".join([f"{k}={v}" for k, v in node_dict])
|
|
return f"{class_name}({node_description})"
|
|
|
|
def __rshift__(self, other):
|
|
"""Set children using >> bitshift operator for self >> other
|
|
|
|
Args:
|
|
other (Self): other node, children
|
|
"""
|
|
other.parent = self
|
|
|
|
def __lshift__(self, other):
|
|
"""Set parent using << bitshift operator for self << other
|
|
|
|
Args:
|
|
other (Self): other node, parent
|
|
"""
|
|
self.parent = other
|