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,68 @@
__version__ = "0.7.2"
from bigtree.binarytree.construct import list_to_binarytree
from bigtree.dag.construct import dataframe_to_dag, dict_to_dag, list_to_dag
from bigtree.dag.export import dag_to_dataframe, dag_to_dict, dag_to_dot, dag_to_list
from bigtree.node.basenode import BaseNode
from bigtree.node.binarynode import BinaryNode
from bigtree.node.dagnode import DAGNode
from bigtree.node.node import Node
from bigtree.tree.construct import (
add_dataframe_to_tree_by_name,
add_dataframe_to_tree_by_path,
add_dict_to_tree_by_name,
add_dict_to_tree_by_path,
add_path_to_tree,
dataframe_to_tree,
dataframe_to_tree_by_relation,
dict_to_tree,
list_to_tree,
list_to_tree_by_relation,
nested_dict_to_tree,
str_to_tree,
)
from bigtree.tree.export import (
print_tree,
tree_to_dataframe,
tree_to_dict,
tree_to_dot,
tree_to_nested_dict,
tree_to_pillow,
yield_tree,
)
from bigtree.tree.helper import clone_tree, get_tree_diff, prune_tree
from bigtree.tree.modify import (
copy_nodes,
copy_nodes_from_tree_to_tree,
copy_or_shift_logic,
shift_nodes,
)
from bigtree.tree.search import (
find,
find_attr,
find_attrs,
find_children,
find_full_path,
find_name,
find_names,
find_path,
find_paths,
findall,
)
from bigtree.utils.exceptions import (
CorruptedTreeError,
DuplicatedNodeError,
LoopError,
NotFoundError,
SearchError,
TreeError,
)
from bigtree.utils.iterators import (
dag_iterator,
inorder_iter,
levelorder_iter,
levelordergroup_iter,
postorder_iter,
preorder_iter,
)
from bigtree.workflows.app_todo import AppToDo

View File

@@ -0,0 +1,50 @@
from typing import List, Type, Union
from bigtree.node.binarynode import BinaryNode
def list_to_binarytree(
heapq_list: List[Union[int, float]], node_type: Type[BinaryNode] = BinaryNode
) -> BinaryNode:
"""Construct tree from list of numbers (int or float) in heapq format.
>>> from bigtree import list_to_binarytree, print_tree, tree_to_dot
>>> nums_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> root = list_to_binarytree(nums_list)
>>> print_tree(root)
1
├── 2
│ ├── 4
│ │ ├── 8
│ │ └── 9
│ └── 5
│ └── 10
└── 3
├── 6
└── 7
>>> graph = tree_to_dot(root, node_colour="gold")
>>> graph.write_png("assets/binarytree.png")
.. image:: https://github.com/kayjan/bigtree/raw/master/assets/binarytree.png
Args:
heapq_list (List[Union[int, float]]): list containing integer node names, ordered in heapq fashion
node_type (Type[BinaryNode]): node type of tree to be created, defaults to BinaryNode
Returns:
(BinaryNode)
"""
if not len(heapq_list):
raise ValueError("Input list does not contain any data, check `heapq_list`")
root = node_type(heapq_list[0])
node_list = [root]
for idx, num in enumerate(heapq_list):
if idx:
if idx % 2:
parent_idx = int((idx - 1) / 2)
else:
parent_idx = int((idx - 2) / 2)
node = node_type(num, parent=node_list[parent_idx])
node_list.append(node)
return root

View File

@@ -0,0 +1,186 @@
from typing import List, Tuple, Type
import numpy as np
import pandas as pd
from bigtree.node.dagnode import DAGNode
__all__ = ["list_to_dag", "dict_to_dag", "dataframe_to_dag"]
def list_to_dag(
relations: List[Tuple[str, str]],
node_type: Type[DAGNode] = DAGNode,
) -> DAGNode:
"""Construct DAG from list of tuple containing parent-child names.
Note that node names must be unique.
>>> from bigtree import list_to_dag, dag_iterator
>>> relations_list = [("a", "c"), ("a", "d"), ("b", "c"), ("c", "d"), ("d", "e")]
>>> dag = list_to_dag(relations_list)
>>> [(parent.node_name, child.node_name) for parent, child in dag_iterator(dag)]
[('a', 'd'), ('c', 'd'), ('d', 'e'), ('a', 'c'), ('b', 'c')]
Args:
relations (list): list containing tuple of parent-child names
node_type (Type[DAGNode]): node type of DAG to be created, defaults to DAGNode
Returns:
(DAGNode)
"""
if not len(relations):
raise ValueError("Input list does not contain any data, check `relations`")
relation_data = pd.DataFrame(relations, columns=["parent", "child"])
return dataframe_to_dag(
relation_data, child_col="child", parent_col="parent", node_type=node_type
)
def dict_to_dag(
relation_attrs: dict,
parent_key: str = "parents",
node_type: Type[DAGNode] = DAGNode,
) -> DAGNode:
"""Construct DAG from nested dictionary, ``key``: child name, ``value``: dict of parent names, attribute name and
attribute value.
Note that node names must be unique.
>>> from bigtree import dict_to_dag, dag_iterator
>>> relation_dict = {
... "a": {"step": 1},
... "b": {"step": 1},
... "c": {"parents": ["a", "b"], "step": 2},
... "d": {"parents": ["a", "c"], "step": 2},
... "e": {"parents": ["d"], "step": 3},
... }
>>> dag = dict_to_dag(relation_dict, parent_key="parents")
>>> [(parent.node_name, child.node_name) for parent, child in dag_iterator(dag)]
[('a', 'd'), ('c', 'd'), ('d', 'e'), ('a', 'c'), ('b', 'c')]
Args:
relation_attrs (dict): dictionary containing node, node parents, and node attribute information,
key: child name, value: dict of parent names, node attribute and attribute value
parent_key (str): key of dictionary to retrieve list of parents name, defaults to "parent"
node_type (Type[DAGNode]): node type of DAG to be created, defaults to DAGNode
Returns:
(DAGNode)
"""
if not len(relation_attrs):
raise ValueError("Dictionary does not contain any data, check `relation_attrs`")
# Convert dictionary to dataframe
data = pd.DataFrame(relation_attrs).T.rename_axis("_tmp_child").reset_index()
assert (
parent_key in data
), f"Parent key {parent_key} not in dictionary, check `relation_attrs` and `parent_key`"
data = data.explode(parent_key)
return dataframe_to_dag(
data,
child_col="_tmp_child",
parent_col=parent_key,
node_type=node_type,
)
def dataframe_to_dag(
data: pd.DataFrame,
child_col: str = None,
parent_col: str = None,
attribute_cols: list = [],
node_type: Type[DAGNode] = DAGNode,
) -> DAGNode:
"""Construct DAG from pandas DataFrame.
Note that node names must be unique.
`child_col` and `parent_col` specify columns for child name and parent name to construct DAG.
`attribute_cols` specify columns for node attribute for child name
If columns are not specified, `child_col` takes first column, `parent_col` takes second column, and all other
columns are `attribute_cols`.
>>> import pandas as pd
>>> from bigtree import dataframe_to_dag, dag_iterator
>>> relation_data = pd.DataFrame([
... ["a", None, 1],
... ["b", None, 1],
... ["c", "a", 2],
... ["c", "b", 2],
... ["d", "a", 2],
... ["d", "c", 2],
... ["e", "d", 3],
... ],
... columns=["child", "parent", "step"]
... )
>>> dag = dataframe_to_dag(relation_data)
>>> [(parent.node_name, child.node_name) for parent, child in dag_iterator(dag)]
[('a', 'd'), ('c', 'd'), ('d', 'e'), ('a', 'c'), ('b', 'c')]
Args:
data (pandas.DataFrame): data containing path and node attribute information
child_col (str): column of data containing child name information, defaults to None
if not set, it will take the first column of data
parent_col (str): column of data containing parent name information, defaults to None
if not set, it will take the second column of data
attribute_cols (list): columns of data containing child node attribute information,
if not set, it will take all columns of data except `child_col` and `parent_col`
node_type (Type[DAGNode]): node type of DAG to be created, defaults to DAGNode
Returns:
(DAGNode)
"""
if not len(data.columns):
raise ValueError("Data does not contain any columns, check `data`")
if not len(data):
raise ValueError("Data does not contain any rows, check `data`")
if not child_col:
child_col = data.columns[0]
if not parent_col:
parent_col = data.columns[1]
if not len(attribute_cols):
attribute_cols = list(data.columns)
attribute_cols.remove(child_col)
attribute_cols.remove(parent_col)
data_check = data.copy()[[child_col] + attribute_cols].drop_duplicates()
_duplicate_check = (
data_check[child_col]
.value_counts()
.to_frame("counts")
.rename_axis(child_col)
.reset_index()
)
_duplicate_check = _duplicate_check[_duplicate_check["counts"] > 1]
if len(_duplicate_check):
raise ValueError(
f"There exists duplicate child name with different attributes\nCheck {_duplicate_check}"
)
if np.any(data[child_col].isnull()):
raise ValueError(f"Child name cannot be empty, check {child_col}")
node_dict = dict()
parent_node = None
for row in data.reset_index(drop=True).to_dict(orient="index").values():
child_name = row[child_col]
parent_name = row[parent_col]
node_attrs = row.copy()
del node_attrs[child_col]
del node_attrs[parent_col]
node_attrs = {k: v for k, v in node_attrs.items() if not pd.isnull(v)}
child_node = node_dict.get(child_name)
if not child_node:
child_node = node_type(child_name)
node_dict[child_name] = child_node
child_node.set_attrs(node_attrs)
if not pd.isnull(parent_name):
parent_node = node_dict.get(parent_name)
if not parent_node:
parent_node = node_type(parent_name)
node_dict[parent_name] = parent_node
child_node.parents = [parent_node]
return parent_node

View File

@@ -0,0 +1,269 @@
from typing import Any, Dict, List, Tuple, Union
import pandas as pd
from bigtree.node.dagnode import DAGNode
from bigtree.utils.iterators import dag_iterator
__all__ = ["dag_to_list", "dag_to_dict", "dag_to_dataframe", "dag_to_dot"]
def dag_to_list(
dag: DAGNode,
) -> List[Tuple[str, str]]:
"""Export DAG to list of tuple containing parent-child names
>>> from bigtree import DAGNode, dag_to_list
>>> a = DAGNode("a", step=1)
>>> b = DAGNode("b", step=1)
>>> c = DAGNode("c", step=2, parents=[a, b])
>>> d = DAGNode("d", step=2, parents=[a, c])
>>> e = DAGNode("e", step=3, parents=[d])
>>> dag_to_list(a)
[('a', 'c'), ('a', 'd'), ('b', 'c'), ('c', 'd'), ('d', 'e')]
Args:
dag (DAGNode): DAG to be exported
Returns:
(List[Tuple[str, str]])
"""
relations = []
for parent_node, child_node in dag_iterator(dag):
relations.append((parent_node.node_name, child_node.node_name))
return relations
def dag_to_dict(
dag: DAGNode,
parent_key: str = "parents",
attr_dict: dict = {},
all_attrs: bool = False,
) -> Dict[str, Any]:
"""Export tree to dictionary.
Exported dictionary will have key as child name, and parent names and node attributes as a nested dictionary.
>>> from bigtree import DAGNode, dag_to_dict
>>> a = DAGNode("a", step=1)
>>> b = DAGNode("b", step=1)
>>> c = DAGNode("c", step=2, parents=[a, b])
>>> d = DAGNode("d", step=2, parents=[a, c])
>>> e = DAGNode("e", step=3, parents=[d])
>>> dag_to_dict(a, parent_key="parent", attr_dict={"step": "step no."})
{'a': {'step no.': 1}, 'c': {'parent': ['a', 'b'], 'step no.': 2}, 'd': {'parent': ['a', 'c'], 'step no.': 2}, 'b': {'step no.': 1}, 'e': {'parent': ['d'], 'step no.': 3}}
Args:
dag (DAGNode): DAG to be exported
parent_key (str): dictionary key for `node.parent.node_name`, defaults to `parents`
attr_dict (dict): dictionary mapping node attributes to dictionary key,
key: node attributes, value: corresponding dictionary key, optional
all_attrs (bool): indicator whether to retrieve all `Node` attributes
Returns:
(dict)
"""
dag = dag.copy()
data_dict = {}
for parent_node, child_node in dag_iterator(dag):
if parent_node.is_root:
data_parent = {}
if all_attrs:
data_parent.update(
parent_node.describe(
exclude_attributes=["name"], exclude_prefix="_"
)
)
else:
for k, v in attr_dict.items():
data_parent[v] = parent_node.get_attr(k)
data_dict[parent_node.node_name] = data_parent
if data_dict.get(child_node.node_name):
data_dict[child_node.node_name][parent_key].append(parent_node.node_name)
else:
data_child = {parent_key: [parent_node.node_name]}
if all_attrs:
data_child.update(
child_node.describe(exclude_attributes=["name"], exclude_prefix="_")
)
else:
for k, v in attr_dict.items():
data_child[v] = child_node.get_attr(k)
data_dict[child_node.node_name] = data_child
return data_dict
def dag_to_dataframe(
dag: DAGNode,
name_col: str = "name",
parent_col: str = "parent",
attr_dict: dict = {},
all_attrs: bool = False,
) -> pd.DataFrame:
"""Export DAG to pandas DataFrame.
>>> from bigtree import DAGNode, dag_to_dataframe
>>> a = DAGNode("a", step=1)
>>> b = DAGNode("b", step=1)
>>> c = DAGNode("c", step=2, parents=[a, b])
>>> d = DAGNode("d", step=2, parents=[a, c])
>>> e = DAGNode("e", step=3, parents=[d])
>>> dag_to_dataframe(a, name_col="name", parent_col="parent", attr_dict={"step": "step no."})
name parent step no.
0 a None 1
1 c a 2
2 d a 2
3 b None 1
4 c b 2
5 d c 2
6 e d 3
Args:
dag (DAGNode): DAG to be exported
name_col (str): column name for `node.node_name`, defaults to 'name'
parent_col (str): column name for `node.parent.node_name`, defaults to 'parent'
attr_dict (dict): dictionary mapping node attributes to column name,
key: node attributes, value: corresponding column in dataframe, optional
all_attrs (bool): indicator whether to retrieve all `Node` attributes
Returns:
(pd.DataFrame)
"""
dag = dag.copy()
data_list = []
for parent_node, child_node in dag_iterator(dag):
if parent_node.is_root:
data_parent = {name_col: parent_node.node_name, parent_col: None}
if all_attrs:
data_parent.update(
parent_node.describe(
exclude_attributes=["name"], exclude_prefix="_"
)
)
else:
for k, v in attr_dict.items():
data_parent[v] = parent_node.get_attr(k)
data_list.append(data_parent)
data_child = {name_col: child_node.node_name, parent_col: parent_node.node_name}
if all_attrs:
data_child.update(
child_node.describe(exclude_attributes=["name"], exclude_prefix="_")
)
else:
for k, v in attr_dict.items():
data_child[v] = child_node.get_attr(k)
data_list.append(data_child)
return pd.DataFrame(data_list).drop_duplicates().reset_index(drop=True)
def dag_to_dot(
dag: Union[DAGNode, List[DAGNode]],
rankdir: str = "TB",
bg_colour: str = None,
node_colour: str = None,
edge_colour: str = None,
node_attr: str = None,
edge_attr: str = None,
):
r"""Export DAG tree or list of DAG trees to image.
Note that node names must be unique.
Posible node attributes include style, fillcolor, shape.
>>> from bigtree import DAGNode, dag_to_dot
>>> a = DAGNode("a", step=1)
>>> b = DAGNode("b", step=1)
>>> c = DAGNode("c", step=2, parents=[a, b])
>>> d = DAGNode("d", step=2, parents=[a, c])
>>> e = DAGNode("e", step=3, parents=[d])
>>> dag_graph = dag_to_dot(a)
Export to image, dot file, etc.
>>> dag_graph.write_png("tree_dag.png")
>>> dag_graph.write_dot("tree_dag.dot")
Export to string
>>> dag_graph.to_string()
'strict digraph G {\nrankdir=TB;\nc [label=c];\na [label=a];\na -> c;\nd [label=d];\na [label=a];\na -> d;\nc [label=c];\nb [label=b];\nb -> c;\nd [label=d];\nc [label=c];\nc -> d;\ne [label=e];\nd [label=d];\nd -> e;\n}\n'
Args:
dag (Union[DAGNode, List[DAGNode]]): DAG or list of DAGs to be exported
rankdir (str): set direction of graph layout, defaults to 'TB', can be 'BT, 'LR', 'RL'
bg_colour (str): background color of image, defaults to None
node_colour (str): fill colour of nodes, defaults to None
edge_colour (str): colour of edges, defaults to None
node_attr (str): node attribute for style, overrides node_colour, defaults to None
Possible node attributes include {"style": "filled", "fillcolor": "gold"}
edge_attr (str): edge attribute for style, overrides edge_colour, defaults to None
Possible edge attributes include {"style": "bold", "label": "edge label", "color": "black"}
Returns:
(pydot.Dot)
"""
try:
import pydot
except ImportError: # pragma: no cover
raise ImportError(
"pydot not available. Please perform a\n\npip install 'bigtree[image]'\n\nto install required dependencies"
)
# Get style
if bg_colour:
graph_style = dict(bgcolor=bg_colour)
else:
graph_style = dict()
if node_colour:
node_style = dict(style="filled", fillcolor=node_colour)
else:
node_style = dict()
if edge_colour:
edge_style = dict(color=edge_colour)
else:
edge_style = dict()
_graph = pydot.Dot(
graph_type="digraph", strict=True, rankdir=rankdir, **graph_style
)
if not isinstance(dag, list):
dag = [dag]
for _dag in dag:
if not isinstance(_dag, DAGNode):
raise ValueError(
"Tree should be of type `DAGNode`, or inherit from `DAGNode`"
)
_dag = _dag.copy()
for parent_node, child_node in dag_iterator(_dag):
child_name = child_node.name
child_node_style = node_style.copy()
if node_attr and child_node.get_attr(node_attr):
child_node_style.update(child_node.get_attr(node_attr))
if edge_attr:
edge_style.update(child_node.get_attr(edge_attr))
pydot_child = pydot.Node(
name=child_name, label=child_name, **child_node_style
)
_graph.add_node(pydot_child)
parent_name = parent_node.name
parent_node_style = node_style.copy()
if node_attr and parent_node.get_attr(node_attr):
parent_node_style.update(parent_node.get_attr(node_attr))
pydot_parent = pydot.Node(
name=parent_name, label=parent_name, **parent_node_style
)
_graph.add_node(pydot_parent)
edge = pydot.Edge(parent_name, child_name, **edge_style)
_graph.add_edge(edge)
return _graph

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})"

