262 lines
7.7 KiB
Python
262 lines
7.7 KiB
Python
from __future__ import annotations
|
|
|
|
from collections import Counter
|
|
from typing import Any, List, TypeVar
|
|
|
|
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.
|
|
|
|
Examples:
|
|
>>> 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`
|
|
|
|
**Node Methods**
|
|
|
|
These are methods available to be performed on `Node`.
|
|
|
|
`Node` methods
|
|
|
|
1. ``show()``: Print tree to console
|
|
2. ``hshow()``: Print tree in horizontal orientation to console
|
|
|
|
----
|
|
|
|
"""
|
|
|
|
def __init__(self, name: str = "", sep: str = "/", **kwargs: Any):
|
|
self.name = name
|
|
self._sep = sep
|
|
super().__init__(**kwargs)
|
|
if not self.node_name:
|
|
raise TreeError("Node must have a `name` attribute")
|
|
|
|
@property
|
|
def sep(self) -> str:
|
|
"""Get separator, gets from root node
|
|
|
|
Returns:
|
|
(str)
|
|
"""
|
|
if self.parent is None:
|
|
return self._sep
|
|
return self.parent.sep
|
|
|
|
@sep.setter
|
|
def sep(self, value: str) -> None:
|
|
"""Set separator, affects root node
|
|
|
|
Args:
|
|
value (str): separator to replace default separator
|
|
"""
|
|
self.root._sep = value
|
|
|
|
@property
|
|
def node_name(self) -> str:
|
|
"""Get node name
|
|
|
|
Returns:
|
|
(str)
|
|
"""
|
|
return self.name
|
|
|
|
@property
|
|
def path_name(self) -> str:
|
|
"""Get path name, separated by self.sep
|
|
|
|
Returns:
|
|
(str)
|
|
"""
|
|
ancestors = [self] + list(self.ancestors)
|
|
sep = ancestors[-1].sep
|
|
return sep + sep.join([str(node.name) for node in reversed(ancestors)])
|
|
|
|
def __pre_assign_children(self: T, new_children: List[T]) -> None:
|
|
"""Custom method to check before attaching children
|
|
Can be overridden with `_Node__pre_assign_children()`
|
|
|
|
Args:
|
|
new_children (List[Self]): new children to be added
|
|
"""
|
|
pass
|
|
|
|
def __post_assign_children(self: T, new_children: List[T]) -> None:
|
|
"""Custom method to check after attaching children
|
|
Can be overridden with `_Node__post_assign_children()`
|
|
|
|
Args:
|
|
new_children (List[Self]): new children to be added
|
|
"""
|
|
pass
|
|
|
|
def __pre_assign_parent(self: T, new_parent: T) -> None:
|
|
"""Custom method to check before attaching parent
|
|
Can be overridden with `_Node__pre_assign_parent()`
|
|
|
|
Args:
|
|
new_parent (Self): new parent to be added
|
|
"""
|
|
pass
|
|
|
|
def __post_assign_parent(self: T, new_parent: T) -> None:
|
|
"""Custom method to check after attaching parent
|
|
Can be overridden with `_Node__post_assign_parent()`
|
|
|
|
Args:
|
|
new_parent (Self): new parent to be added
|
|
"""
|
|
pass
|
|
|
|
def _BaseNode__pre_assign_parent(self: T, new_parent: T) -> None:
|
|
"""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"Duplicate node with same path\n"
|
|
f"There exist a node with same path {new_parent.path_name}{new_parent.sep}{self.node_name}"
|
|
)
|
|
|
|
def _BaseNode__post_assign_parent(self: T, new_parent: T) -> None:
|
|
"""No rules
|
|
|
|
Args:
|
|
new_parent (Self): new parent to be added
|
|
"""
|
|
self.__post_assign_parent(new_parent)
|
|
|
|
def _BaseNode__pre_assign_children(self: T, new_children: List[T]) -> None:
|
|
"""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]
|
|
duplicate_names = [
|
|
item[0] for item in Counter(children_names).items() if item[1] > 1
|
|
]
|
|
if len(duplicate_names):
|
|
duplicate_names_str = " and ".join(
|
|
[f"{self.path_name}{self.sep}{name}" for name in duplicate_names]
|
|
)
|
|
raise TreeError(
|
|
f"Duplicate node with same path\n"
|
|
f"Attempting to add nodes with same path {duplicate_names_str}"
|
|
)
|
|
|
|
def _BaseNode__post_assign_children(self: T, new_children: List[T]) -> None:
|
|
"""No rules
|
|
|
|
Args:
|
|
new_children (List[Self]): new children to be added
|
|
"""
|
|
self.__post_assign_children(new_children)
|
|
|
|
def show(self, **kwargs: Any) -> None:
|
|
"""Print tree to console, takes in same keyword arguments as `print_tree` function"""
|
|
from bigtree.tree.export import print_tree
|
|
|
|
print_tree(self, **kwargs)
|
|
|
|
def hshow(self, **kwargs: Any) -> None:
|
|
"""Print tree in horizontal orientation to console, takes in same keyword arguments as `hprint_tree` function"""
|
|
from bigtree.tree.export import hprint_tree
|
|
|
|
hprint_tree(self, **kwargs)
|
|
|
|
def __getitem__(self, child_name: str) -> T:
|
|
"""Get child by name identifier
|
|
|
|
Args:
|
|
child_name (str): name of child node
|
|
|
|
Returns:
|
|
(Self): child node
|
|
"""
|
|
from bigtree.tree.search import find_child_by_name
|
|
|
|
return find_child_by_name(self, child_name) # type: ignore
|
|
|
|
def __delitem__(self, child_name: str) -> None:
|
|
"""Delete child by name identifier, will not throw error if child does not exist
|
|
|
|
Args:
|
|
child_name (str): name of child node
|
|
"""
|
|
from bigtree.tree.search import find_child_by_name
|
|
|
|
child = find_child_by_name(self, child_name)
|
|
if child:
|
|
child.parent = None
|
|
|
|
def __repr__(self) -> str:
|
|
"""Print format of Node
|
|
|
|
Returns:
|
|
(str)
|
|
"""
|
|
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})"
|
|
|
|
|
|
T = TypeVar("T", bound=Node)
|