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