View File

@@ -0,0 +1,914 @@
import re
from collections import OrderedDict
from typing import List, Tuple, Type
import numpy as np
import pandas as pd
from bigtree.node.node import Node
from bigtree.tree.export import tree_to_dataframe
from bigtree.tree.search import find_children, find_name
from bigtree.utils.exceptions import DuplicatedNodeError, TreeError
__all__ = [
"add_path_to_tree",
"add_dict_to_tree_by_path",
"add_dict_to_tree_by_name",
"add_dataframe_to_tree_by_path",
"add_dataframe_to_tree_by_name",
"str_to_tree",
"list_to_tree",
"list_to_tree_by_relation",
"dict_to_tree",
"nested_dict_to_tree",
"dataframe_to_tree",
"dataframe_to_tree_by_relation",
]
def add_path_to_tree(
tree: Node,
path: str,
sep: str = "/",
duplicate_name_allowed: bool = True,
node_attrs: dict = {},
) -> Node:
"""Add nodes and attributes to existing tree *in-place*, return node of added path.
Adds to existing tree from list of path strings.
Path should contain `Node` name, separated by `sep`.
- For example: Path string "a/b" refers to Node("b") with parent Node("a").
- Path separator `sep` is for the input `path` and can be different from that of existing tree.
Path can start from root node `name`, or start with `sep`.
- For example: Path string can be "/a/b" or "a/b", if sep is "/".
All paths should start from the same root node.
- For example: Path strings should be "a/b", "a/c", "a/b/d" etc. and should not start with another root node.
>>> from bigtree import add_path_to_tree, print_tree
>>> root = Node("a")
>>> add_path_to_tree(root, "a/b/c")
Node(/a/b/c, )
>>> print_tree(root)
a
└── b
└── c
Args:
tree (Node): existing tree
path (str): path to be added to tree
sep (str): path separator for input `path`
duplicate_name_allowed (bool): indicator if nodes with duplicated `Node` name is allowed, defaults to True
node_attrs (dict): attributes to add to node, key: attribute name, value: attribute value, optional
Returns:
(Node)
"""
if not len(path):
raise ValueError("Path is empty, check `path`")
tree_root = tree.root
tree_sep = tree_root.sep
node_type = tree_root.__class__
branch = path.lstrip(sep).rstrip(sep).split(sep)
if branch[0] != tree_root.node_name:
raise TreeError(
f"Error: Path does not have same root node, expected {tree_root.node_name}, received {branch[0]}\n"
f"Check your input paths or verify that path separator `sep` is set correctly"
)
# Grow tree
node = tree_root
parent_node = tree_root
for idx in range(1, len(branch)):
node_name = branch[idx]
node_path = tree_sep.join(branch[: idx + 1])
if not duplicate_name_allowed:
node = find_name(tree_root, node_name)
if node and not node.path_name.endswith(node_path):
raise DuplicatedNodeError(
f"Node {node_name} already exists, try setting `duplicate_name_allowed` to True "
f"to allow `Node` with same node name"
)
else:
node = find_children(parent_node, node_name)
if not node:
node = node_type(branch[idx])
node.parent = parent_node
parent_node = node
node.set_attrs(node_attrs)
return node
def add_dict_to_tree_by_path(
tree: Node,
path_attrs: dict,
sep: str = "/",
duplicate_name_allowed: bool = True,
) -> Node:
"""Add nodes and attributes to tree *in-place*, return root of tree.
Adds to existing tree from nested dictionary, ``key``: path, ``value``: dict of attribute name and attribute value.
Path should contain `Node` name, separated by `sep`.
- For example: Path string "a/b" refers to Node("b") with parent Node("a").
- Path separator `sep` is for the input `path` and can be different from that of existing tree.
Path can start from root node `name`, or start with `sep`.
- For example: Path string can be "/a/b" or "a/b", if sep is "/".
All paths should start from the same root node.
- For example: Path strings should be "a/b", "a/c", "a/b/d" etc. and should not start with another root node.
>>> from bigtree import Node, add_dict_to_tree_by_path, print_tree
>>> root = Node("a")
>>> path_dict = {
... "a": {"age": 90},
... "a/b": {"age": 65},
... "a/c": {"age": 60},
... "a/b/d": {"age": 40},
... "a/b/e": {"age": 35},
... "a/c/f": {"age": 38},
... "a/b/e/g": {"age": 10},
... "a/b/e/h": {"age": 6},
... }
>>> root = add_dict_to_tree_by_path(root, path_dict)
>>> print_tree(root)
a
├── b
│ ├── d
│ └── e
│ ├── g
│ └── h
└── c
└── f
Args:
tree (Node): existing tree
path_attrs (dict): dictionary containing node path and attribute information,
key: node path, value: dict of node attribute name and attribute value
sep (str): path separator for input `path_attrs`
duplicate_name_allowed (bool): indicator if nodes with duplicated `Node` name is allowed, defaults to True
Returns:
(Node)
"""
if not len(path_attrs):
raise ValueError("Dictionary does not contain any data, check `path_attrs`")
tree_root = tree.root
for k, v in path_attrs.items():
add_path_to_tree(
tree_root,
k,
sep=sep,
duplicate_name_allowed=duplicate_name_allowed,
node_attrs=v,
)
return tree_root
def add_dict_to_tree_by_name(
tree: Node, path_attrs: dict, join_type: str = "left"
) -> Node:
"""Add attributes to tree, return *new* root of tree.
Adds to existing tree from nested dictionary, ``key``: name, ``value``: dict of attribute name and attribute value.
Function can return all existing tree nodes or only tree nodes that are in the input dictionary keys.
Input dictionary keys that are not existing node names will be ignored.
Note that if multiple nodes have the same name, attributes will be added to all nodes sharing same name.
>>> from bigtree import Node, add_dict_to_tree_by_name, print_tree
>>> root = Node("a")
>>> b = Node("b", parent=root)
>>> name_dict = {
... "a": {"age": 90},
... "b": {"age": 65},
... }
>>> root = add_dict_to_tree_by_name(root, name_dict)
>>> print_tree(root, attr_list=["age"])
a [age=90]
└── b [age=65]
Args:
tree (Node): existing tree
path_attrs (dict): dictionary containing node name and attribute information,
key: node name, value: dict of node attribute name and attribute value
join_type (str): join type with attribute, default of 'left' takes existing tree nodes,
if join_type is set to 'inner' it will only take tree nodes that are in `path_attrs` key and drop others
Returns:
(Node)
"""
if join_type not in ["inner", "left"]:
raise ValueError("`join_type` must be one of 'inner' or 'left'")
if not len(path_attrs):
raise ValueError("Dictionary does not contain any data, check `path_attrs`")
# Convert dictionary to dataframe
data = pd.DataFrame(path_attrs).T.rename_axis("NAME").reset_index()
return add_dataframe_to_tree_by_name(tree, data=data, join_type=join_type)
def add_dataframe_to_tree_by_path(
tree: Node,
data: pd.DataFrame,
path_col: str = "",
attribute_cols: list = [],
sep: str = "/",
duplicate_name_allowed: bool = True,
) -> Node:
"""Add nodes and attributes to tree *in-place*, return root of tree.
`path_col` and `attribute_cols` specify columns for node path and attributes to add to existing tree.
If columns are not specified, `path_col` takes first column and all other columns are `attribute_cols`
Path in path column should contain `Node` name, separated by `sep`.
- For example: Path string "a/b" refers to Node("b") with parent Node("a").
- Path separator `sep` is for the input `path_col` and can be different from that of existing tree.
Path in path column can start from root node `name`, or start with `sep`.
- For example: Path string can be "/a/b" or "a/b", if sep is "/".
All paths should start from the same root node.
- For example: Path strings should be "a/b", "a/c", "a/b/d" etc. and should not start with another root node.
>>> import pandas as pd
>>> from bigtree import add_dataframe_to_tree_by_path, print_tree
>>> root = Node("a")
>>> path_data = pd.DataFrame([
... ["a", 90],
... ["a/b", 65],
... ["a/c", 60],
... ["a/b/d", 40],
... ["a/b/e", 35],
... ["a/c/f", 38],
... ["a/b/e/g", 10],
... ["a/b/e/h", 6],
... ],
... columns=["PATH", "age"]
... )
>>> root = add_dataframe_to_tree_by_path(root, path_data)
>>> print_tree(root, attr_list=["age"])
a [age=90]
├── b [age=65]
│ ├── d [age=40]
│ └── e [age=35]
│ ├── g [age=10]
│ └── h [age=6]
└── c [age=60]
└── f [age=38]
Args:
tree (Node): existing tree
data (pandas.DataFrame): data containing node path and attribute information
path_col (str): column of data containing `path_name` information,
if not set, it will take the first column of data
attribute_cols (list): columns of data containing node attribute information,
if not set, it will take all columns of data except `path_col`
sep (str): path separator for input `path_col`
duplicate_name_allowed (bool): indicator if nodes with duplicated `Node` name is allowed, defaults to True
Returns:
(Node)
"""
if not len(data.columns):
raise ValueError("Data does not contain any columns, check `data`")
if not len(data):
raise ValueError("Data does not contain any rows, check `data`")
if not path_col:
path_col = data.columns[0]
if not len(attribute_cols):
attribute_cols = list(data.columns)
attribute_cols.remove(path_col)
tree_root = tree.root
data[path_col] = data[path_col].str.lstrip(sep).str.rstrip(sep)
data2 = data.copy()[[path_col] + attribute_cols].astype(str).drop_duplicates()
_duplicate_check = (
data2[path_col]
.value_counts()
.to_frame("counts")
.rename_axis(path_col)
.reset_index()
)
_duplicate_check = _duplicate_check[_duplicate_check["counts"] > 1]
if len(_duplicate_check):
raise ValueError(
f"There exists duplicate path with different attributes\nCheck {_duplicate_check}"
)
for row in data.to_dict(orient="index").values():
node_attrs = row.copy()
del node_attrs[path_col]
node_attrs = {k: v for k, v in node_attrs.items() if not np.all(pd.isnull(v))}
add_path_to_tree(
tree_root,
row[path_col],
sep=sep,
duplicate_name_allowed=duplicate_name_allowed,
node_attrs=node_attrs,
)
return tree_root
def add_dataframe_to_tree_by_name(
tree: Node,
data: pd.DataFrame,
name_col: str = "",
attribute_cols: list = [],
join_type: str = "left",
):
"""Add attributes to tree, return *new* root of tree.
`name_col` and `attribute_cols` specify columns for node name and attributes to add to existing tree.
If columns are not specified, the first column will be taken as name column and all other columns as attributes.
Function can return all existing tree nodes or only tree nodes that are in the input data node names.
Input data node names that are not existing node names will be ignored.
Note that if multiple nodes have the same name, attributes will be added to all nodes sharing same name.
>>> import pandas as pd
>>> from bigtree import add_dataframe_to_tree_by_name, print_tree
>>> root = Node("a")
>>> b = Node("b", parent=root)
>>> name_data = pd.DataFrame([
... ["a", 90],
... ["b", 65],
... ],
... columns=["NAME", "age"]
... )
>>> root = add_dataframe_to_tree_by_name(root, name_data)
>>> print_tree(root, attr_list=["age"])
a [age=90]
└── b [age=65]
Args:
tree (Node): existing tree
data (pandas.DataFrame): data containing node name and attribute information
name_col (str): column of data containing `name` information,
if not set, it will take the first column of data
attribute_cols (list): column(s) of data containing node attribute information,
if not set, it will take all columns of data except path_col
join_type (str): join type with attribute, default of 'left' takes existing tree nodes,
if join_type is set to 'inner' it will only take tree nodes with attributes and drop the other nodes
Returns:
(Node)
"""
if join_type not in ["inner", "left"]:
raise ValueError("`join_type` must be one of 'inner' or 'left'")
if not len(data.columns):
raise ValueError("Data does not contain any columns, check `data`")
if not len(data):
raise ValueError("Data does not contain any rows, check `data`")
if not name_col:
name_col = data.columns[0]
if not len(attribute_cols):
attribute_cols = list(data.columns)
attribute_cols.remove(name_col)
# Attribute data
path_col = "PATH"
data2 = data.copy()[[name_col] + attribute_cols].astype(str).drop_duplicates()
_duplicate_check = (
data2[name_col]
.value_counts()
.to_frame("counts")
.rename_axis(name_col)
.reset_index()
)
_duplicate_check = _duplicate_check[_duplicate_check["counts"] > 1]
if len(_duplicate_check):
raise ValueError(
f"There exists duplicate name with different attributes\nCheck {_duplicate_check}"
)
# Tree data
tree_root = tree.root
sep = tree_root.sep
node_type = tree_root.__class__
data_tree = tree_to_dataframe(
tree_root, name_col=name_col, path_col=path_col, all_attrs=True
)
common_cols = list(set(data_tree.columns).intersection(attribute_cols))
data_tree = data_tree.drop(columns=common_cols)
# Attribute data
data_tree_attrs = pd.merge(data_tree, data, on=name_col, how=join_type)
data_tree_attrs = data_tree_attrs.drop(columns=name_col)
return dataframe_to_tree(
data_tree_attrs, path_col=path_col, sep=sep, node_type=node_type
)
def str_to_tree(
tree_string: str,
tree_prefix_list: List[str] = [],
node_type: Type[Node] = Node,
) -> Node:
r"""Construct tree from tree string
>>> from bigtree import str_to_tree, print_tree
>>> tree_str = 'a\n├── b\n│ ├── d\n│ └── e\n│ ├── g\n│ └── h\n└── c\n └── f'
>>> root = str_to_tree(tree_str, tree_prefix_list=["├──", "└──"])
>>> print_tree(root)
a
├── b
│ ├── d
│ └── e
│ ├── g
│ └── h
└── c
└── f
Args:
tree_string (str): String to construct tree
tree_prefix_list (list): List of prefix to mark the end of tree branch/stem and start of node name, optional.
If not specified, it will infer unicode characters and whitespace as prefix.
node_type (Type[Node]): node type of tree to be created, defaults to Node
Returns:
(Node)
"""
tree_string = tree_string.strip("\n")
if not len(tree_string):
raise ValueError("Tree string does not contain any data, check `tree_string`")
tree_list = tree_string.split("\n")
tree_root = node_type(tree_list[0])
# Infer prefix length
prefix_length = None
cur_parent = tree_root
for node_str in tree_list[1:]:
if len(tree_prefix_list):
node_name = re.split("|".join(tree_prefix_list), node_str)[-1].lstrip()
else:
node_name = node_str.encode("ascii", "ignore").decode("ascii").lstrip()
# Find node parent
if not prefix_length:
prefix_length = node_str.index(node_name)
if not prefix_length:
raise ValueError(
f"Invalid prefix, prefix should be unicode character or whitespace, "
f"otherwise specify one or more prefixes in `tree_prefix_list`, check: {node_str}"
)
node_prefix_length = node_str.index(node_name)
if node_prefix_length % prefix_length:
raise ValueError(
f"Tree string have different prefix length, check branch: {node_str}"
)
while cur_parent.depth > node_prefix_length / prefix_length:
cur_parent = cur_parent.parent
# Link node
child_node = node_type(node_name)
child_node.parent = cur_parent
cur_parent = child_node
return tree_root
def list_to_tree(
paths: list,
sep: str = "/",
duplicate_name_allowed: bool = True,
node_type: Type[Node] = Node,
) -> Node:
"""Construct tree from list of path strings.
Path should contain `Node` name, separated by `sep`.
- For example: Path string "a/b" refers to Node("b") with parent Node("a").
Path can start from root node `name`, or start with `sep`.
- For example: Path string can be "/a/b" or "a/b", if sep is "/".
All paths should start from the same root node.
- For example: Path strings should be "a/b", "a/c", "a/b/d" etc. and should not start with another root node.
>>> from bigtree import list_to_tree, print_tree
>>> path_list = ["a/b", "a/c", "a/b/d", "a/b/e", "a/c/f", "a/b/e/g", "a/b/e/h"]
>>> root = list_to_tree(path_list)
>>> print_tree(root)
a
├── b
│ ├── d
│ └── e
│ ├── g
│ └── h
└── c
└── f
Args:
paths (list): list containing path strings
sep (str): path separator for input `paths` and created tree, defaults to `/`
duplicate_name_allowed (bool): indicator if nodes with duplicated `Node` name is allowed, defaults to True
node_type (Type[Node]): node type of tree to be created, defaults to Node
Returns:
(Node)
"""
if not len(paths):
raise ValueError("Path list does not contain any data, check `paths`")
# Remove duplicates
paths = list(OrderedDict.fromkeys(paths))
# Construct root node
root_name = paths[0].lstrip(sep).split(sep)[0]
root_node = node_type(root_name)
root_node.sep = sep
for path in paths:
add_path_to_tree(
root_node, path, sep=sep, duplicate_name_allowed=duplicate_name_allowed
)
root_node.sep = sep
return root_node
def list_to_tree_by_relation(
relations: List[Tuple[str, str]],
node_type: Type[Node] = Node,
) -> Node:
"""Construct tree from list of tuple containing parent-child names.
Note that node names must be unique since tree is created from parent-child names,
except for leaf nodes - names of leaf nodes may be repeated as there is no confusion.
>>> from bigtree import list_to_tree_by_relation, print_tree
>>> relations_list = [("a", "b"), ("a", "c"), ("b", "d"), ("b", "e"), ("c", "f"), ("e", "g"), ("e", "h")]
>>> root = list_to_tree_by_relation(relations_list)
>>> print_tree(root)
a
├── b
│ ├── d
│ └── e
│ ├── g
│ └── h
└── c
└── f
Args:
relations (list): list containing tuple containing parent-child names
node_type (Type[Node]): node type of tree to be created, defaults to Node
Returns:
(Node)
"""
if not len(relations):
raise ValueError("Path list does not contain any data, check `relations`")
relation_data = pd.DataFrame(relations, columns=["parent", "child"])
return dataframe_to_tree_by_relation(
relation_data, child_col="child", parent_col="parent", node_type=node_type
)
def dict_to_tree(
path_attrs: dict,
sep: str = "/",
duplicate_name_allowed: bool = True,
node_type: Type[Node] = Node,
) -> Node:
"""Construct tree from nested dictionary using path,
``key``: path, ``value``: dict of attribute name and attribute value.
Path should contain `Node` name, separated by `sep`.
- For example: Path string "a/b" refers to Node("b") with parent Node("a").
Path can start from root node `name`, or start with `sep`.
- For example: Path string can be "/a/b" or "a/b", if sep is "/".
All paths should start from the same root node.
- For example: Path strings should be "a/b", "a/c", "a/b/d" etc. and should not start with another root node.
>>> from bigtree import dict_to_tree, print_tree
>>> path_dict = {
... "a": {"age": 90},
... "a/b": {"age": 65},
... "a/c": {"age": 60},
... "a/b/d": {"age": 40},
... "a/b/e": {"age": 35},
... "a/c/f": {"age": 38},
... "a/b/e/g": {"age": 10},
... "a/b/e/h": {"age": 6},
... }
>>> root = dict_to_tree(path_dict)
>>> print_tree(root, attr_list=["age"])
a [age=90]
├── b [age=65]
│ ├── d [age=40]
│ └── e [age=35]
│ ├── g [age=10]
│ └── h [age=6]
└── c [age=60]
└── f [age=38]
Args:
path_attrs (dict): dictionary containing path and node attribute information,
key: path, value: dict of tree attribute and attribute value
sep (str): path separator of input `path_attrs` and created tree, defaults to `/`
duplicate_name_allowed (bool): indicator if nodes with duplicated `Node` name is allowed, defaults to True
node_type (Type[Node]): node type of tree to be created, defaults to Node
Returns:
(Node)
"""
if not len(path_attrs):
raise ValueError("Dictionary does not contain any data, check `path_attrs`")
# Convert dictionary to dataframe
data = pd.DataFrame(path_attrs).T.rename_axis("PATH").reset_index()
return dataframe_to_tree(
data,
sep=sep,
duplicate_name_allowed=duplicate_name_allowed,
node_type=node_type,
)
def nested_dict_to_tree(
node_attrs: dict,
name_key: str = "name",
child_key: str = "children",
node_type: Type[Node] = Node,
) -> Node:
"""Construct tree from nested recursive dictionary.
- ``key``: `name_key`, `child_key`, or any attributes key.
- ``value`` of `name_key` (str): node name.
- ``value`` of `child_key` (list): list of dict containing `name_key` and `child_key` (recursive).
>>> from bigtree import nested_dict_to_tree, print_tree
>>> path_dict = {
... "name": "a",
... "age": 90,
... "children": [
... {"name": "b",
... "age": 65,
... "children": [
... {"name": "d", "age": 40},
... {"name": "e", "age": 35, "children": [
... {"name": "g", "age": 10},
... ]},
... ]},
... ],
... }
>>> root = nested_dict_to_tree(path_dict)
>>> print_tree(root, attr_list=["age"])
a [age=90]
└── b [age=65]
├── d [age=40]
└── e [age=35]
└── g [age=10]
Args:
node_attrs (dict): dictionary containing node, children, and node attribute information,
key: `name_key` and `child_key`
value of `name_key` (str): node name
value of `child_key` (list): list of dict containing `name_key` and `child_key` (recursive)
name_key (str): key of node name, value is type str
child_key (str): key of child list, value is type list
node_type (Type[Node]): node type of tree to be created, defaults to Node
Returns:
(Node)
"""
def recursive_add_child(child_dict, parent_node=None):
child_dict = child_dict.copy()
node_name = child_dict.pop(name_key)
node_children = child_dict.pop(child_key, [])
node = node_type(node_name, parent=parent_node, **child_dict)
for _child in node_children:
recursive_add_child(_child, parent_node=node)
return node
root_node = recursive_add_child(node_attrs)
return root_node
def dataframe_to_tree(
data: pd.DataFrame,
path_col: str = "",
attribute_cols: list = [],
sep: str = "/",
duplicate_name_allowed: bool = True,
node_type: Type[Node] = Node,
) -> Node:
"""Construct tree from pandas DataFrame using path, return root of tree.
`path_col` and `attribute_cols` specify columns for node path and attributes to construct tree.
If columns are not specified, `path_col` takes first column and all other columns are `attribute_cols`.
Path in path column can start from root node `name`, or start with `sep`.
- For example: Path string can be "/a/b" or "a/b", if sep is "/".
Path in path column should contain `Node` name, separated by `sep`.
- For example: Path string "a/b" refers to Node("b") with parent Node("a").
All paths should start from the same root node.
- For example: Path strings should be "a/b", "a/c", "a/b/d" etc. and should not start with another root node.
>>> import pandas as pd
>>> from bigtree import dataframe_to_tree, print_tree
>>> path_data = pd.DataFrame([
... ["a", 90],
... ["a/b", 65],
... ["a/c", 60],
... ["a/b/d", 40],
... ["a/b/e", 35],
... ["a/c/f", 38],
... ["a/b/e/g", 10],
... ["a/b/e/h", 6],
... ],
... columns=["PATH", "age"]
... )
>>> root = dataframe_to_tree(path_data)
>>> print_tree(root, attr_list=["age"])
a [age=90]
├── b [age=65]
│ ├── d [age=40]
│ └── e [age=35]
│ ├── g [age=10]
│ └── h [age=6]
└── c [age=60]
└── f [age=38]
Args:
data (pandas.DataFrame): data containing path and node attribute information
path_col (str): column of data containing `path_name` information,
if not set, it will take the first column of data
attribute_cols (list): columns of data containing node attribute information,
if not set, it will take all columns of data except `path_col`
sep (str): path separator of input `path_col` and created tree, defaults to `/`
duplicate_name_allowed (bool): indicator if nodes with duplicated `Node` name is allowed, defaults to True
node_type (Type[Node]): node type of tree to be created, defaults to Node
Returns:
(Node)
"""
if not len(data.columns):
raise ValueError("Data does not contain any columns, check `data`")
if not len(data):
raise ValueError("Data does not contain any rows, check `data`")
if not path_col:
path_col = data.columns[0]
if not len(attribute_cols):
attribute_cols = list(data.columns)
attribute_cols.remove(path_col)
data[path_col] = data[path_col].str.lstrip(sep).str.rstrip(sep)
data2 = data.copy()[[path_col] + attribute_cols].astype(str).drop_duplicates()
_duplicate_check = (
data2[path_col]
.value_counts()
.to_frame("counts")
.rename_axis(path_col)
.reset_index()
)
_duplicate_check = _duplicate_check[_duplicate_check["counts"] > 1]
if len(_duplicate_check):
raise ValueError(
f"There exists duplicate path with different attributes\nCheck {_duplicate_check}"
)
root_name = data[path_col].values[0].split(sep)[0]
root_node = node_type(root_name)
add_dataframe_to_tree_by_path(
root_node,
data,
sep=sep,
duplicate_name_allowed=duplicate_name_allowed,
)
root_node.sep = sep
return root_node
def dataframe_to_tree_by_relation(
data: pd.DataFrame,
child_col: str = "",
parent_col: str = "",
attribute_cols: list = [],
node_type: Type[Node] = Node,
) -> Node:
"""Construct tree from pandas DataFrame using parent and child names, return root of tree.
Note that node names must be unique since tree is created from parent-child names,
except for leaf nodes - names of leaf nodes may be repeated as there is no confusion.
`child_col` and `parent_col` specify columns for child name and parent name to construct tree.
`attribute_cols` specify columns for node attribute for child name
If columns are not specified, `child_col` takes first column, `parent_col` takes second column, and all other
columns are `attribute_cols`.
>>> import pandas as pd
>>> from bigtree import dataframe_to_tree_by_relation, print_tree
>>> relation_data = pd.DataFrame([
... ["a", None, 90],
... ["b", "a", 65],
... ["c", "a", 60],
... ["d", "b", 40],
... ["e", "b", 35],
... ["f", "c", 38],
... ["g", "e", 10],
... ["h", "e", 6],
... ],
... columns=["child", "parent", "age"]
... )
>>> root = dataframe_to_tree_by_relation(relation_data)
>>> print_tree(root, attr_list=["age"])
a [age=90]
├── b [age=65]
│ ├── d [age=40]
│ └── e [age=35]
│ ├── g [age=10]
│ └── h [age=6]
└── c [age=60]
└── f [age=38]
Args:
data (pandas.DataFrame): data containing path and node attribute information
child_col (str): column of data containing child name information, defaults to None
if not set, it will take the first column of data
parent_col (str): column of data containing parent name information, defaults to None
if not set, it will take the second column of data
attribute_cols (list): columns of data containing node attribute information,
if not set, it will take all columns of data except `child_col` and `parent_col`
node_type (Type[Node]): node type of tree to be created, defaults to Node
Returns:
(Node)
"""
if not len(data.columns):
raise ValueError("Data does not contain any columns, check `data`")
if not len(data):
raise ValueError("Data does not contain any rows, check `data`")
if not child_col:
child_col = data.columns[0]
if not parent_col:
parent_col = data.columns[1]
if not len(attribute_cols):
attribute_cols = list(data.columns)
attribute_cols.remove(child_col)
attribute_cols.remove(parent_col)
data_check = data.copy()[[child_col, parent_col]].drop_duplicates()
# Filter for child nodes that are parent of other nodes
data_check = data_check[data_check[child_col].isin(data_check[parent_col])]
_duplicate_check = (
data_check[child_col]
.value_counts()
.to_frame("counts")
.rename_axis(child_col)
.reset_index()
)
_duplicate_check = _duplicate_check[_duplicate_check["counts"] > 1]
if len(_duplicate_check):
raise ValueError(
f"There exists duplicate child with different parent where the child is also a parent node.\n"
f"Duplicated node names should not happen, but can only exist in leaf nodes to avoid confusion.\n"
f"Check {_duplicate_check}"
)
# If parent-child contains None -> root
root_row = data[data[parent_col].isnull()]
root_names = list(root_row[child_col])
if not len(root_names):
root_names = list(set(data[parent_col]) - set(data[child_col]))
if len(root_names) != 1:
raise ValueError(f"Unable to determine root node\nCheck {root_names}")
root_name = root_names[0]
root_node = node_type(root_name)
def retrieve_attr(row):
node_attrs = row.copy()
node_attrs["name"] = node_attrs[child_col]
del node_attrs[child_col]
del node_attrs[parent_col]
_node_attrs = {k: v for k, v in node_attrs.items() if not np.all(pd.isnull(v))}
return _node_attrs
def recursive_create_child(parent_node):
child_rows = data[data[parent_col] == parent_node.node_name]
for row in child_rows.to_dict(orient="index").values():
child_node = node_type(**retrieve_attr(row))
child_node.parent = parent_node
recursive_create_child(child_node)
# Create root node attributes
if len(root_row):
row = list(root_row.to_dict(orient="index").values())[0]
root_node.set_attrs(retrieve_attr(row))
recursive_create_child(root_node)
return root_node

