added 3rd party packages, elog, bigtree
This commit is contained in:
68
python37/packages/bigtree/__init__.py
Normal file
68
python37/packages/bigtree/__init__.py
Normal 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
|
||||
50
python37/packages/bigtree/binarytree/construct.py
Normal file
50
python37/packages/bigtree/binarytree/construct.py
Normal 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
|
||||
0
python37/packages/bigtree/dag/__init__.py
Normal file
0
python37/packages/bigtree/dag/__init__.py
Normal file
186
python37/packages/bigtree/dag/construct.py
Normal file
186
python37/packages/bigtree/dag/construct.py
Normal 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
|
||||
269
python37/packages/bigtree/dag/export.py
Normal file
269
python37/packages/bigtree/dag/export.py
Normal 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
|
||||
0
python37/packages/bigtree/node/__init__.py
Normal file
0
python37/packages/bigtree/node/__init__.py
Normal file
696
python37/packages/bigtree/node/basenode.py
Normal file
696
python37/packages/bigtree/node/basenode.py
Normal 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
|
||||
395
python37/packages/bigtree/node/binarynode.py
Normal file
395
python37/packages/bigtree/node/binarynode.py
Normal 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})"
|
||||
570
python37/packages/bigtree/node/dagnode.py
Normal file
570
python37/packages/bigtree/node/dagnode.py
Normal 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})"
|
||||
204
python37/packages/bigtree/node/node.py
Normal file
204
python37/packages/bigtree/node/node.py
Normal 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})"
|
||||
0
python37/packages/bigtree/tree/__init__.py
Normal file
0
python37/packages/bigtree/tree/__init__.py
Normal file
914
python37/packages/bigtree/tree/construct.py
Normal file
914
python37/packages/bigtree/tree/construct.py
Normal 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
|
||||
831
python37/packages/bigtree/tree/export.py
Normal file
831
python37/packages/bigtree/tree/export.py
Normal 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
|
||||
201
python37/packages/bigtree/tree/helper.py
Normal file
201
python37/packages/bigtree/tree/helper.py
Normal 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__,
|
||||
)
|
||||
856
python37/packages/bigtree/tree/modify.py
Normal file
856
python37/packages/bigtree/tree/modify.py
Normal 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
|
||||
316
python37/packages/bigtree/tree/search.py
Normal file
316
python37/packages/bigtree/tree/search.py
Normal 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]
|
||||
0
python37/packages/bigtree/utils/__init__.py
Normal file
0
python37/packages/bigtree/utils/__init__.py
Normal file
32
python37/packages/bigtree/utils/exceptions.py
Normal file
32
python37/packages/bigtree/utils/exceptions.py
Normal 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
|
||||
371
python37/packages/bigtree/utils/iterators.py
Normal file
371
python37/packages/bigtree/utils/iterators.py
Normal 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)
|
||||
0
python37/packages/bigtree/workflows/__init__.py
Normal file
0
python37/packages/bigtree/workflows/__init__.py
Normal file
249
python37/packages/bigtree/workflows/app_todo.py
Normal file
249
python37/packages/bigtree/workflows/app_todo.py
Normal 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)
|
||||
BIN
python37/packages/bigtree_info
Normal file
BIN
python37/packages/bigtree_info
Normal file
Binary file not shown.
Reference in New Issue
Block a user