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)