View File

@@ -0,0 +1,831 @@
import collections
from typing import Any, Dict, List, Tuple, Union
import pandas as pd
from bigtree.node.node import Node
from bigtree.tree.search import find_path
from bigtree.utils.iterators import preorder_iter
__all__ = [
"print_tree",
"yield_tree",
"tree_to_dict",
"tree_to_nested_dict",
"tree_to_dataframe",
"tree_to_dot",
"tree_to_pillow",
]
available_styles = {
"ansi": ("| ", "|-- ", "`-- "),
"ascii": ("| ", "|-- ", "+-- "),
"const": ("\u2502 ", "\u251c\u2500\u2500 ", "\u2514\u2500\u2500 "),
"const_bold": ("\u2503 ", "\u2523\u2501\u2501 ", "\u2517\u2501\u2501 "),
"rounded": ("\u2502 ", "\u251c\u2500\u2500 ", "\u2570\u2500\u2500 "),
"double": ("\u2551 ", "\u2560\u2550\u2550 ", "\u255a\u2550\u2550 "),
"custom": ("", "", ""),
}
def print_tree(
tree: Node,
node_name_or_path: str = "",
max_depth: int = None,
attr_list: List[str] = None,
all_attrs: bool = False,
attr_omit_null: bool = True,
attr_bracket: List[str] = ["[", "]"],
style: str = "const",
custom_style: List[str] = [],
):
"""Print tree to console, starting from `tree`.
- Able to select which node to print from, resulting in a subtree, using `node_name_or_path`
- Able to customize for maximum depth to print, using `max_depth`
- Able to choose which attributes to show or show all attributes, using `attr_name_filter` and `all_attrs`
- Able to omit showing of attributes if it is null, using `attr_omit_null`
- Able to customize open and close brackets if attributes are shown, using `attr_bracket`
- Able to customize style, to choose from `ansi`, `ascii`, `const`, `rounded`, `double`, and `custom` style
- Default style is `const` style
- If style is set to custom, user can choose their own style for stem, branch and final stem icons
- Stem, branch, and final stem symbol should have the same number of characters
**Printing tree**
>>> from bigtree import Node, print_tree
>>> root = Node("a", age=90)
>>> b = Node("b", age=65, parent=root)
>>> c = Node("c", age=60, parent=root)
>>> d = Node("d", age=40, parent=b)
>>> e = Node("e", age=35, parent=b)
>>> print_tree(root)
a
├── b
│ ├── d
│ └── e
└── c
**Printing Sub-tree**
>>> print_tree(root, node_name_or_path="b")
b
├── d
└── e
>>> print_tree(root, max_depth=2)
a
├── b
└── c
**Printing Attributes**
>>> print_tree(root, attr_list=["age"])
a [age=90]
├── b [age=65]
│ ├── d [age=40]
│ └── e [age=35]
└── c [age=60]
>>> print_tree(root, attr_list=["age"], attr_bracket=["*(", ")"])
a *(age=90)
├── b *(age=65)
│ ├── d *(age=40)
│ └── e *(age=35)
└── c *(age=60)
**Available Styles**
>>> print_tree(root, style="ansi")
a
|-- b
| |-- d
| `-- e
`-- c
>>> print_tree(root, style="ascii")
a
|-- b
| |-- d
| +-- e
+-- c
>>> print_tree(root, style="const")
a
├── b
│ ├── d
│ └── e
└── c
>>> print_tree(root, style="const_bold")
a
┣━━ b
┃ ┣━━ d
┃ ┗━━ e
┗━━ c
>>> print_tree(root, style="rounded")
a
├── b
│ ├── d
│ ╰── e
╰── c
>>> print_tree(root, style="double")
a
╠══ b
║ ╠══ d
║ ╚══ e
╚══ c
Args:
tree (Node): tree to print
node_name_or_path (str): node to print from, becomes the root node of printing
max_depth (int): maximum depth of tree to print, based on `depth` attribute, optional
attr_list (list): list of node attributes to print, optional
all_attrs (bool): indicator to show all attributes, overrides `attr_list`
attr_omit_null (bool): indicator whether to omit showing of null attributes, defaults to True
attr_bracket (List[str]): open and close bracket for `all_attrs` or `attr_list`
style (str): style of print, defaults to abstract style
custom_style (List[str]): style of stem, branch and final stem, used when `style` is set to 'custom'
"""
for pre_str, fill_str, _node in yield_tree(
tree=tree,
node_name_or_path=node_name_or_path,
max_depth=max_depth,
style=style,
custom_style=custom_style,
):
# Get node_str (node name and attributes)
attr_str = ""
if all_attrs or attr_list:
if len(attr_bracket) != 2:
raise ValueError(
f"Expect open and close brackets in `attr_bracket`, received {attr_bracket}"
)
attr_bracket_open, attr_bracket_close = attr_bracket
if all_attrs:
attrs = _node.describe(exclude_attributes=["name"], exclude_prefix="_")
attr_str_list = [f"{k}={v}" for k, v in attrs]
else:
if attr_omit_null:
attr_str_list = [
f"{attr_name}={_node.get_attr(attr_name)}"
for attr_name in attr_list
if _node.get_attr(attr_name)
]
else:
attr_str_list = [
f"{attr_name}={_node.get_attr(attr_name)}"
for attr_name in attr_list
]
attr_str = ", ".join(attr_str_list)
if attr_str:
attr_str = f" {attr_bracket_open}{attr_str}{attr_bracket_close}"
node_str = f"{_node.node_name}{attr_str}"
print(f"{pre_str}{fill_str}{node_str}")
def yield_tree(
tree: Node,
node_name_or_path: str = "",
max_depth: int = None,
style: str = "const",
custom_style: List[str] = [],
):
"""Generator method for customizing printing of tree, starting from `tree`.
- Able to select which node to print from, resulting in a subtree, using `node_name_or_path`
- Able to customize for maximum depth to print, using `max_depth`
- Able to customize style, to choose from `ansi`, `ascii`, `const`, `rounded`, `double`, and `custom` style
- Default style is `const` style
- If style is set to custom, user can choose their own style for stem, branch and final stem icons
- Stem, branch, and final stem symbol should have the same number of characters
**Printing tree**
>>> from bigtree import Node, print_tree
>>> root = Node("a", age=90)
>>> b = Node("b", age=65, parent=root)
>>> c = Node("c", age=60, parent=root)
>>> d = Node("d", age=40, parent=b)
>>> e = Node("e", age=35, parent=b)
>>> for branch, stem, node in yield_tree(root):
... print(f"{branch}{stem}{node.node_name}")
a
├── b
│ ├── d
│ └── e
└── c
**Printing Sub-tree**
>>> for branch, stem, node in yield_tree(root, node_name_or_path="b"):
... print(f"{branch}{stem}{node.node_name}")
b
├── d
└── e
>>> for branch, stem, node in yield_tree(root, max_depth=2):
... print(f"{branch}{stem}{node.node_name}")
a
├── b
└── c
**Available Styles**
>>> for branch, stem, node in yield_tree(root, style="ansi"):
... print(f"{branch}{stem}{node.node_name}")
a
|-- b
| |-- d
| `-- e
`-- c
>>> for branch, stem, node in yield_tree(root, style="ascii"):
... print(f"{branch}{stem}{node.node_name}")
a
|-- b
| |-- d
| +-- e
+-- c
>>> for branch, stem, node in yield_tree(root, style="const"):
... print(f"{branch}{stem}{node.node_name}")
a
├── b
│ ├── d
│ └── e
└── c
>>> for branch, stem, node in yield_tree(root, style="const_bold"):
... print(f"{branch}{stem}{node.node_name}")
a
┣━━ b
┃ ┣━━ d
┃ ┗━━ e
┗━━ c
>>> for branch, stem, node in yield_tree(root, style="rounded"):
... print(f"{branch}{stem}{node.node_name}")
a
├── b
│ ├── d
│ ╰── e
╰── c
>>> for branch, stem, node in yield_tree(root, style="double"):
... print(f"{branch}{stem}{node.node_name}")
a
╠══ b
║ ╠══ d
║ ╚══ e
╚══ c
**Printing Attributes**
>>> for branch, stem, node in yield_tree(root, style="const"):
... print(f"{branch}{stem}{node.node_name} [age={node.age}]")
a [age=90]
├── b [age=65]
│ ├── d [age=40]
│ └── e [age=35]
└── c [age=60]
Args:
tree (Node): tree to print
node_name_or_path (str): node to print from, becomes the root node of printing, optional
max_depth (int): maximum depth of tree to print, based on `depth` attribute, optional
style (str): style of print, defaults to abstract style
custom_style (List[str]): style of stem, branch and final stem, used when `style` is set to 'custom'
"""
if style not in available_styles.keys():
raise ValueError(
f"Choose one of {available_styles.keys()} style, use `custom` to define own style"
)
tree = tree.copy()
if node_name_or_path:
tree = find_path(tree, node_name_or_path)
if not tree.is_root:
tree.parent = None
# Set style
if style == "custom":
if len(custom_style) != 3:
raise ValueError(
"Custom style selected, please specify the style of stem, branch, and final stem in `custom_style`"
)
style_stem, style_branch, style_stem_final = custom_style
else:
style_stem, style_branch, style_stem_final = available_styles[style]
if not len(style_stem) == len(style_branch) == len(style_stem_final):
raise ValueError(
"`style_stem`, `style_branch`, and `style_stem_final` are of different length"
)
gap_str = " " * len(style_stem)
unclosed_depth = set()
initial_depth = tree.depth
for _node in preorder_iter(tree, max_depth=max_depth):
pre_str = ""
fill_str = ""
if not _node.is_root:
node_depth = _node.depth - initial_depth
# Get fill_str (style_branch or style_stem_final)
if _node.right_sibling:
unclosed_depth.add(node_depth)
fill_str = style_branch
else:
if node_depth in unclosed_depth:
unclosed_depth.remove(node_depth)
fill_str = style_stem_final
# Get pre_str (style_stem, style_branch, style_stem_final, or gap)
pre_str = ""
for _depth in range(1, node_depth):
if _depth in unclosed_depth:
pre_str += style_stem
else:
pre_str += gap_str
yield pre_str, fill_str, _node
def tree_to_dict(
tree: Node,
name_key: str = "name",
parent_key: str = "",
attr_dict: dict = {},
all_attrs: bool = False,
max_depth: int = None,
skip_depth: int = None,
leaf_only: bool = False,
) -> Dict[str, Any]:
"""Export tree to dictionary.
All descendants from `tree` will be exported, `tree` can be the root node or child node of tree.
Exported dictionary will have key as node path, and node attributes as a nested dictionary.
>>> from bigtree import Node, tree_to_dict
>>> root = Node("a", age=90)
>>> b = Node("b", age=65, parent=root)
>>> c = Node("c", age=60, parent=root)
>>> d = Node("d", age=40, parent=b)
>>> e = Node("e", age=35, parent=b)
>>> tree_to_dict(root, name_key="name", parent_key="parent", attr_dict={"age": "person age"})
{'/a': {'name': 'a', 'parent': None, 'person age': 90}, '/a/b': {'name': 'b', 'parent': 'a', 'person age': 65}, '/a/b/d': {'name': 'd', 'parent': 'b', 'person age': 40}, '/a/b/e': {'name': 'e', 'parent': 'b', 'person age': 35}, '/a/c': {'name': 'c', 'parent': 'a', 'person age': 60}}
For a subset of a tree
>>> tree_to_dict(c, name_key="name", parent_key="parent", attr_dict={"age": "person age"})
{'/a/c': {'name': 'c', 'parent': 'a', 'person age': 60}}
Args:
tree (Node): tree to be exported
name_key (str): dictionary key for `node.node_name`, defaults to 'name'
parent_key (str): dictionary key for `node.parent.node_name`, optional
attr_dict (dict): dictionary mapping node attributes to dictionary key,
key: node attributes, value: corresponding dictionary key, optional
all_attrs (bool): indicator whether to retrieve all `Node` attributes
max_depth (int): maximum depth to export tree, optional
skip_depth (int): number of initial depth to skip, optional
leaf_only (bool): indicator to retrieve only information from leaf nodes
Returns:
(dict)
"""
tree = tree.copy()
data_dict = {}
def recursive_append(node):
if node:
if (
(not max_depth or node.depth <= max_depth)
and (not skip_depth or node.depth > skip_depth)
and (not leaf_only or node.is_leaf)
):
data_child = {}
if name_key:
data_child[name_key] = node.node_name
if parent_key:
parent_name = None
if node.parent:
parent_name = node.parent.node_name
data_child[parent_key] = parent_name
if all_attrs:
data_child.update(
dict(
node.describe(
exclude_attributes=["name"], exclude_prefix="_"
)
)
)
else:
for k, v in attr_dict.items():
data_child[v] = node.get_attr(k)
data_dict[node.path_name] = data_child
for _node in node.children:
recursive_append(_node)
recursive_append(tree)
return data_dict
def tree_to_nested_dict(
tree: Node,
name_key: str = "name",
child_key: str = "children",
attr_dict: dict = {},
all_attrs: bool = False,
max_depth: int = None,
) -> Dict[str, Any]:
"""Export tree to nested dictionary.
All descendants from `tree` will be exported, `tree` can be the root node or child node of tree.
Exported dictionary will have key as node attribute names, and children as a nested recursive dictionary.
>>> from bigtree import Node, tree_to_nested_dict
>>> root = Node("a", age=90)
>>> b = Node("b", age=65, parent=root)
>>> c = Node("c", age=60, parent=root)
>>> d = Node("d", age=40, parent=b)
>>> e = Node("e", age=35, parent=b)
>>> tree_to_nested_dict(root, all_attrs=True)
{'name': 'a', 'age': 90, 'children': [{'name': 'b', 'age': 65, 'children': [{'name': 'd', 'age': 40}, {'name': 'e', 'age': 35}]}, {'name': 'c', 'age': 60}]}
Args:
tree (Node): tree to be exported
name_key (str): dictionary key for `node.node_name`, defaults to 'name'
child_key (str): dictionary key for list of children, optional
attr_dict (dict): dictionary mapping node attributes to dictionary key,
key: node attributes, value: corresponding dictionary key, optional
all_attrs (bool): indicator whether to retrieve all `Node` attributes
max_depth (int): maximum depth to export tree, optional
Returns:
(dict)
"""
tree = tree.copy()
data_dict = {}
def recursive_append(node, parent_dict):
if node:
if not max_depth or node.depth <= max_depth:
data_child = {name_key: node.node_name}
if all_attrs:
data_child.update(
dict(
node.describe(
exclude_attributes=["name"], exclude_prefix="_"
)
)
)
else:
for k, v in attr_dict.items():
data_child[v] = node.get_attr(k)
if child_key in parent_dict:
parent_dict[child_key].append(data_child)
else:
parent_dict[child_key] = [data_child]
for _node in node.children:
recursive_append(_node, data_child)
recursive_append(tree, data_dict)
return data_dict[child_key][0]
def tree_to_dataframe(
tree: Node,
path_col: str = "path",
name_col: str = "name",
parent_col: str = "",
attr_dict: dict = {},
all_attrs: bool = False,
max_depth: int = None,
skip_depth: int = None,
leaf_only: bool = False,
) -> pd.DataFrame:
"""Export tree to pandas DataFrame.
All descendants from `tree` will be exported, `tree` can be the root node or child node of tree.
>>> from bigtree import Node, tree_to_dataframe
>>> root = Node("a", age=90)
>>> b = Node("b", age=65, parent=root)
>>> c = Node("c", age=60, parent=root)
>>> d = Node("d", age=40, parent=b)
>>> e = Node("e", age=35, parent=b)
>>> tree_to_dataframe(root, name_col="name", parent_col="parent", path_col="path", attr_dict={"age": "person age"})
path name parent person age
0 /a a None 90
1 /a/b b a 65
2 /a/b/d d b 40
3 /a/b/e e b 35
4 /a/c c a 60
For a subset of a tree.
>>> tree_to_dataframe(b, name_col="name", parent_col="parent", path_col="path", attr_dict={"age": "person age"})
path name parent person age
0 /a/b b a 65
1 /a/b/d d b 40
2 /a/b/e e b 35
Args:
tree (Node): tree to be exported
path_col (str): column name for `node.path_name`, optional
name_col (str): column name for `node.node_name`, defaults to 'name'
parent_col (str): column name for `node.parent.node_name`, optional
attr_dict (dict): dictionary mapping node attributes to column name,
key: node attributes, value: corresponding column in dataframe, optional
all_attrs (bool): indicator whether to retrieve all `Node` attributes
max_depth (int): maximum depth to export tree, optional
skip_depth (int): number of initial depth to skip, optional
leaf_only (bool): indicator to retrieve only information from leaf nodes
Returns:
(pd.DataFrame)
"""
tree = tree.copy()
data_list = []
def recursive_append(node):
if node:
if (
(not max_depth or node.depth <= max_depth)
and (not skip_depth or node.depth > skip_depth)
and (not leaf_only or node.is_leaf)
):
data_child = {}
if path_col:
data_child[path_col] = node.path_name
if name_col:
data_child[name_col] = node.node_name
if parent_col:
parent_name = None
if node.parent:
parent_name = node.parent.node_name
data_child[parent_col] = parent_name
if all_attrs:
data_child.update(
node.describe(exclude_attributes=["name"], exclude_prefix="_")
)
else:
for k, v in attr_dict.items():
data_child[v] = node.get_attr(k)
data_list.append(data_child)
for _node in node.children:
recursive_append(_node)
recursive_append(tree)
return pd.DataFrame(data_list)
def tree_to_dot(
tree: Union[Node, List[Node]],
directed: bool = True,
rankdir: str = "TB",
bg_colour: str = None,
node_colour: str = None,
node_shape: str = None,
edge_colour: str = None,
node_attr: str = None,
edge_attr: str = None,
):
r"""Export tree or list of trees to image.
Posible node attributes include style, fillcolor, shape.
>>> from bigtree import Node, tree_to_dot
>>> root = Node("a", age=90)
>>> b = Node("b", age=65, parent=root)
>>> c = Node("c", age=60, parent=root)
>>> d = Node("d", age=40, parent=b)
>>> e = Node("e", age=35, parent=b)
>>> graph = tree_to_dot(root)
Export to image, dot file, etc.
>>> graph.write_png("tree.png")
>>> graph.write_dot("tree.dot")
Export to string
>>> graph.to_string()
'strict digraph G {\nrankdir=TB;\na0 [label=a];\nb0 [label=b];\na0 -> b0;\nd0 [label=d];\nb0 -> d0;\ne0 [label=e];\nb0 -> e0;\nc0 [label=c];\na0 -> c0;\n}\n'
Defining node and edge attributes
>>> class CustomNode(Node):
... def __init__(self, name, node_shape="", edge_label="", **kwargs):
... super().__init__(name, **kwargs)
... self.node_shape = node_shape
... self.edge_label = edge_label
...
... @property
... def edge_attr(self):
... if self.edge_label:
... return {"label": self.edge_label}
... return {}
...
... @property
... def node_attr(self):
... if self.node_shape:
... return {"shape": self.node_shape}
... return {}
>>>
>>>
>>> root = CustomNode("a", node_shape="circle")
>>> b = CustomNode("b", edge_label="child", parent=root)
>>> c = CustomNode("c", edge_label="child", parent=root)
>>> d = CustomNode("d", node_shape="square", edge_label="child", parent=b)
>>> e = CustomNode("e", node_shape="square", edge_label="child", parent=b)
>>> graph = tree_to_dot(root, node_colour="gold", node_shape="diamond", node_attr="node_attr", edge_attr="edge_attr")
>>> graph.write_png("assets/custom_tree.png")
.. image:: https://github.com/kayjan/bigtree/raw/master/assets/custom_tree.png
Args:
tree (Node/List[Node]): tree or list of trees to be exported
directed (bool): indicator whether graph should be directed or undirected, defaults to True
rankdir (str): set direction of graph layout, defaults to 'TB' (top to bottom), can be 'BT' (bottom to top),
'LR' (left to right), 'RL' (right to left)
bg_colour (str): background color of image, defaults to None
node_colour (str): fill colour of nodes, defaults to None
node_shape (str): shape of nodes, defaults to None
Possible node_shape include "circle", "square", "diamond", "triangle"
edge_colour (str): colour of edges, defaults to None
node_attr (str): `Node` attribute for node style, overrides `node_colour` and `node_shape`, defaults to None.
Possible node style (attribute value) include {"style": "filled", "fillcolor": "gold", "shape": "diamond"}
edge_attr (str): `Node` attribute for edge style, overrides `edge_colour`, defaults to None.
Possible edge style (attribute value) include {"style": "bold", "label": "edge label", "color": "black"}
Returns:
(pydot.Dot)
"""
try:
import pydot
except ImportError: # pragma: no cover
raise ImportError(
"pydot not available. Please perform a\n\npip install 'bigtree[image]'\n\nto install required dependencies"
)
# Get style
if bg_colour:
graph_style = dict(bgcolor=bg_colour)
else:
graph_style = dict()
if node_colour:
node_style = dict(style="filled", fillcolor=node_colour)
else:
node_style = dict()
if node_shape:
node_style["shape"] = node_shape
if edge_colour:
edge_style = dict(color=edge_colour)
else:
edge_style = dict()
tree = tree.copy()
if directed:
_graph = pydot.Dot(
graph_type="digraph", strict=True, rankdir=rankdir, **graph_style
)
else:
_graph = pydot.Dot(
graph_type="graph", strict=True, rankdir=rankdir, **graph_style
)
if not isinstance(tree, list):
tree = [tree]
for _tree in tree:
if not isinstance(_tree, Node):
raise ValueError("Tree should be of type `Node`, or inherit from `Node`")
name_dict = collections.defaultdict(list)
def recursive_create_node_and_edges(parent_name, child_node):
_node_style = node_style.copy()
_edge_style = edge_style.copy()
child_label = child_node.node_name
if child_node.path_name not in name_dict[child_label]: # pragma: no cover
name_dict[child_label].append(child_node.path_name)
child_name = child_label + str(
name_dict[child_label].index(child_node.path_name)
)
if node_attr and child_node.get_attr(node_attr):
_node_style.update(child_node.get_attr(node_attr))
if edge_attr:
_edge_style.update(child_node.get_attr(edge_attr))
node = pydot.Node(name=child_name, label=child_label, **_node_style)
_graph.add_node(node)
if parent_name is not None:
edge = pydot.Edge(parent_name, child_name, **_edge_style)
_graph.add_edge(edge)
for child in child_node.children:
if child:
recursive_create_node_and_edges(child_name, child)
recursive_create_node_and_edges(None, _tree.root)
return _graph
def tree_to_pillow(
tree: Node,
width: int = 0,
height: int = 0,
start_pos: Tuple[float, float] = (10, 10),
font_family: str = "assets/DejaVuSans.ttf",
font_size: int = 12,
font_colour: Union[Tuple[float, float, float], str] = "black",
bg_colour: Union[Tuple[float, float, float], str] = "white",
**kwargs,
):
"""Export tree to image (JPG, PNG).
Image will be similar format as `print_tree`, accepts additional keyword arguments as input to `yield_tree`
>>> from bigtree import Node, tree_to_pillow
>>> root = Node("a", age=90)
>>> b = Node("b", age=65, parent=root)
>>> c = Node("c", age=60, parent=root)
>>> d = Node("d", age=40, parent=b)
>>> e = Node("e", age=35, parent=b)
>>> pillow_image = tree_to_pillow(root)
Export to image (PNG, JPG) file, etc.
>>> pillow_image.save("tree_pillow.png")
>>> pillow_image.save("tree_pillow.jpg")
Args:
tree (Node): tree to be exported
width (int): width of image, optional as width of image is calculated automatically
height (int): height of image, optional as height of image is calculated automatically
start_pos (Tuple[float, float]): start position of text, (x-offset, y-offset), defaults to (10, 10)
font_family (str): file path of font family, requires .ttf file, defaults to DejaVuSans
font_size (int): font size, defaults to 12
font_colour (Union[List[int], str]): font colour, accepts tuple of RGB values or string, defaults to black
bg_colour (Union[List[int], str]): background of image, accepts tuple of RGB values or string, defaults to white
Returns:
(PIL.Image.Image)
"""
try:
from PIL import Image, ImageDraw, ImageFont
except ImportError: # pragma: no cover
raise ImportError(
"Pillow not available. Please perform a\n\npip install 'bigtree[image]'\n\nto install required dependencies"
)
# Initialize font
font = ImageFont.truetype(font_family, font_size)
# Initialize text
image_text = []
for branch, stem, node in yield_tree(tree, **kwargs):
image_text.append(f"{branch}{stem}{node.node_name}\n")
# Calculate image dimension from text, otherwise override with argument
def get_list_of_text_dimensions(text_list):
"""Get list dimensions
Args:
text_list (List[str]): list of texts
Returns:
(List[Iterable[int]]): list of (left, top, right, bottom) bounding box
"""
_image = Image.new("RGB", (0, 0))
_draw = ImageDraw.Draw(_image)
return [_draw.textbbox((0, 0), text_line, font=font) for text_line in text_list]
text_dimensions = get_list_of_text_dimensions(image_text)
text_height = sum(
[text_dimension[3] + text_dimension[1] for text_dimension in text_dimensions]
)
text_width = max(
[text_dimension[2] + text_dimension[0] for text_dimension in text_dimensions]
)
image_text = "".join(image_text)
width = max(width, text_width + 2 * start_pos[0])
height = max(height, text_height + 2 * start_pos[1])
# Initialize and draw image
image = Image.new("RGB", (width, height), bg_colour)
image_draw = ImageDraw.Draw(image)
image_draw.text(start_pos, image_text, font=font, fill=font_colour)
return image

