added 3rd party packages, elog, bigtree

This commit is contained in:
2024-02-27 15:40:00 +01:00
parent 277c22f800
commit 6b59fe16ce
69 changed files with 17449 additions and 0 deletions

View File

@@ -0,0 +1,696 @@
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

View File

@@ -0,0 +1,395 @@
from typing import Iterable, List, Union
from bigtree.node.node import Node
from bigtree.utils.exceptions import CorruptedTreeError, LoopError, TreeError
class BinaryNode(Node):
"""
BinaryNode is an extension of Node, and is able to extend to any Python class for Binary Tree implementation.
Nodes can have attributes if they are initialized from `BinaryNode`, *dictionary*, or *pandas DataFrame*.
BinaryNode can be linked to each other with `children`, `left`, or `right` setter methods.
If initialized with `children`, it must be length 2, denoting left and right child.
>>> from bigtree import BinaryNode, print_tree
>>> a = BinaryNode(1)
>>> b = BinaryNode(2)
>>> c = BinaryNode(3)
>>> d = BinaryNode(4)
>>> a.children = [b, c]
>>> b.right = d
>>> print_tree(a)
1
├── 2
│ └── 4
└── 3
Directly passing `left`, `right`, or `children` argument.
>>> from bigtree import BinaryNode
>>> d = BinaryNode(4)
>>> c = BinaryNode(3)
>>> b = BinaryNode(2, right=d)
>>> a = BinaryNode(1, children=[b, c])
**BinaryNode Creation**
Node can be created by instantiating a `BinaryNode` 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 BinaryNode
>>> a = BinaryNode.from_dict({"name": "1"})
>>> a
BinaryNode(name=1, val=1)
**BinaryNode Attributes**
These are node attributes that have getter and/or setter methods.
Get `BinaryNode` configuration
1. ``left``: Get left children
2. ``right``: Get right children
----
"""
def __init__(
self,
name: Union[str, int] = "",
left=None,
right=None,
parent=None,
children: List = None,
**kwargs,
):
self.val = int(name)
self.name = str(name)
self._sep = "/"
self.__parent = None
self.__children = []
if not children:
children = []
if len(children):
if len(children) and len(children) != 2:
raise ValueError("Children input must have length 2")
if left and left != children[0]:
raise ValueError(
f"Attempting to set both left and children with mismatched values\n"
f"Check left {left} and children {children}"
)
if right and right != children[1]:
raise ValueError(
f"Attempting to set both right and children with mismatched values\n"
f"Check right {right} and children {children}"
)
else:
children = [left, right]
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 left(self):
"""Get left children
Returns:
(Self)
"""
return self.__children[0]
@left.setter
def left(self, left_child):
"""Set left children
Args:
left_child (Self): left child
"""
self.children = [left_child, self.right]
@property
def right(self):
"""Get right children
Returns:
(Self)
"""
return self.__children[1]
@right.setter
def right(self, right_child):
"""Set right children
Args:
right_child (Self): right child
"""
self.children = [self.left, right_child]
@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, BinaryNode) or new_parent is None):
raise TypeError(
f"Expect input to be BinaryNode type or NoneType, received input type {type(new_parent)}"
)
@parent.setter
def parent(self, new_parent):
"""Set parent node
Args:
new_parent (Self): parent node
"""
self.__check_parent_type(new_parent)
self._BaseNode__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[current_child_idx] = None
# Assign self to new parent
self.__parent = new_parent
if new_parent is not None:
inserted = False
for child_idx, child in enumerate(new_parent.__children):
if not child and not inserted:
new_parent.__children[child_idx] = self
inserted = True
if not inserted:
raise TreeError(f"Parent {new_parent} already has 2 children")
self.__post_assign_parent(new_parent)
except Exception as exc_info:
# Remove self from new parent
if new_parent is not None and self in new_parent.__children:
child_idx = new_parent.__children.index(self)
new_parent.__children[child_idx] = None
# Reassign self to old parent
self.__parent = current_parent
if current_child_idx is not None:
current_parent.__children[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 `_BinaryNode__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 `_BinaryNode__post_assign_parent()`
Args:
new_parent (Self): new parent to be added
"""
pass
@property
def children(self) -> Iterable:
"""Get child nodes
Returns:
(Iterable[Self])
"""
return tuple(self.__children)
def __check_children_type(self, new_children: List) -> List:
"""Check child type
Args:
new_children (List[Self]): child node
"""
if not len(new_children):
new_children = [None, None]
if len(new_children) != 2:
raise ValueError("Children input must have length 2")
return 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 new_child is not None and not isinstance(new_child, BinaryNode):
raise TypeError(
f"Expect input to be BinaryNode type or NoneType, 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 new_child is not None:
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._BaseNode__check_children_type(new_children)
new_children = 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 is not None and new_child.parent is not None
}
current_new_orphan = [
new_child
for new_child in new_children
if new_child is not None and 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 is not None:
if new_child.parent:
child_idx = new_child.parent.__children.index(new_child)
new_child.parent.__children[child_idx] = None
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[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:
if child:
child.__parent = self
raise TreeError(exc_info)
@children.deleter
def children(self):
"""Delete child node(s)"""
for child in self.children:
if child is not None:
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 `_BinaryNode__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 `_BinaryNode__post_assign_children()`
Args:
new_children (List[Self]): new children to be added
"""
pass
@property
def is_leaf(self) -> bool:
"""Get indicator if self is leaf node
Returns:
(bool)
"""
return not len([child for child in self.children if child])
def sort(self, **kwargs):
"""Sort children, possible keyword arguments include ``key=lambda node: node.name``, ``reverse=True``
>>> from bigtree import BinaryNode, print_tree
>>> a = BinaryNode(1)
>>> c = BinaryNode(3, parent=a)
>>> b = BinaryNode(2, parent=a)
>>> print_tree(a)
1
├── 3
└── 2
>>> a.sort(key=lambda node: node.val)
>>> print_tree(a)
1
├── 2
└── 3
"""
children = [child for child in self.children if child]
if len(children) == 2:
children.sort(**kwargs)
self.__children = children
def __repr__(self):
class_name = self.__class__.__name__
node_dict = self.describe(exclude_prefix="_", exclude_attributes=[])
node_description = ", ".join([f"{k}={v}" for k, v in node_dict])
return f"{class_name}({node_description})"

View File

@@ -0,0 +1,570 @@
import copy
from typing import Any, Dict, Iterable, List
from bigtree.utils.exceptions import LoopError, TreeError
from bigtree.utils.iterators import preorder_iter
class DAGNode:
"""
Base DAGNode extends any Python class to a DAG node, for DAG implementation.
In DAG implementation, a node can have multiple parents.
Parents and children cannot be reassigned once assigned, as Nodes are allowed to have multiple parents and children.
If each node only has one parent, use `Node` class.
DAGNodes can have attributes if they are initialized from `DAGNode` or dictionary.
DAGNode can be linked to each other with `parents` and `children` setter methods,
or using bitshift operator with the convention `parent_node >> child_node` or `child_node << parent_node`.
>>> from bigtree import DAGNode
>>> a = DAGNode("a")
>>> b = DAGNode("b")
>>> c = DAGNode("c")
>>> d = DAGNode("d")
>>> c.parents = [a, b]
>>> c.children = [d]
>>> from bigtree import DAGNode
>>> a = DAGNode("a")
>>> b = DAGNode("b")
>>> c = DAGNode("c")
>>> d = DAGNode("d")
>>> a >> c
>>> b >> c
>>> d << c
Directly passing `parents` argument.
>>> from bigtree import DAGNode
>>> a = DAGNode("a")
>>> b = DAGNode("b")
>>> c = DAGNode("c", parents=[a, b])
>>> d = DAGNode("d", parents=[c])
Directly passing `children` argument.
>>> from bigtree import DAGNode
>>> d = DAGNode("d")
>>> c = DAGNode("c", children=[d])
>>> b = DAGNode("b", children=[c])
>>> a = DAGNode("a", children=[c])
**DAGNode Creation**
Node can be created by instantiating a `DAGNode` 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 DAGNode
>>> a = DAGNode.from_dict({"name": "a", "age": 90})
**DAGNode Attributes**
These are node attributes that have getter and/or setter methods.
Get and set other `DAGNode`
1. ``parents``: Get/set parent nodes
2. ``children``: Get/set child nodes
Get other `DAGNode`
1. ``ancestors``: Get ancestors of node excluding self, iterator
2. ``descendants``: Get descendants of node excluding self, iterator
3. ``siblings``: Get siblings of self
Get `DAGNode` configuration
1. ``node_name``: Get node name, without accessing `name` directly
2. ``is_root``: Get indicator if self is root node
3. ``is_leaf``: Get indicator if self is leaf node
**DAGNode Methods**
These are methods available to be performed on `DAGNode`.
Constructor methods
1. ``from_dict()``: Create DAGNode from dictionary
`DAGNode` 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 DAG
5. ``copy()``: Deep copy DAGNode
----
"""
def __init__(
self, name: str = "", parents: List = None, children: List = None, **kwargs
):
self.name = name
self.__parents = []
self.__children = []
if parents is None:
parents = []
if children is None:
children = []
self.parents = parents
self.children = children
if "parent" in kwargs:
raise ValueError(
"Attempting to set `parent` attribute, do you mean `parents`?"
)
self.__dict__.update(**kwargs)
@property
def parent(self) -> None:
"""Do not allow `parent` attribute to be accessed"""
raise ValueError(
"Attempting to access `parent` attribute, do you mean `parents`?"
)
@parent.setter
def parent(self, new_parent):
"""Do not allow `parent` attribute to be set
Args:
new_parent (Self): parent node
"""
raise ValueError("Attempting to set `parent` attribute, do you mean `parents`?")
@property
def parents(self) -> Iterable:
"""Get parent nodes
Returns:
(Iterable[Self])
"""
return tuple(self.__parents)
@staticmethod
def __check_parent_type(new_parents: List):
"""Check parent type
Args:
new_parents (List[Self]): parent nodes
"""
if not isinstance(new_parents, list):
raise TypeError(
f"Parents input should be list type, received input type {type(new_parents)}"
)
def __check_parent_loop(self, new_parents: List):
"""Check parent type
Args:
new_parents (List[Self]): parent nodes
"""
seen_parent = []
for new_parent in new_parents:
# Check type
if not isinstance(new_parent, DAGNode):
raise TypeError(
f"Expect input to be DAGNode type, received input type {type(new_parent)}"
)
# Check for loop and tree structure
if new_parent is self:
raise LoopError("Error setting parent: Node cannot be parent of itself")
if new_parent.ancestors:
if any(ancestor is self for ancestor in new_parent.ancestors):
raise LoopError(
"Error setting parent: Node cannot be ancestor of itself"
)
# Check for duplicate children
if id(new_parent) in seen_parent:
raise TreeError(
"Error setting parent: Node cannot be added multiple times as a parent"
)
else:
seen_parent.append(id(new_parent))
@parents.setter
def parents(self, new_parents: List):
"""Set parent node
Args:
new_parents (List[Self]): parent nodes
"""
self.__check_parent_type(new_parents)
self.__check_parent_loop(new_parents)
current_parents = self.__parents.copy()
# Assign new parents - rollback if error
self.__pre_assign_parents(new_parents)
try:
# Assign self to new parent
for new_parent in new_parents:
if new_parent not in self.__parents:
self.__parents.append(new_parent)
new_parent.__children.append(self)
self.__post_assign_parents(new_parents)
except Exception as exc_info:
# Remove self from new parent
for new_parent in new_parents:
if new_parent not in current_parents:
self.__parents.remove(new_parent)
new_parent.__children.remove(self)
raise TreeError(
f"{exc_info}, current parents {current_parents}, new parents {new_parents}"
)
def __pre_assign_parents(self, new_parents: List):
"""Custom method to check before attaching parent
Can be overriden with `_DAGNode__pre_assign_parent()`
Args:
new_parents (List): new parents to be added
"""
pass
def __post_assign_parents(self, new_parents: List):
"""Custom method to check after attaching parent
Can be overriden with `_DAGNode__post_assign_parent()`
Args:
new_parents (List): new parents to be added
"""
pass
@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, DAGNode):
raise TypeError(
f"Expect input to be DAGNode 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_children = list(self.children)
# Assign new children - rollback if error
self.__pre_assign_children(new_children)
try:
# Assign new children to self
for new_child in new_children:
if self not in new_child.__parents:
new_child.__parents.append(self)
self.__children.append(new_child)
self.__post_assign_children(new_children)
except Exception as exc_info:
# Reassign old children to self
for new_child in new_children:
if new_child not in current_children:
new_child.__parents.remove(self)
self.__children.remove(new_child)
raise TreeError(exc_info)
def __pre_assign_children(self, new_children: List):
"""Custom method to check before attaching children
Can be overriden with `_DAGNode__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 `_DAGNode__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])
"""
if not len(list(self.parents)):
return ()
def recursive_parent(node):
for _node in node.parents:
yield from recursive_parent(_node)
yield _node
ancestors = list(recursive_parent(self))
return list(dict.fromkeys(ancestors))
@property
def descendants(self) -> Iterable:
"""Get iterator to yield all descendants of self, does not include self
Returns:
(Iterable[Self])
"""
descendants = list(
preorder_iter(self, filter_condition=lambda _node: _node != self)
)
return list(dict.fromkeys(descendants))
@property
def siblings(self) -> Iterable:
"""Get siblings of self
Returns:
(Iterable[Self])
"""
if self.is_root:
return ()
return tuple(
child
for parent in self.parents
for child in parent.children
if child is not self
)
@property
def node_name(self) -> str:
"""Get node name
Returns:
(str)
"""
return self.name
@property
def is_root(self) -> bool:
"""Get indicator if self is root node
Returns:
(bool)
"""
return not len(list(self.parents))
@property
def is_leaf(self) -> bool:
"""Get indicator if self is leaf node
Returns:
(bool)
"""
return not len(list(self.children))
@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 DAGNode
>>> a = DAGNode.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
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
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.dagnode import DAGNode
>>> a = DAGNode('a')
>>> a.set_attrs({"age": 90})
>>> a
DAGNode(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[Iterable]:
"""Get list of possible paths from current node to specified node from same tree
>>> from bigtree import DAGNode
>>> a = DAGNode("a")
>>> b = DAGNode("b")
>>> c = DAGNode("c")
>>> d = DAGNode("d")
>>> a >> c
>>> b >> c
>>> c >> d
>>> a >> d
>>> a.go_to(c)
[[DAGNode(a, ), DAGNode(c, )]]
>>> a.go_to(d)
[[DAGNode(a, ), DAGNode(c, ), DAGNode(d, )], [DAGNode(a, ), DAGNode(d, )]]
>>> a.go_to(b)
Traceback (most recent call last):
...
bigtree.utils.exceptions.TreeError: It is not possible to go to DAGNode(b, )
Args:
node (Self): node to travel to from current node, inclusive of start and end node
Returns:
(Iterable[Iterable])
"""
if not isinstance(node, DAGNode):
raise TypeError(
f"Expect node to be DAGNode type, received input type {type(node)}"
)
if self == node:
return [self]
if node not in self.descendants:
raise TreeError(f"It is not possible to go to {node}")
self.__path = []
def recursive_path(_node, _path, _ans):
if _node: # pragma: no cover
_path.append(_node)
if _node == node:
return _path
for _child in _node.children:
ans = recursive_path(_child, _path.copy(), _ans)
if ans:
self.__path.append(ans)
recursive_path(self, [], [])
return self.__path
def copy(self):
"""Deep copy self; clone DAGNode
>>> from bigtree.node.dagnode import DAGNode
>>> a = DAGNode('a')
>>> a_copy = a.copy()
Returns:
(Self)
"""
return copy.deepcopy(self)
def __copy__(self):
"""Shallow copy self
>>> import copy
>>> from bigtree.node.dagnode import DAGNode
>>> a = DAGNode('a')
>>> a_copy = copy.deepcopy(a)
Returns:
(Self)
"""
obj = type(self).__new__(self.__class__)
obj.__dict__.update(self.__dict__)
return obj
def __rshift__(self, other):
"""Set children using >> bitshift operator for self >> other
Args:
other (Self): other node, children
"""
other.parents = [self]
def __lshift__(self, other):
"""Set parent using << bitshift operator for self << other
Args:
other (Self): other node, parent
"""
self.parents = [other]
def __repr__(self):
class_name = self.__class__.__name__
node_dict = self.describe(exclude_attributes=["name"])
node_description = ", ".join(
[f"{k}={v}" for k, v in node_dict if not k.startswith("_")]
)
return f"{class_name}({self.node_name}, {node_description})"

View File

@@ -0,0 +1,204 @@
from collections import Counter
from typing import List
from bigtree.node.basenode import BaseNode
from bigtree.utils.exceptions import TreeError
class Node(BaseNode):
"""
Node is an extension of BaseNode, and is able to extend to any Python class.
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.
>>> from bigtree import Node
>>> a = Node("a")
>>> b = Node("b")
>>> c = Node("c")
>>> d = Node("d")
>>> b.parent = a
>>> b.children = [c, d]
Directly passing `parent` argument.
>>> from bigtree import Node
>>> a = Node("a")
>>> b = Node("b", parent=a)
>>> c = Node("c", parent=b)
>>> d = Node("d", parent=b)
Directly passing `children` argument.
>>> from bigtree import Node
>>> d = Node("d")
>>> c = Node("c")
>>> b = Node("b", children=[c, d])
>>> a = Node("a", children=[b])
**Node Creation**
Node can be created by instantiating a `Node` 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
>>> a = Node.from_dict({"name": "a", "age": 90})
**Node Attributes**
These are node attributes that have getter and/or setter methods.
Get and set `Node` configuration
1. ``sep``: Get/set separator for path name
Get `Node` configuration
1. ``node_name``: Get node name, without accessing `name` directly
2. ``path_name``: Get path name from root, separated by `sep`
----
"""
def __init__(self, name: str = "", **kwargs):
self.name = name
self._sep: str = "/"
super().__init__(**kwargs)
if not self.node_name:
raise TreeError("Node must have a `name` attribute")
@property
def node_name(self) -> str:
"""Get node name
Returns:
(str)
"""
return self.name
@property
def sep(self) -> str:
"""Get separator, gets from root node
Returns:
(str)
"""
if self.is_root:
return self._sep
return self.parent.sep
@sep.setter
def sep(self, value: str):
"""Set separator, affects root node
Args:
value (str): separator to replace default separator
"""
self.root._sep = value
@property
def path_name(self) -> str:
"""Get path name, separated by self.sep
Returns:
(str)
"""
if self.is_root:
return f"{self.sep}{self.name}"
return f"{self.parent.path_name}{self.sep}{self.name}"
def __pre_assign_children(self, new_children: List):
"""Custom method to check before attaching children
Can be overriden with `_Node__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 `_Node__post_assign_children()`
Args:
new_children (List[Self]): new children to be added
"""
pass
def __pre_assign_parent(self, new_parent):
"""Custom method to check before attaching parent
Can be overriden with `_Node__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 `_Node__post_assign_parent()`
Args:
new_parent (Self): new parent to be added
"""
pass
def _BaseNode__pre_assign_parent(self, new_parent):
"""Do not allow duplicate nodes of same path
Args:
new_parent (Self): new parent to be added
"""
self.__pre_assign_parent(new_parent)
if new_parent is not None:
if any(
child.node_name == self.node_name and child is not self
for child in new_parent.children
):
raise TreeError(
f"Error: Duplicate node with same path\n"
f"There exist a node with same path {new_parent.path_name}{self.sep}{self.node_name}"
)
def _BaseNode__post_assign_parent(self, new_parent):
"""No rules
Args:
new_parent (Self): new parent to be added
"""
self.__post_assign_parent(new_parent)
def _BaseNode__pre_assign_children(self, new_children: List):
"""Do not allow duplicate nodes of same path
Args:
new_children (List[Self]): new children to be added
"""
self.__pre_assign_children(new_children)
children_names = [node.node_name for node in new_children]
duplicated_names = [
item[0] for item in Counter(children_names).items() if item[1] > 1
]
if len(duplicated_names):
duplicated_names = " and ".join(
[f"{self.path_name}{self.sep}{name}" for name in duplicated_names]
)
raise TreeError(
f"Error: Duplicate node with same path\n"
f"Attempting to add nodes same path {duplicated_names}"
)
def _BaseNode__post_assign_children(self, new_children: List):
"""No rules
Args:
new_children (List[Self]): new children to be added
"""
self.__post_assign_children(new_children)
def __repr__(self):
class_name = self.__class__.__name__
node_dict = self.describe(exclude_prefix="_", exclude_attributes=["name"])
node_description = ", ".join([f"{k}={v}" for k, v in node_dict])
return f"{class_name}({self.path_name}, {node_description})"