Source code for session_py.tree
import uuid
from typing import Any, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from .treenode import TreeNode
[docs]
class Tree:
"""A hierarchical data structure with parent-child relationships.
Parameters
----------
name : str, optional
The name of the tree. Defaults to "Tree".
Attributes
----------
guid : UUID
The unique identifier of the tree.
name : str
The name of the tree.
root : :class:`TreeNode`
The root node of the tree.
"""
[docs]
def __init__(self, name="my_tree"):
self.guid = str(uuid.uuid4())
self.name = name
self._root = None
def __str__(self):
return "<Tree with {} nodes>".format(len(list(self.nodes)))
def __repr__(self):
return "<Tree with {} nodes>".format(len(list(self.nodes)))
###########################################################################################
# JSON (polymorphic)
###########################################################################################
def __jsondump__(self) -> dict:
"""Serialize to polymorphic JSON format with type field."""
return {
"type": f"{self.__class__.__name__}",
"guid": self.guid,
"name": self.name,
"root": self.root.__jsondump__() if self.root else None,
}
@classmethod
def __jsonload__(
cls, data: dict, guid: Optional[str] = None, name: Optional[str] = None
) -> "Tree":
"""Deserialize from polymorphic JSON format."""
tree = cls(name=data.get("name", "Tree"))
tree.guid = guid if guid is not None else data.get("guid", tree.guid)
if data.get("root"):
from .encoders import decode_node
root = decode_node(data["root"])
tree.add(root)
return tree
###########################################################################################
# Details
###########################################################################################
@property
def root(self):
return self._root
[docs]
def add(self, node, parent=None):
"""Add a node to the tree.
Parameters
----------
node : :class:`TreeNode`
The node to add.
parent : :class:`TreeNode`, optional
The parent node. If None, adds as root.
"""
from .treenode import TreeNode
if not isinstance(node, TreeNode):
raise TypeError("The node is not a TreeNode object.")
if node.parent:
raise ValueError(
"The node already has a parent, remove it from that parent first."
)
if parent is None:
# add the node as a root node
if self.root is not None:
raise ValueError("The tree already has a root node, remove it first.")
self._root = node
node._tree = self # type: ignore
else:
# add the node as a child of the parent node
if not isinstance(parent, TreeNode):
raise TypeError("The parent node is not a TreeNode object.")
if parent.tree is not self:
raise ValueError("The parent node is not part of this tree.")
parent.add(node)
@property
def nodes(self):
if self.root:
for node in self.root.traverse():
yield node
[docs]
def remove(self, node):
"""Remove a node from the tree.
Parameters
----------
node : :class:`TreeNode`
The node to remove.
"""
if node == self.root:
self._root = None
node._tree = None
else:
node.parent.remove(node)
@property
def leaves(self):
for node in self.nodes:
if node.is_leaf:
yield node
[docs]
def traverse(self, strategy="depthfirst", order="preorder"):
"""
Traverse the tree from the root node.
Parameters
----------
strategy : {"depthfirst", "breadthfirst"}, optional
The traversal strategy.
order : {"preorder", "postorder"}, optional
The traversal order. This parameter is only used for depth-first traversal.
Yields
------
:class:`TreeNode`
The next node in the traversal.
Raises
------
ValueError
If the strategy is not ``"depthfirst"`` or ``"breadthfirst"``.
If the order is not ``"preorder"`` or ``"postorder"``.
"""
if self.root:
for node in self.root.traverse(strategy=strategy, order=order):
yield node
[docs]
def get_node_by_name(self, name):
"""Get a node by its name.
Parameters
----------
name : str
The name of the node.
"""
for node in self.nodes:
if node.name == name:
return node
[docs]
def get_nodes_by_name(self, name):
"""
Get all nodes by their name.
Parameters
----------
name : str
The name of the node.
Returns
-------
list[:class:`TreeNode`]
The nodes.
"""
nodes = []
for node in self.nodes:
if node.name == name:
nodes.append(node)
return nodes
[docs]
def add_child_by_guid(self, parent_guid: uuid.UUID, child_guid: uuid.UUID) -> bool:
"""
Add a parent-child relationship using GUIDs.
Parameters
----------
parent_guid : UUID
The GUID of the parent node.
child_guid : UUID
The GUID of the child node.
Returns
-------
bool
True if the relationship was added, False if nodes not found.
"""
parent_node = self.find_node_by_guid(parent_guid)
child_node = self.find_node_by_guid(child_guid)
if not parent_node or not child_node:
return False
# Remove child from its current parent if it has one
if child_node.parent:
child_node.parent.remove(child_node)
# Add to new parent
parent_node.add(child_node)
return True
[docs]
def get_children_guids(self, guid: uuid.UUID) -> list[uuid.UUID]:
"""
Get all children GUIDs of a node by its GUID.
Parameters
----------
guid : UUID
The GUID of the parent node.
Returns
-------
list[UUID]
List of children GUIDs.
"""
node = self.find_node_by_guid(guid)
if not node:
return []
return [child.guid for child in node.children if hasattr(child, "guid")]
[docs]
def print_hierarchy(self):
"""Print the spatial hierarchy of the tree."""
def _print(node, prefix="", last=True):
connector = "└── " if last else "├── "
print("{}{}{}".format(prefix, connector, node))
prefix += " " if last else "│ "
for i, child in enumerate(node.children):
_print(child, prefix, i == len(node.children) - 1)
if self.root:
_print(self.root)
else:
print("Empty tree")