View File

@@ -0,0 +1,201 @@
from typing import Optional, Type
import numpy as np
from bigtree.node.basenode import BaseNode
from bigtree.node.binarynode import BinaryNode
from bigtree.node.node import Node
from bigtree.tree.construct import dataframe_to_tree
from bigtree.tree.export import tree_to_dataframe
from bigtree.tree.search import find_path
from bigtree.utils.exceptions import NotFoundError
__all__ = ["clone_tree", "prune_tree", "get_tree_diff"]
def clone_tree(tree: BaseNode, node_type: Type[BaseNode]) -> BaseNode:
"""Clone tree to another `Node` type.
If the same type is needed, simply do a tree.copy().
>>> from bigtree import BaseNode, Node, clone_tree
>>> root = BaseNode(name="a")
>>> b = BaseNode(name="b", parent=root)
>>> clone_tree(root, Node)
Node(/a, )
Args:
tree (BaseNode): tree to be cloned, must inherit from BaseNode
node_type (Type[BaseNode]): type of cloned tree
Returns:
(BaseNode)
"""
if not isinstance(tree, BaseNode):
raise ValueError(
"Tree should be of type `BaseNode`, or inherit from `BaseNode`"
)
# Start from root
root_info = dict(tree.root.describe(exclude_prefix="_"))
root_node = node_type(**root_info)
def recursive_add_child(_new_parent_node, _parent_node):
for _child in _parent_node.children:
if _child:
child_info = dict(_child.describe(exclude_prefix="_"))
child_node = node_type(**child_info)
child_node.parent = _new_parent_node
recursive_add_child(child_node, _child)
recursive_add_child(root_node, tree.root)
return root_node
def prune_tree(tree: Node, prune_path: str, sep: str = "/") -> Node:
"""Prune tree to leave only the prune path, returns the root of a *copy* of the original tree.
All siblings along the prune path will be removed.
Prune path name should be unique, can be full path or partial path (trailing part of path) or node name.
Path should contain `Node` name, separated by `sep`.
- For example: Path string "a/b" refers to Node("b") with parent Node("a").
>>> from bigtree import Node, prune_tree, print_tree
>>> root = Node("a")
>>> b = Node("b", parent=root)
>>> c = Node("c", parent=root)
>>> print_tree(root)
a
├── b
└── c
>>> root_pruned = prune_tree(root, "a/b")
>>> print_tree(root_pruned)
a
└── b
Args:
tree (Node): existing tree
prune_path (str): prune path, all siblings along the prune path will be removed
sep (str): path separator
Returns:
(Node)
"""
prune_path = prune_path.replace(sep, tree.sep)
tree_copy = tree.copy()
child = find_path(tree_copy, prune_path)
if not child:
raise NotFoundError(
f"Cannot find any node matching path_name ending with {prune_path}"
)
if isinstance(child.parent, BinaryNode):
while child.parent:
child.parent.children = [child, None]
child = child.parent
return tree_copy
while child.parent:
child.parent.children = [child]
child = child.parent
return tree_copy
def get_tree_diff(
tree: Node, other_tree: Node, only_diff: bool = True
) -> Optional[Node]:
"""Get difference of `tree` to `other_tree`, changes are relative to `tree`.
(+) and (-) will be added relative to `tree`.
- For example: (+) refers to nodes that are in `other_tree` but not `tree`.
- For example: (-) refers to nodes that are in `tree` but not `other_tree`.
Note that only leaf nodes are compared and have (+) or (-) indicator. Intermediate parent nodes are not compared.
Function can return all original tree nodes and differences, or only the differences.
>>> from bigtree import Node, get_tree_diff, print_tree
>>> root = Node("a")
>>> b = Node("b", parent=root)
>>> c = Node("c", parent=root)
>>> d = Node("d", parent=b)
>>> e = Node("e", parent=root)
>>> print_tree(root)
a
├── b
│ └── d
├── c
└── e
>>> root_other = Node("a")
>>> b_other = Node("b", parent=root_other)
>>> c_other = Node("c", parent=b_other)
>>> d_other = Node("d", parent=root_other)
>>> e_other = Node("e", parent=root_other)
>>> print_tree(root_other)
a
├── b
│ └── c
├── d
└── e
>>> tree_diff = get_tree_diff(root, root_other)
>>> print_tree(tree_diff)
a
├── b
│ ├── c (+)
│ └── d (-)
├── c (-)
└── d (+)
>>> tree_diff = get_tree_diff(root, root_other, only_diff=False)
>>> print_tree(tree_diff)
a
├── b
│ ├── c (+)
│ └── d (-)
├── c (-)
├── d (+)
└── e
Args:
tree (Node): tree to be compared against
other_tree (Node): tree to be compared with
only_diff (bool): indicator to show all nodes or only nodes that are different (+/-), defaults to True
Returns:
(Node)
"""
tree = tree.copy()
other_tree = other_tree.copy()
name_col = "name"
path_col = "PATH"
indicator_col = "Exists"
data = tree_to_dataframe(tree, name_col=name_col, path_col=path_col, leaf_only=True)
data_other = tree_to_dataframe(
other_tree, name_col=name_col, path_col=path_col, leaf_only=True
)
data_both = data[[path_col, name_col]].merge(
data_other[[path_col, name_col]], how="outer", indicator=indicator_col
)
data_both[name_col] = np.where(
data_both[indicator_col] == "left_only",
data_both[name_col] + " (-)",
np.where(
data_both[indicator_col] == "right_only",
data_both[name_col] + " (+)",
data_both[name_col],
),
)
if only_diff:
data_both = data_both.query(f"{indicator_col} != 'both'")
data_both = data_both.drop(columns=indicator_col).sort_values(path_col)
if len(data_both):
return dataframe_to_tree(
data_both,
node_type=tree.__class__,
)

View File

@@ -0,0 +1,856 @@
import logging
from typing import List, Optional
from bigtree.node.node import Node
from bigtree.tree.search import find_path
from bigtree.utils.exceptions import NotFoundError, TreeError
logging.getLogger(__name__).addHandler(logging.NullHandler())
__all__ = [
"shift_nodes",
"copy_nodes",
"copy_nodes_from_tree_to_tree",
"copy_or_shift_logic",
]
def shift_nodes(
tree: Node,
from_paths: List[str],
to_paths: List[str],
sep: str = "/",
skippable: bool = False,
overriding: bool = False,
merge_children: bool = False,
merge_leaves: bool = False,
delete_children: bool = False,
):
"""Shift nodes from `from_paths` to `to_paths` *in-place*.
- Creates intermediate nodes if to path is not present
- Able to skip nodes if from path is not found, defaults to False (from-nodes must be found; not skippable).
- Able to override existing node if it exists, defaults to False (to-nodes must not exist; not overridden).
- Able to merge children and remove intermediate parent node, defaults to False (nodes are shifted; not merged).
- Able to merge only leaf nodes and remove all intermediate nodes, defaults to False (nodes are shifted; not merged)
- Able to shift node only and delete children, defaults to False (nodes are shifted together with children).
For paths in `from_paths` and `to_paths`,
- Path name can be with or without leading tree path separator symbol.
- Path name can be partial path (trailing part of path) or node name.
- Path name must be unique to one node.
For paths in `to_paths`,
- Can set to empty string or None to delete the path in `from_paths`, note that ``copy`` must be set to False.
If ``merge_children=True``,
- If `to_path` is not present, it shifts children of `from_path`.
- If `to_path` is present, and ``overriding=False``, original and new children are merged.
- If `to_path` is present and ``overriding=True``, it behaves like overriding and only new children are retained.
If ``merge_leaves=True``,
- If `to_path` is not present, it shifts leaves of `from_path`.
- If `to_path` is present, and ``overriding=False``, original children and leaves are merged.
- If `to_path` is present and ``overriding=True``, it behaves like overriding and only new leaves are retained,
original node in `from_path` is retained.
>>> from bigtree import Node, shift_nodes, print_tree
>>> root = Node("a")
>>> b = Node("b", parent=root)
>>> c = Node("c", parent=root)
>>> d = Node("d", parent=root)
>>> print_tree(root)
a
├── b
├── c
└── d
>>> shift_nodes(root, ["a/c", "a/d"], ["a/b/c", "a/dummy/d"])
>>> print_tree(root)
a
├── b
│ └── c
└── dummy
└── d
To delete node,
>>> root = Node("a")
>>> b = Node("b", parent=root)
>>> c = Node("c", parent=root)
>>> print_tree(root)
a
├── b
└── c
>>> shift_nodes(root, ["a/b"], [None])
>>> print_tree(root)
a
└── c
In overriding case,
>>> root = Node("a")
>>> b = Node("b", parent=root)
>>> c = Node("c", parent=root)
>>> d = Node("d", parent=c)
>>> c2 = Node("c", parent=b)
>>> e = Node("e", parent=c2)
>>> print_tree(root)
a
├── b
│ └── c
│ └── e
└── c
└── d
>>> shift_nodes(root, ["a/b/c"], ["a/c"], overriding=True)
>>> print_tree(root)
a
├── b
└── c
└── e
In ``merge_children`` case, child nodes are shifted instead of the parent node.
- If the path already exists, child nodes are merged with existing children.
- If same node is shifted, the child nodes of the node are merged with the node's parent.
>>> root = Node("a")
>>> b = Node("b", parent=root)
>>> c = Node("c", parent=root)
>>> d = Node("d", parent=c)
>>> c2 = Node("c", parent=b)
>>> e = Node("e", parent=c2)
>>> z = Node("z", parent=b)
>>> y = Node("y", parent=z)
>>> f = Node("f", parent=root)
>>> g = Node("g", parent=f)
>>> h = Node("h", parent=g)
>>> print_tree(root)
a
├── b
│ ├── c
│ │ └── e
│ └── z
│ └── y
├── c
│ └── d
└── f
└── g
└── h
>>> shift_nodes(root, ["a/b/c", "z", "a/f"], ["a/c", "a/z", "a/f"], merge_children=True)
>>> print_tree(root)
a
├── b
├── c
│ ├── d
│ └── e
├── y
└── g
└── h
In ``merge_leaves`` case, leaf nodes are copied instead of the parent node.
- If the path already exists, leaf nodes are merged with existing children.
- If same node is copied, the leaf nodes of the node are merged with the node's parent.
>>> root = Node("a")
>>> b = Node("b", parent=root)
>>> c = Node("c", parent=root)
>>> d = Node("d", parent=c)
>>> c2 = Node("c", parent=b)
>>> e = Node("e", parent=c2)
>>> z = Node("z", parent=b)
>>> y = Node("y", parent=z)
>>> f = Node("f", parent=root)
>>> g = Node("g", parent=f)
>>> h = Node("h", parent=g)
>>> print_tree(root)
a
├── b
│ ├── c
│ │ └── e
│ └── z
│ └── y
├── c
│ └── d
└── f
└── g
└── h
>>> shift_nodes(root, ["a/b/c", "z", "a/f"], ["a/c", "a/z", "a/f"], merge_leaves=True)
>>> print_tree(root)
a
├── b
│ ├── c
│ └── z
├── c
│ ├── d
│ └── e
├── f
│ └── g
├── y
└── h
In ``delete_children`` case, only the node is shifted without its accompanying children/descendants.
>>> root = Node("a")
>>> b = Node("b", parent=root)
>>> c = Node("c", parent=root)
>>> d = Node("d", parent=c)
>>> c2 = Node("c", parent=b)
>>> e = Node("e", parent=c2)
>>> z = Node("z", parent=b)
>>> y = Node("y", parent=z)
>>> print_tree(root)
a
├── b
│ ├── c
│ │ └── e
│ └── z
│ └── y
└── c
└── d
>>> shift_nodes(root, ["a/b/z"], ["a/z"], delete_children=True)
>>> print_tree(root)
a
├── b
│ └── c
│ └── e
├── c
│ └── d
└── z
Args:
tree (Node): tree to modify
from_paths (list): original paths to shift nodes from
to_paths (list): new paths to shift nodes to
sep (str): path separator for input paths, applies to `from_path` and `to_path`
skippable (bool): indicator to skip if from path is not found, defaults to False
overriding (bool): indicator to override existing to path if there is clashes, defaults to False
merge_children (bool): indicator to merge children and remove intermediate parent node, defaults to False
merge_leaves (bool): indicator to merge leaf nodes and remove intermediate parent node(s), defaults to False
delete_children (bool): indicator to shift node only without children, defaults to False
"""
return copy_or_shift_logic(
tree=tree,
from_paths=from_paths,
to_paths=to_paths,
sep=sep,
copy=False,
skippable=skippable,
overriding=overriding,
merge_children=merge_children,
merge_leaves=merge_leaves,
delete_children=delete_children,
to_tree=None,
) # pragma: no cover
def copy_nodes(
tree: Node,
from_paths: List[str],
to_paths: List[str],
sep: str = "/",
skippable: bool = False,
overriding: bool = False,
merge_children: bool = False,
merge_leaves: bool = False,
delete_children: bool = False,
):
"""Copy nodes from `from_paths` to `to_paths` *in-place*.
- Creates intermediate nodes if to path is not present
- Able to skip nodes if from path is not found, defaults to False (from-nodes must be found; not skippable).
- Able to override existing node if it exists, defaults to False (to-nodes must not exist; not overridden).
- Able to merge children and remove intermediate parent node, defaults to False (nodes are shifted; not merged).
- Able to merge only leaf nodes and remove all intermediate nodes, defaults to False (nodes are shifted; not merged)
- Able to copy node only and delete children, defaults to False (nodes are copied together with children).
For paths in `from_paths` and `to_paths`,
- Path name can be with or without leading tree path separator symbol.
- Path name can be partial path (trailing part of path) or node name.
- Path name must be unique to one node.
If ``merge_children=True``,
- If `to_path` is not present, it copies children of `from_path`.
- If `to_path` is present, and ``overriding=False``, original and new children are merged.
- If `to_path` is present and ``overriding=True``, it behaves like overriding and only new children are retained.
If ``merge_leaves=True``,
- If `to_path` is not present, it copies leaves of `from_path`.
- If `to_path` is present, and ``overriding=False``, original children and leaves are merged.
- If `to_path` is present and ``overriding=True``, it behaves like overriding and only new leaves are retained.
>>> from bigtree import Node, copy_nodes, print_tree
>>> root = Node("a")
>>> b = Node("b", parent=root)
>>> c = Node("c", parent=root)
>>> d = Node("d", parent=root)
>>> print_tree(root)
a
├── b
├── c
└── d
>>> copy_nodes(root, ["a/c", "a/d"], ["a/b/c", "a/dummy/d"])
>>> print_tree(root)
a
├── b
│ └── c
├── c
├── d
└── dummy
└── d
In overriding case,
>>> root = Node("a")
>>> b = Node("b", parent=root)
>>> c = Node("c", parent=root)
>>> d = Node("d", parent=c)
>>> c2 = Node("c", parent=b)
>>> e = Node("e", parent=c2)
>>> print_tree(root)
a
├── b
│ └── c
│ └── e
└── c
└── d
>>> copy_nodes(root, ["a/b/c"], ["a/c"], overriding=True)
>>> print_tree(root)
a
├── b
│ └── c
│ └── e
└── c
└── e
In ``merge_children`` case, child nodes are copied instead of the parent node.
- If the path already exists, child nodes are merged with existing children.
- If same node is copied, the child nodes of the node are merged with the node's parent.
>>> root = Node("a")
>>> b = Node("b", parent=root)
>>> c = Node("c", parent=root)
>>> d = Node("d", parent=c)
>>> c2 = Node("c", parent=b)
>>> e = Node("e", parent=c2)
>>> z = Node("z", parent=b)
>>> y = Node("y", parent=z)
>>> f = Node("f", parent=root)
>>> g = Node("g", parent=f)
>>> h = Node("h", parent=g)
>>> print_tree(root)
a
├── b
│ ├── c
│ │ └── e
│ └── z
│ └── y
├── c
│ └── d
└── f
└── g
└── h
>>> copy_nodes(root, ["a/b/c", "z", "a/f"], ["a/c", "a/z", "a/f"], merge_children=True)
>>> print_tree(root)
a
├── b
│ ├── c
│ │ └── e
│ └── z
│ └── y
├── c
│ ├── d
│ └── e
├── y
└── g
└── h
In ``merge_leaves`` case, leaf nodes are copied instead of the parent node.
- If the path already exists, leaf nodes are merged with existing children.
- If same node is copied, the leaf nodes of the node are merged with the node's parent.
>>> root = Node("a")
>>> b = Node("b", parent=root)
>>> c = Node("c", parent=root)
>>> d = Node("d", parent=c)
>>> c2 = Node("c", parent=b)
>>> e = Node("e", parent=c2)
>>> z = Node("z", parent=b)
>>> y = Node("y", parent=z)
>>> f = Node("f", parent=root)
>>> g = Node("g", parent=f)
>>> h = Node("h", parent=g)
>>> print_tree(root)
a
├── b
│ ├── c
│ │ └── e
│ └── z
│ └── y
├── c
│ └── d
└── f
└── g
└── h
>>> copy_nodes(root, ["a/b/c", "z", "a/f"], ["a/c", "a/z", "a/f"], merge_leaves=True)
>>> print_tree(root)
a
├── b
│ ├── c
│ │ └── e
│ └── z
│ └── y
├── c
│ ├── d
│ └── e
├── f
│ └── g
│ └── h
├── y
└── h
In ``delete_children`` case, only the node is copied without its accompanying children/descendants.
>>> root = Node("a")
>>> b = Node("b", parent=root)
>>> c = Node("c", parent=root)
>>> d = Node("d", parent=c)
>>> c2 = Node("c", parent=b)
>>> e = Node("e", parent=c2)
>>> z = Node("z", parent=b)
>>> y = Node("y", parent=z)
>>> print_tree(root)
a
├── b
│ ├── c
│ │ └── e
│ └── z
│ └── y
└── c
└── d
>>> copy_nodes(root, ["a/b/z"], ["a/z"], delete_children=True)
>>> print_tree(root)
a
├── b
│ ├── c
│ │ └── e
│ └── z
│ └── y
├── c
│ └── d
└── z
Args:
tree (Node): tree to modify
from_paths (list): original paths to shift nodes from
to_paths (list): new paths to shift nodes to
sep (str): path separator for input paths, applies to `from_path` and `to_path`
skippable (bool): indicator to skip if from path is not found, defaults to False
overriding (bool): indicator to override existing to path if there is clashes, defaults to False
merge_children (bool): indicator to merge children and remove intermediate parent node, defaults to False
merge_leaves (bool): indicator to merge leaf nodes and remove intermediate parent node(s), defaults to False
delete_children (bool): indicator to copy node only without children, defaults to False
"""
return copy_or_shift_logic(
tree=tree,
from_paths=from_paths,
to_paths=to_paths,
sep=sep,
copy=True,
skippable=skippable,
overriding=overriding,
merge_children=merge_children,
merge_leaves=merge_leaves,
delete_children=delete_children,
to_tree=None,
) # pragma: no cover
def copy_nodes_from_tree_to_tree(
from_tree: Node,
to_tree: Node,
from_paths: List[str],
to_paths: List[str],
sep: str = "/",
skippable: bool = False,
overriding: bool = False,
merge_children: bool = False,
merge_leaves: bool = False,
delete_children: bool = False,
):
"""Copy nodes from `from_paths` to `to_paths` *in-place*.
- Creates intermediate nodes if to path is not present
- Able to skip nodes if from path is not found, defaults to False (from-nodes must be found; not skippable).
- Able to override existing node if it exists, defaults to False (to-nodes must not exist; not overridden).
- Able to merge children and remove intermediate parent node, defaults to False (nodes are shifted; not merged).
- Able to merge only leaf nodes and remove all intermediate nodes, defaults to False (nodes are shifted; not merged)
- Able to copy node only and delete children, defaults to False (nodes are copied together with children).
For paths in `from_paths` and `to_paths`,
- Path name can be with or without leading tree path separator symbol.
- Path name can be partial path (trailing part of path) or node name.
- Path name must be unique to one node.
If ``merge_children=True``,
- If `to_path` is not present, it copies children of `from_path`
- If `to_path` is present, and ``overriding=False``, original and new children are merged
- If `to_path` is present and ``overriding=True``, it behaves like overriding and only new leaves are retained.
If ``merge_leaves=True``,
- If `to_path` is not present, it copies leaves of `from_path`.
- If `to_path` is present, and ``overriding=False``, original children and leaves are merged.
- If `to_path` is present and ``overriding=True``, it behaves like overriding and only new leaves are retained.
>>> from bigtree import Node, copy_nodes_from_tree_to_tree, print_tree
>>> root = Node("a")
>>> b = Node("b", parent=root)
>>> c = Node("c", parent=root)
>>> d = Node("d", parent=c)
>>> e = Node("e", parent=root)
>>> f = Node("f", parent=e)
>>> g = Node("g", parent=f)
>>> print_tree(root)
a
├── b
├── c
│ └── d
└── e
└── f
└── g
>>> root_other = Node("aa")
>>> copy_nodes_from_tree_to_tree(root, root_other, ["a/b", "a/c", "a/e"], ["aa/b", "aa/b/c", "aa/dummy/e"])
>>> print_tree(root_other)
aa
├── b
│ └── c
│ └── d
└── dummy
└── e
└── f
└── g
In overriding case,
>>> root_other = Node("aa")
>>> c = Node("c", parent=root_other)
>>> e = Node("e", parent=c)
>>> print_tree(root_other)
aa
└── c
└── e
>>> copy_nodes_from_tree_to_tree(root, root_other, ["a/b", "a/c"], ["aa/b", "aa/c"], overriding=True)
>>> print_tree(root_other)
aa
├── b
└── c
└── d
In ``merge_children`` case, child nodes are copied instead of the parent node.
- If the path already exists, child nodes are merged with existing children.
>>> root_other = Node("aa")
>>> c = Node("c", parent=root_other)
>>> e = Node("e", parent=c)
>>> print_tree(root_other)
aa
└── c
└── e
>>> copy_nodes_from_tree_to_tree(root, root_other, ["a/c", "e"], ["a/c", "a/e"], merge_children=True)
>>> print_tree(root_other)
aa
├── c
│ ├── e
│ └── d
└── f
└── g
In ``merge_leaves`` case, leaf nodes are copied instead of the parent node.
- If the path already exists, leaf nodes are merged with existing children.
>>> root_other = Node("aa")
>>> c = Node("c", parent=root_other)
>>> e = Node("e", parent=c)
>>> print_tree(root_other)
aa
└── c
└── e
>>> copy_nodes_from_tree_to_tree(root, root_other, ["a/c", "e"], ["a/c", "a/e"], merge_leaves=True)
>>> print_tree(root_other)
aa
├── c
│ ├── e
│ └── d
└── g
In ``delete_children`` case, only the node is copied without its accompanying children/descendants.
>>> root_other = Node("aa")
>>> print_tree(root_other)
aa
>>> copy_nodes_from_tree_to_tree(root, root_other, ["a/c", "e"], ["a/c", "a/e"], delete_children=True)
>>> print_tree(root_other)
aa
├── c
└── e
Args:
from_tree (Node): tree to copy nodes from
to_tree (Node): tree to copy nodes to
from_paths (list): original paths to shift nodes from
to_paths (list): new paths to shift nodes to
sep (str): path separator for input paths, applies to `from_path` and `to_path`
skippable (bool): indicator to skip if from path is not found, defaults to False
overriding (bool): indicator to override existing to path if there is clashes, defaults to False
merge_children (bool): indicator to merge children and remove intermediate parent node, defaults to False
merge_leaves (bool): indicator to merge leaf nodes and remove intermediate parent node(s), defaults to False
delete_children (bool): indicator to copy node only without children, defaults to False
"""
return copy_or_shift_logic(
tree=from_tree,
from_paths=from_paths,
to_paths=to_paths,
sep=sep,
copy=True,
skippable=skippable,
overriding=overriding,
merge_children=merge_children,
merge_leaves=merge_leaves,
delete_children=delete_children,
to_tree=to_tree,
) # pragma: no cover
def copy_or_shift_logic(
tree: Node,
from_paths: List[str],
to_paths: List[str],
sep: str = "/",
copy: bool = False,
skippable: bool = False,
overriding: bool = False,
merge_children: bool = False,
merge_leaves: bool = False,
delete_children: bool = False,
to_tree: Optional[Node] = None,
):
"""Shift or copy nodes from `from_paths` to `to_paths` *in-place*.
- Creates intermediate nodes if to path is not present
- Able to copy node, defaults to False (nodes are shifted; not copied).
- Able to skip nodes if from path is not found, defaults to False (from-nodes must be found; not skippable)
- Able to override existing node if it exists, defaults to False (to-nodes must not exist; not overridden)
- Able to merge children and remove intermediate parent node, defaults to False (nodes are shifted; not merged)
- Able to merge only leaf nodes and remove all intermediate nodes, defaults to False (nodes are shifted; not merged)
- Able to shift/copy node only and delete children, defaults to False (nodes are shifted/copied together with children).
- Able to shift/copy nodes from one tree to another tree, defaults to None (shifting/copying happens within same tree)
For paths in `from_paths` and `to_paths`,
- Path name can be with or without leading tree path separator symbol.
- Path name can be partial path (trailing part of path) or node name.
- Path name must be unique to one node.
For paths in `to_paths`,
- Can set to empty string or None to delete the path in `from_paths`, note that ``copy`` must be set to False.
If ``merge_children=True``,
- If `to_path` is not present, it shifts/copies children of `from_path`.
- If `to_path` is present, and ``overriding=False``, original and new children are merged.
- If `to_path` is present and ``overriding=True``, it behaves like overriding and only new children are retained.
If ``merge_leaves=True``,
- If `to_path` is not present, it shifts/copies leaves of `from_path`.
- If `to_path` is present, and ``overriding=False``, original children and leaves are merged.
- If `to_path` is present and ``overriding=True``, it behaves like overriding and only new leaves are retained,
original non-leaf nodes in `from_path` are retained.
Args:
tree (Node): tree to modify
from_paths (list): original paths to shift nodes from
to_paths (list): new paths to shift nodes to
sep (str): path separator for input paths, applies to `from_path` and `to_path`
copy (bool): indicator to copy node, defaults to False
skippable (bool): indicator to skip if from path is not found, defaults to False
overriding (bool): indicator to override existing to path if there is clashes, defaults to False
merge_children (bool): indicator to merge children and remove intermediate parent node, defaults to False
merge_leaves (bool): indicator to merge leaf nodes and remove intermediate parent node(s), defaults to False
delete_children (bool): indicator to shift/copy node only without children, defaults to False
to_tree (Node): tree to copy to, defaults to None
"""
if merge_children and merge_leaves:
raise ValueError(
"Invalid shifting, can only specify one type of merging, check `merge_children` and `merge_leaves`"
)
if not (isinstance(from_paths, list) and isinstance(to_paths, list)):
raise ValueError(
"Invalid type, `from_paths` and `to_paths` should be list type"
)
if len(from_paths) != len(to_paths):
raise ValueError(
f"Paths are different length, input `from_paths` have {len(from_paths)} entries, "
f"while output `to_paths` have {len(to_paths)} entries"
)
for from_path, to_path in zip(from_paths, to_paths):
if to_path:
if from_path.split(sep)[-1] != to_path.split(sep)[-1]:
raise ValueError(
f"Unable to assign from_path {from_path} to to_path {to_path}\n"
f"Verify that `sep` is defined correctly for path\n"
f"Alternatively, check that `from_path` and `to_path` is reassigning the same node"
)
transfer_indicator = False
node_type = tree.__class__
tree_sep = tree.sep
if to_tree:
transfer_indicator = True
node_type = to_tree.__class__
tree_sep = to_tree.sep
for from_path, to_path in zip(from_paths, to_paths):
from_path = from_path.replace(sep, tree.sep)
from_node = find_path(tree, from_path)
# From node not found
if not from_node:
if not skippable:
raise NotFoundError(
f"Unable to find from_path {from_path}\n"
f"Set `skippable` to True to skip shifting for nodes not found"
)
else:
logging.info(f"Unable to find from_path {from_path}")
# From node found
else:
# Node to be deleted
if not to_path:
to_node = None
# Node to be copied/shifted
else:
to_path = to_path.replace(sep, tree_sep)
if transfer_indicator:
to_node = find_path(to_tree, to_path)
else:
to_node = find_path(tree, to_path)
# To node found
if to_node:
if from_node == to_node:
if merge_children:
parent = to_node.parent
to_node.parent = None
to_node = parent
elif merge_leaves:
to_node = to_node.parent
else:
raise TreeError(
f"Attempting to shift the same node {from_node.node_name} back to the same position\n"
f"Check from path {from_path} and to path {to_path}\n"
f"Alternatively, set `merge_children` or `merge_leaves` to True if intermediate node is to be removed"
)
elif merge_children:
# Specify override to remove existing node, else children are merged
if not overriding:
logging.info(
f"Path {to_path} already exists and children are merged"
)
else:
logging.info(
f"Path {to_path} already exists and its children be overridden by the merge"
)
parent = to_node.parent
to_node.parent = None
to_node = parent
merge_children = False
elif merge_leaves:
# Specify override to remove existing node, else leaves are merged
if not overriding:
logging.info(
f"Path {to_path} already exists and leaves are merged"
)
else:
logging.info(
f"Path {to_path} already exists and its leaves be overridden by the merge"
)
del to_node.children
else:
if not overriding:
raise TreeError(
f"Path {to_path} already exists and unable to override\n"
f"Set `overriding` to True to perform overrides\n"
f"Alternatively, set `merge_children` to True if nodes are to be merged"
)
logging.info(
f"Path {to_path} already exists and will be overridden"
)
parent = to_node.parent
to_node.parent = None
to_node = parent
# To node not found
else:
# Find parent node
to_path_list = to_path.split(tree_sep)
idx = 1
to_path_parent = tree_sep.join(to_path_list[:-idx])
if transfer_indicator:
to_node = find_path(to_tree, to_path_parent)
else:
to_node = find_path(tree, to_path_parent)
# Create intermediate parent node, if applicable
while (not to_node) & (idx + 1 < len(to_path_list)):
idx += 1
to_path_parent = sep.join(to_path_list[:-idx])
if transfer_indicator:
to_node = find_path(to_tree, to_path_parent)
else:
to_node = find_path(tree, to_path_parent)
if not to_node:
raise NotFoundError(
f"Unable to find to_path {to_path}\n"
f"Please specify valid path to shift node to"
)
for depth in range(len(to_path_list) - idx, len(to_path_list) - 1):
intermediate_child_node = node_type(to_path_list[depth])
intermediate_child_node.parent = to_node
to_node = intermediate_child_node
# Reassign from_node to new parent
if copy:
logging.debug(f"Copying {from_node.node_name}")
from_node = from_node.copy()
if merge_children:
logging.debug(
f"Reassigning children from {from_node.node_name} to {to_node.node_name}"
)
for children in from_node.children:
if delete_children:
del children.children
children.parent = to_node
from_node.parent = None
elif merge_leaves:
logging.debug(
f"Reassigning leaf nodes from {from_node.node_name} to {to_node.node_name}"
)
for children in from_node.leaves:
children.parent = to_node
else:
if delete_children:
del from_node.children
from_node.parent = to_node

View File

@@ -0,0 +1,316 @@
from typing import Any, Callable, Iterable
from bigtree.node.basenode import BaseNode
from bigtree.node.node import Node
from bigtree.utils.exceptions import CorruptedTreeError, SearchError
from bigtree.utils.iterators import preorder_iter
__all__ = [
"findall",
"find",
"find_name",
"find_names",
"find_full_path",
"find_path",
"find_paths",
"find_attr",
"find_attrs",
"find_children",
]
def findall(
tree: BaseNode,
condition: Callable,
max_depth: int = None,
min_count: int = None,
max_count: int = None,
) -> tuple:
"""
Search tree for nodes matching condition (callable function).
>>> from bigtree import Node, findall
>>> root = Node("a", age=90)
>>> b = Node("b", age=65, parent=root)
>>> c = Node("c", age=60, parent=root)
>>> d = Node("d", age=40, parent=c)
>>> findall(root, lambda node: node.age > 62)
(Node(/a, age=90), Node(/a/b, age=65))
Args:
tree (BaseNode): tree to search
condition (Callable): function that takes in node as argument, returns node if condition evaluates to `True`
max_depth (int): maximum depth to search for, based on `depth` attribute, defaults to None
min_count (int): checks for minimum number of occurrence,
raise SearchError if number of results do not meet min_count, defaults to None
max_count (int): checks for maximum number of occurrence,
raise SearchError if number of results do not meet min_count, defaults to None
Returns:
(tuple)
"""
result = tuple(preorder_iter(tree, filter_condition=condition, max_depth=max_depth))
if min_count and len(result) < min_count:
raise SearchError(
f"Expected more than {min_count} element(s), found {len(result)} elements\n{result}"
)
if max_count and len(result) > max_count:
raise SearchError(
f"Expected less than {max_count} element(s), found {len(result)} elements\n{result}"
)
return result
def find(tree: BaseNode, condition: Callable, max_depth: int = None) -> BaseNode:
"""
Search tree for *single node* matching condition (callable function).
>>> from bigtree import Node, find
>>> root = Node("a", age=90)
>>> b = Node("b", age=65, parent=root)
>>> c = Node("c", age=60, parent=root)
>>> d = Node("d", age=40, parent=c)
>>> find(root, lambda node: node.age == 65)
Node(/a/b, age=65)
>>> find(root, lambda node: node.age > 5)
Traceback (most recent call last):
...
bigtree.utils.exceptions.SearchError: Expected less than 1 element(s), found 4 elements
(Node(/a, age=90), Node(/a/b, age=65), Node(/a/c, age=60), Node(/a/c/d, age=40))
Args:
tree (BaseNode): tree to search
condition (Callable): function that takes in node as argument, returns node if condition evaluates to `True`
max_depth (int): maximum depth to search for, based on `depth` attribute, defaults to None
Returns:
(BaseNode)
"""
result = findall(tree, condition, max_depth, max_count=1)
if result:
return result[0]
def find_name(tree: Node, name: str, max_depth: int = None) -> Node:
"""
Search tree for single node matching name attribute.
>>> from bigtree import Node, find_name
>>> root = Node("a", age=90)
>>> b = Node("b", age=65, parent=root)
>>> c = Node("c", age=60, parent=root)
>>> d = Node("d", age=40, parent=c)
>>> find_name(root, "c")
Node(/a/c, age=60)
Args:
tree (Node): tree to search
name (str): value to match for name attribute
max_depth (int): maximum depth to search for, based on `depth` attribute, defaults to None
Returns:
(Node)
"""
return find(tree, lambda node: node.node_name == name, max_depth)
def find_names(tree: Node, name: str, max_depth: int = None) -> Iterable[Node]:
"""
Search tree for multiple node(s) matching name attribute.
>>> from bigtree import Node, find_names
>>> root = Node("a", age=90)
>>> b = Node("b", age=65, parent=root)
>>> c = Node("c", age=60, parent=root)
>>> d = Node("b", age=40, parent=c)
>>> find_names(root, "c")
(Node(/a/c, age=60),)
>>> find_names(root, "b")
(Node(/a/b, age=65), Node(/a/c/b, age=40))
Args:
tree (Node): tree to search
name (str): value to match for name attribute
max_depth (int): maximum depth to search for, based on `depth` attribute, defaults to None
Returns:
(Iterable[Node])
"""
return findall(tree, lambda node: node.node_name == name, max_depth)
def find_full_path(tree: Node, path_name: str) -> Node:
"""
Search tree for single node matching path attribute.
- Path name can be with or without leading tree path separator symbol.
- Path name must be full path, works similar to `find_path` but faster.
>>> from bigtree import Node, find_full_path
>>> root = Node("a", age=90)
>>> b = Node("b", age=65, parent=root)
>>> c = Node("c", age=60, parent=root)
>>> d = Node("d", age=40, parent=c)
>>> find_full_path(root, "/a/c/d")
Node(/a/c/d, age=40)
Args:
tree (Node): tree to search
path_name (str): value to match (full path) of path_name attribute
Returns:
(Node)
"""
path_name = path_name.rstrip(tree.sep).lstrip(tree.sep)
path_list = path_name.split(tree.sep)
if path_list[0] != tree.root.node_name:
raise ValueError(
f"Path {path_name} does not match the root node name {tree.root.node_name}"
)
parent_node = tree.root
child_node = parent_node
for child_name in path_list[1:]:
child_node = find_children(parent_node, child_name)
if not child_node:
break
parent_node = child_node
return child_node
def find_path(tree: Node, path_name: str) -> Node:
"""
Search tree for single node matching path attribute.
- Path name can be with or without leading tree path separator symbol.
- Path name can be full path or partial path (trailing part of path) or node name.
>>> from bigtree import Node, find_path
>>> root = Node("a", age=90)
>>> b = Node("b", age=65, parent=root)
>>> c = Node("c", age=60, parent=root)
>>> d = Node("d", age=40, parent=c)
>>> find_path(root, "c")
Node(/a/c, age=60)
>>> find_path(root, "/c")
Node(/a/c, age=60)
Args:
tree (Node): tree to search
path_name (str): value to match (full path) or trailing part (partial path) of path_name attribute
Returns:
(Node)
"""
path_name = path_name.rstrip(tree.sep)
return find(tree, lambda node: node.path_name.endswith(path_name))
def find_paths(tree: Node, path_name: str) -> tuple:
"""
Search tree for multiple nodes matching path attribute.
- Path name can be with or without leading tree path separator symbol.
- Path name can be partial path (trailing part of path) or node name.
>>> from bigtree import Node, find_paths
>>> root = Node("a", age=90)
>>> b = Node("b", age=65, parent=root)
>>> c = Node("c", age=60, parent=root)
>>> d = Node("c", age=40, parent=c)
>>> find_paths(root, "/a/c")
(Node(/a/c, age=60),)
>>> find_paths(root, "/c")
(Node(/a/c, age=60), Node(/a/c/c, age=40))
Args:
tree (Node): tree to search
path_name (str): value to match (full path) or trailing part (partial path) of path_name attribute
Returns:
(tuple)
"""
path_name = path_name.rstrip(tree.sep)
return findall(tree, lambda node: node.path_name.endswith(path_name))
def find_attr(
tree: BaseNode, attr_name: str, attr_value: Any, max_depth: int = None
) -> BaseNode:
"""
Search tree for single node matching custom attribute.
>>> from bigtree import Node, find_attr
>>> root = Node("a", age=90)
>>> b = Node("b", age=65, parent=root)
>>> c = Node("c", age=60, parent=root)
>>> d = Node("d", age=40, parent=c)
>>> find_attr(root, "age", 65)
Node(/a/b, age=65)
Args:
tree (BaseNode): tree to search
attr_name (str): attribute name to perform matching
attr_value (Any): value to match for attr_name attribute
max_depth (int): maximum depth to search for, based on `depth` attribute, defaults to None
Returns:
(BaseNode)
"""
return find(
tree, lambda node: node.__getattribute__(attr_name) == attr_value, max_depth
)
def find_attrs(
tree: BaseNode, attr_name: str, attr_value: Any, max_depth: int = None
) -> tuple:
"""
Search tree for node(s) matching custom attribute.
>>> from bigtree import Node, find_attrs
>>> root = Node("a", age=90)
>>> b = Node("b", age=65, parent=root)
>>> c = Node("c", age=65, parent=root)
>>> d = Node("d", age=40, parent=c)
>>> find_attrs(root, "age", 65)
(Node(/a/b, age=65), Node(/a/c, age=65))
Args:
tree (BaseNode): tree to search
attr_name (str): attribute name to perform matching
attr_value (Any): value to match for attr_name attribute
max_depth (int): maximum depth to search for, based on `depth` attribute, defaults to None
Returns:
(tuple)
"""
return findall(
tree, lambda node: node.__getattribute__(attr_name) == attr_value, max_depth
)
def find_children(tree: Node, name: str) -> Node:
"""
Search tree for single node matching name attribute.
>>> from bigtree import Node, find_children
>>> root = Node("a", age=90)
>>> b = Node("b", age=65, parent=root)
>>> c = Node("c", age=60, parent=root)
>>> d = Node("d", age=40, parent=c)
>>> find_children(root, "c")
Node(/a/c, age=60)
>>> find_children(c, "d")
Node(/a/c/d, age=40)
Args:
tree (Node): tree to search, parent node
name (str): value to match for name attribute, child node
Returns:
(Node)
"""
child = [node for node in tree.children if node and node.node_name == name]
if len(child) > 1: # pragma: no cover
raise CorruptedTreeError(
f"There are more than one path for {child[0].path_name}, check {child}"
)
elif len(child):
return child[0]

View File

@@ -0,0 +1,32 @@
class TreeError(Exception):
pass
class LoopError(TreeError):
"""Error during node creation"""
pass
class CorruptedTreeError(TreeError):
"""Error during node creation or tree creation"""
pass
class DuplicatedNodeError(TreeError):
"""Error during tree creation"""
pass
class NotFoundError(TreeError):
"""Error during tree creation or tree search"""
pass
class SearchError(TreeError):
"""Error during tree search"""
pass

View File

@@ -0,0 +1,371 @@
from typing import Callable, Iterable, List, Tuple
__all__ = [
"inorder_iter",
"preorder_iter",
"postorder_iter",
"levelorder_iter",
"levelordergroup_iter",
"dag_iterator",
]
def inorder_iter(
tree,
filter_condition: Callable = None,
max_depth: int = None,
) -> Iterable:
"""Iterate through all children of a tree.
In Iteration Algorithm, LNR
1. Recursively traverse the current node's left subtree.
2. Visit the current node.
3. Recursively traverse the current node's right subtree.
>>> from bigtree import BinaryNode, list_to_binarytree, inorder_iter, print_tree
>>> num_list = [1, 2, 3, 4, 5, 6, 7, 8]
>>> root = list_to_binarytree(num_list)
>>> print_tree(root)
1
├── 2
│ ├── 4
│ │ └── 8
│ └── 5
└── 3
├── 6
└── 7
>>> [node.node_name for node in inorder_iter(root)]
['8', '4', '2', '5', '1', '6', '3', '7']
>>> [node.node_name for node in inorder_iter(root, filter_condition=lambda x: x.node_name in ["1", "4", "3", "6", "7"])]
['4', '1', '6', '3', '7']
>>> [node.node_name for node in inorder_iter(root, max_depth=3)]
['4', '2', '5', '1', '6', '3', '7']
Args:
tree (BaseNode): input tree
filter_condition (Callable): function that takes in node as argument, optional
Returns node if condition evaluates to `True`
max_depth (int): maximum depth of iteration, based on `depth` attribute, optional
Returns:
(Iterable[BaseNode])
"""
if tree and (not max_depth or not tree.depth > max_depth):
yield from inorder_iter(tree.left, filter_condition, max_depth)
if not filter_condition or filter_condition(tree):
yield tree
yield from inorder_iter(tree.right, filter_condition, max_depth)
def preorder_iter(
tree,
filter_condition: Callable = None,
stop_condition: Callable = None,
max_depth: int = None,
) -> Iterable:
"""Iterate through all children of a tree.
Pre-Order Iteration Algorithm, NLR
1. Visit the current node.
2. Recursively traverse the current node's left subtree.
3. Recursively traverse the current node's right subtree.
It is topologically sorted because a parent node is processed before its child nodes.
>>> from bigtree import Node, list_to_tree, preorder_iter, print_tree
>>> path_list = ["a/b/d", "a/b/e/g", "a/b/e/h", "a/c/f"]
>>> root = list_to_tree(path_list)
>>> print_tree(root)
a
├── b
│ ├── d
│ └── e
│ ├── g
│ └── h
└── c
└── f
>>> [node.node_name for node in preorder_iter(root)]
['a', 'b', 'd', 'e', 'g', 'h', 'c', 'f']
>>> [node.node_name for node in preorder_iter(root, filter_condition=lambda x: x.node_name in ["a", "d", "e", "f", "g"])]
['a', 'd', 'e', 'g', 'f']
>>> [node.node_name for node in preorder_iter(root, stop_condition=lambda x: x.node_name=="e")]
['a', 'b', 'd', 'c', 'f']
>>> [node.node_name for node in preorder_iter(root, max_depth=3)]
['a', 'b', 'd', 'e', 'c', 'f']
Args:
tree (BaseNode): input tree
filter_condition (Callable): function that takes in node as argument, optional
Returns node if condition evaluates to `True`
stop_condition (Callable): function that takes in node as argument, optional
Stops iteration if condition evaluates to `True`
max_depth (int): maximum depth of iteration, based on `depth` attribute, optional
Returns:
(Iterable[BaseNode])
"""
if (
tree
and (not max_depth or not tree.depth > max_depth)
and (not stop_condition or not stop_condition(tree))
):
if not filter_condition or filter_condition(tree):
yield tree
for child in tree.children:
yield from preorder_iter(child, filter_condition, stop_condition, max_depth)
def postorder_iter(
tree,
filter_condition: Callable = None,
stop_condition: Callable = None,
max_depth: int = None,
) -> Iterable:
"""Iterate through all children of a tree.
Post-Order Iteration Algorithm, LRN
1. Recursively traverse the current node's left subtree.
2. Recursively traverse the current node's right subtree.
3. Visit the current node.
>>> from bigtree import Node, list_to_tree, postorder_iter, print_tree
>>> path_list = ["a/b/d", "a/b/e/g", "a/b/e/h", "a/c/f"]
>>> root = list_to_tree(path_list)
>>> print_tree(root)
a
├── b
│ ├── d
│ └── e
│ ├── g
│ └── h
└── c
└── f
>>> [node.node_name for node in postorder_iter(root)]
['d', 'g', 'h', 'e', 'b', 'f', 'c', 'a']
>>> [node.node_name for node in postorder_iter(root, filter_condition=lambda x: x.node_name in ["a", "d", "e", "f", "g"])]
['d', 'g', 'e', 'f', 'a']
>>> [node.node_name for node in postorder_iter(root, stop_condition=lambda x: x.node_name=="e")]
['d', 'b', 'f', 'c', 'a']
>>> [node.node_name for node in postorder_iter(root, max_depth=3)]
['d', 'e', 'b', 'f', 'c', 'a']
Args:
tree (BaseNode): input tree
filter_condition (Callable): function that takes in node as argument, optional
Returns node if condition evaluates to `True`
stop_condition (Callable): function that takes in node as argument, optional
Stops iteration if condition evaluates to `True`
max_depth (int): maximum depth of iteration, based on `depth` attribute, optional
Returns:
(Iterable[BaseNode])
"""
if (
tree
and (not max_depth or not tree.depth > max_depth)
and (not stop_condition or not stop_condition(tree))
):
for child in tree.children:
yield from postorder_iter(
child, filter_condition, stop_condition, max_depth
)
if not filter_condition or filter_condition(tree):
yield tree
def levelorder_iter(
tree,
filter_condition: Callable = None,
stop_condition: Callable = None,
max_depth: int = None,
) -> Iterable:
"""Iterate through all children of a tree.
Level Order Algorithm
1. Recursively traverse the nodes on same level.
>>> from bigtree import Node, list_to_tree, levelorder_iter, print_tree
>>> path_list = ["a/b/d", "a/b/e/g", "a/b/e/h", "a/c/f"]
>>> root = list_to_tree(path_list)
>>> print_tree(root)
a
├── b
│ ├── d
│ └── e
│ ├── g
│ └── h
└── c
└── f
>>> [node.node_name for node in levelorder_iter(root)]
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
>>> [node.node_name for node in levelorder_iter(root, filter_condition=lambda x: x.node_name in ["a", "d", "e", "f", "g"])]
['a', 'd', 'e', 'f', 'g']
>>> [node.node_name for node in levelorder_iter(root, stop_condition=lambda x: x.node_name=="e")]
['a', 'b', 'c', 'd', 'f']
>>> [node.node_name for node in levelorder_iter(root, max_depth=3)]
['a', 'b', 'c', 'd', 'e', 'f']
Args:
tree (BaseNode): input tree
filter_condition (Callable): function that takes in node as argument, optional
Returns node if condition evaluates to `True`
stop_condition (Callable): function that takes in node as argument, optional
Stops iteration if condition evaluates to `True`
max_depth (int): maximum depth of iteration, based on `depth` attribute, defaults to None
Returns:
(Iterable[BaseNode])
"""
if not isinstance(tree, List):
tree = [tree]
next_level = []
for _tree in tree:
if _tree:
if (not max_depth or not _tree.depth > max_depth) and (
not stop_condition or not stop_condition(_tree)
):
if not filter_condition or filter_condition(_tree):
yield _tree
next_level.extend(list(_tree.children))
if len(next_level):
yield from levelorder_iter(
next_level, filter_condition, stop_condition, max_depth
)
def levelordergroup_iter(
tree,
filter_condition: Callable = None,
stop_condition: Callable = None,
max_depth: int = None,
) -> Iterable[Iterable]:
"""Iterate through all children of a tree.
Level Order Group Algorithm
1. Recursively traverse the nodes on same level, returns nodes level by level in a nested list.
>>> from bigtree import Node, list_to_tree, levelordergroup_iter, print_tree
>>> path_list = ["a/b/d", "a/b/e/g", "a/b/e/h", "a/c/f"]
>>> root = list_to_tree(path_list)
>>> print_tree(root)
a
├── b
│ ├── d
│ └── e
│ ├── g
│ └── h
└── c
└── f
>>> [[node.node_name for node in group] for group in levelordergroup_iter(root)]
[['a'], ['b', 'c'], ['d', 'e', 'f'], ['g', 'h']]
>>> [[node.node_name for node in group] for group in levelordergroup_iter(root, filter_condition=lambda x: x.node_name in ["a", "d", "e", "f", "g"])]
[['a'], [], ['d', 'e', 'f'], ['g']]
>>> [[node.node_name for node in group] for group in levelordergroup_iter(root, stop_condition=lambda x: x.node_name=="e")]
[['a'], ['b', 'c'], ['d', 'f']]
>>> [[node.node_name for node in group] for group in levelordergroup_iter(root, max_depth=3)]
[['a'], ['b', 'c'], ['d', 'e', 'f']]
Args:
tree (BaseNode): input tree
filter_condition (Callable): function that takes in node as argument, optional
Returns node if condition evaluates to `True`
stop_condition (Callable): function that takes in node as argument, optional
Stops iteration if condition evaluates to `True`
max_depth (int): maximum depth of iteration, based on `depth` attribute, defaults to None
Returns:
(Iterable[Iterable])
"""
if not isinstance(tree, List):
tree = [tree]
current_tree = []
next_tree = []
for _tree in tree:
if (not max_depth or not _tree.depth > max_depth) and (
not stop_condition or not stop_condition(_tree)
):
if not filter_condition or filter_condition(_tree):
current_tree.append(_tree)
next_tree.extend([_child for _child in _tree.children if _child])
yield tuple(current_tree)
if len(next_tree) and (not max_depth or not next_tree[0].depth > max_depth):
yield from levelordergroup_iter(
next_tree, filter_condition, stop_condition, max_depth
)
def dag_iterator(dag) -> Iterable[Tuple]:
"""Iterate through all nodes of a Directed Acyclic Graph (DAG).
Note that node names must be unique.
Note that DAG must at least have two nodes to be shown on graph.
1. Visit the current node.
2. Recursively traverse the current node's parents.
3. Recursively traverse the current node's children.
>>> from bigtree import DAGNode, dag_iterator
>>> a = DAGNode("a", step=1)
>>> b = DAGNode("b", step=1)
>>> c = DAGNode("c", step=2, parents=[a, b])
>>> d = DAGNode("d", step=2, parents=[a, c])
>>> e = DAGNode("e", step=3, parents=[d])
>>> [(parent.node_name, child.node_name) for parent, child in dag_iterator(a)]
[('a', 'c'), ('a', 'd'), ('b', 'c'), ('c', 'd'), ('d', 'e')]
Args:
dag (DAGNode): input dag
Returns:
(Iterable[Tuple[DAGNode, DAGNode]])
"""
visited_nodes = set()
def recursively_parse_dag(node):
node_name = node.node_name
visited_nodes.add(node_name)
# Parse upwards
for parent in node.parents:
parent_name = parent.node_name
if parent_name not in visited_nodes:
yield parent, node
# Parse downwards
for child in node.children:
child_name = child.node_name
if child_name not in visited_nodes:
yield node, child
# Parse upwards
for parent in node.parents:
parent_name = parent.node_name
if parent_name not in visited_nodes:
yield from recursively_parse_dag(parent)
# Parse downwards
for child in node.children:
child_name = child.node_name
if child_name not in visited_nodes:
yield from recursively_parse_dag(child)
yield from recursively_parse_dag(dag)

View File

@@ -0,0 +1,249 @@
import json
import logging
from typing import List, Union
from bigtree.node.node import Node
from bigtree.tree.construct import dict_to_tree
from bigtree.tree.export import print_tree, tree_to_dict
from bigtree.tree.search import find_children, find_name
logging.getLogger(__name__).addHandler(logging.NullHandler())
class AppToDo:
"""
To-Do List Implementation with Big Tree.
- To-Do List has three levels - app name, list name, and item name.
- If list name is not given, item will be assigned to a `General` list.
*Initializing and Adding Items*
>>> from bigtree import AppToDo
>>> app = AppToDo("To Do App")
>>> app.add_item(item_name="Homework 1", list_name="School")
>>> app.add_item(item_name=["Milk", "Bread"], list_name="Groceries", description="Urgent")
>>> app.add_item(item_name="Cook")
>>> app.show()
To Do App
├── School
│ └── Homework 1
├── Groceries
│ ├── Milk [description=Urgent]
│ └── Bread [description=Urgent]
└── General
└── Cook
*Reorder List and Item*
>>> app.prioritize_list(list_name="General")
>>> app.show()
To Do App
├── General
│ └── Cook
├── School
│ └── Homework 1
└── Groceries
├── Milk [description=Urgent]
└── Bread [description=Urgent]
>>> app.prioritize_item(item_name="Bread")
>>> app.show()
To Do App
├── General
│ └── Cook
├── School
│ └── Homework 1
└── Groceries
├── Bread [description=Urgent]
└── Milk [description=Urgent]
*Removing Items*
>>> app.remove_item("Homework 1")
>>> app.show()
To Do App
├── General
│ └── Cook
└── Groceries
├── Bread [description=Urgent]
└── Milk [description=Urgent]
*Exporting and Importing List*
>>> app.save("list.json")
>>> app2 = AppToDo.load("list.json")
>>> app2.show()
To Do App
├── General
│ └── Cook
└── Groceries
├── Bread [description=Urgent]
└── Milk [description=Urgent]
"""
def __init__(
self,
app_name: str = "",
):
"""Initialize To-Do app
Args:
app_name (str): name of to-do app, optional
"""
self._root = Node(app_name)
def add_list(self, list_name: str, **kwargs) -> Node:
"""Add list to app
If list is present, return list node, else a new list will be created
Args:
list_name (str): name of list
Returns:
(Node)
"""
list_node = find_children(self._root, list_name)
if not list_node:
list_node = Node(list_name, parent=self._root, **kwargs)
logging.info(f"Created list {list_name}")
return list_node
def prioritize_list(self, list_name: str):
"""Prioritize list in app, shift it to be the first list
Args:
list_name (str): name of list
"""
list_node = find_children(self._root, list_name)
if not list_node:
raise ValueError(f"List {list_name} not found")
current_children = list(self._root.children)
current_children.remove(list_node)
current_children.insert(0, list_node)
self._root.children = current_children
def add_item(self, item_name: Union[str, List[str]], list_name: str = "", **kwargs):
"""Add items to list
Args:
item_name (str/List[str]): items to be added
list_name (str): list to add items to, optional
"""
if not isinstance(item_name, str) and not isinstance(item_name, list):
raise TypeError("Invalid data type for item")
# Get list to add to
if list_name:
list_node = self.add_list(list_name)
else:
list_node = self.add_list("General")
# Add items to list
if isinstance(item_name, str):
_ = Node(item_name, parent=list_node, **kwargs)
logging.info(f"Created item {item_name}")
elif isinstance(item_name, list):
for _item in item_name:
_ = Node(_item, parent=list_node, **kwargs)
logging.info(f"Created items {', '.join(item_name)}")
def remove_item(self, item_name: Union[str, List[str]], list_name: str = ""):
"""Remove items from list
Args:
item_name (str/List[str]): items to be added
list_name (str): list to add items to, optional
"""
if not isinstance(item_name, str) and not isinstance(item_name, list):
raise TypeError("Invalid data type for item")
# Check if items can be found
items_to_remove = []
parent_to_check = set()
if list_name:
list_node = find_children(self._root, list_name)
if isinstance(item_name, str):
item_node = find_children(list_node, item_name)
items_to_remove.append(item_node)
parent_to_check.add(item_node.parent)
elif isinstance(item_name, list):
for _item in item_name:
item_node = find_children(list_node, _item)
items_to_remove.append(item_node)
parent_to_check.add(item_node.parent)
else:
if isinstance(item_name, str):
item_node = find_name(self._root, item_name)
items_to_remove.append(item_node)
parent_to_check.add(item_node.parent)
elif isinstance(item_name, list):
for _item in item_name:
item_node = find_name(self._root, _item)
items_to_remove.append(item_node)
parent_to_check.add(item_node.parent)
# Remove items
for item_node in items_to_remove:
item_node.parent = None
logging.info(
f"Removed items {', '.join(item.node_name for item in items_to_remove)}"
)
# Remove list if empty
for list_node in parent_to_check:
if not len(list(list_node.children)):
list_node.parent = None
logging.info(f"Removed list {list_node.node_name}")
def prioritize_item(self, item_name: str):
"""Prioritize item in list, shift it to be the first item in list
Args:
item_name (str): name of item
"""
item_node = find_name(self._root, item_name)
if not item_node:
raise ValueError(f"Item {item_node} not found")
current_parent = item_node.parent
current_children = list(current_parent.children)
current_children.remove(item_node)
current_children.insert(0, item_node)
current_parent.children = current_children
def show(self, **kwargs):
"""Print tree to console"""
print_tree(self._root, all_attrs=True, **kwargs)
@staticmethod
def load(json_path: str):
"""Load To-Do app from json
Args:
json_path (str): json load path
Returns:
(Self)
"""
if not json_path.endswith(".json"):
raise ValueError("Path should end with .json")
with open(json_path, "r") as fp:
app_dict = json.load(fp)
_app = AppToDo("dummy")
AppToDo.__setattr__(_app, "_root", dict_to_tree(app_dict["root"]))
return _app
def save(self, json_path: str):
"""Save To-Do app as json
Args:
json_path (str): json save path
"""
if not json_path.endswith(".json"):
raise ValueError("Path should end with .json")
node_dict = tree_to_dict(self._root, all_attrs=True)
app_dict = {"root": node_dict}
with open(json_path, "w") as fp:
json.dump(app_dict, fp)

Binary file not shown.