Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/treelib/tree.py: 21%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

500 statements  

1#!/usr/bin/env python 

2# -*- coding: utf-8 -*- 

3# Copyright (C) 2011 

4# Brett Alistair Kromkamp - brettkromkamp@gmail.com 

5# Copyright (C) 2012-2025 

6# Xiaming Chen - chenxm35@gmail.com 

7# and other contributors. 

8# All rights reserved. 

9""" 

10Tree structure in `treelib`. 

11 

12The :class:`Tree` object defines the tree-like structure based on :class:`Node` objects. 

13Trees provide hierarchical data organization with efficient operations for traversal, 

14modification, search, and visualization. 

15 

16A new tree can be created from scratch without any parameter or a shallow/deep copy of another tree. 

17When deep=True, a deepcopy operation is performed on feeding tree parameter and more memory 

18is required to create the tree. 

19 

20Key Features: 

21 - O(1) node lookup and access 

22 - Multiple traversal modes (depth-first, breadth-first, zigzag) 

23 - Flexible tree modification (add, remove, move, paste) 

24 - Rich display options with customizable formatting 

25 - JSON/dict export and import capabilities 

26 - Subtree operations and filtering 

27 - Tree metrics and analysis tools 

28""" 

29from __future__ import print_function, unicode_literals 

30 

31try: 

32 from builtins import str as text 

33except ImportError: 

34 from __builtin__ import str as text # type: ignore 

35 

36import codecs 

37import json 

38import sys 

39import uuid 

40from copy import deepcopy 

41from typing import Any, Callable, List, Optional, Union, cast 

42 

43from six import iteritems, python_2_unicode_compatible 

44 

45try: 

46 from StringIO import StringIO # type: ignore 

47except ImportError: 

48 from io import StringIO 

49 

50from .exceptions import ( 

51 DuplicatedNodeIdError, 

52 InvalidLevelNumber, 

53 LinkPastRootNodeError, 

54 LoopError, 

55 MultipleRootError, 

56 NodeIDAbsentError, 

57) 

58from .node import Node 

59 

60if sys.version_info >= (3, 9): 

61 StrList = list[str] 

62 StrLList = list[list[str]] 

63 NodeList = list[Node] 

64else: 

65 StrList = List[str] # Python 3.8 and earlier 

66 StrLList = List[List[str]] 

67 NodeList = List[Node] 

68 

69 

70__author__ = "chenxm" 

71 

72 

73@python_2_unicode_compatible 

74class Tree(object): 

75 """ 

76 Hierarchical tree data structure. 

77 

78 A Tree is a collection of Node objects organized in a hierarchical structure 

79 with exactly one root node and zero or more child nodes. Each node (except root) 

80 has exactly one parent, but can have multiple children. 

81 

82 The tree provides efficient operations for: 

83 - Adding, removing, and moving nodes 

84 - Traversing the tree in different orders 

85 - Searching and filtering nodes 

86 - Displaying tree structure 

87 - Exporting to various formats 

88 

89 Attributes: 

90 root (str): Identifier of the root node, or None if tree is empty. 

91 nodes (dict): Dictionary mapping node identifiers to Node objects. 

92 

93 Constants: 

94 ROOT (int): Tree traversal starting from root level. 

95 DEPTH (int): Depth-first tree traversal mode. 

96 WIDTH (int): Breadth-first tree traversal mode. 

97 ZIGZAG (int): Zigzag tree traversal mode. 

98 

99 Example: 

100 Creating and manipulating a tree:: 

101 

102 tree = Tree() 

103 

104 # Build structure 

105 tree.create_node("Company", "company") 

106 tree.create_node("Engineering", "eng", parent="company") 

107 tree.create_node("Sales", "sales", parent="company") 

108 

109 # Add team members 

110 tree.create_node("Alice", "alice", parent="eng") 

111 tree.create_node("Bob", "bob", parent="eng") 

112 tree.create_node("Carol", "carol", parent="sales") 

113 

114 # Display tree 

115 tree.show() 

116 

117 # Find specific nodes 

118 eng_team = tree.children("eng") 

119 all_employees = [tree[node].tag for node in tree.expand_tree() 

120 if tree.level(node) > 1] 

121 

122 # Tree operations 

123 tree.move_node("alice", "sales") # Alice moves to sales 

124 subtree = tree.subtree("eng") # Get engineering subtree 

125 """ 

126 

127 #: ROOT, DEPTH, WIDTH, ZIGZAG constants : 

128 (ROOT, DEPTH, WIDTH, ZIGZAG) = list(range(4)) 

129 node_class = Node 

130 

131 def __contains__(self, identifier: str) -> bool: 

132 """ 

133 Check if a node with the given identifier exists in this tree. 

134 

135 Implements the 'in' operator for tree membership testing, providing 

136 a convenient way to check node existence without raising exceptions. 

137 

138 Args: 

139 identifier: Node identifier to check for existence. 

140 

141 Returns: 

142 bool: True if node exists in tree, False otherwise. 

143 

144 Example: 

145 Checking node membership:: 

146 

147 if "user123" in tree: 

148 print("User node exists") 

149 user = tree["user123"] 

150 else: 

151 print("User node not found") 

152 

153 # More concise than try/except approach 

154 exists = "node_id" in tree # True or False 

155 """ 

156 return identifier in self.nodes.keys() 

157 

158 def __init__( 

159 self, 

160 tree=None, 

161 deep: bool = False, 

162 node_class=None, 

163 identifier: Optional[str] = None, 

164 ) -> None: 

165 """ 

166 Initialize a new tree or copy another tree. 

167 

168 Creates either an empty tree or a copy of an existing tree. When copying, 

169 you can choose between shallow copy (references to same nodes) or deep copy 

170 (completely independent nodes and data). 

171 

172 Args: 

173 tree: Existing Tree object to copy from. If None, creates empty tree. 

174 deep: If True, perform deep copy of all nodes and their data. 

175 If False, creates shallow copy sharing node references. 

176 node_class: Custom Node class to use instead of default Node. 

177 Must be subclass of Node. 

178 identifier: Unique identifier for this tree instance. 

179 If None, generates UUID automatically. 

180 

181 Example: 

182 Different ways to create trees:: 

183 

184 # Empty tree 

185 tree1 = Tree() 

186 

187 # Tree with custom identifier 

188 tree2 = Tree(identifier="my_tree") 

189 

190 # Shallow copy of existing tree 

191 tree3 = Tree(tree1) 

192 

193 # Deep copy with independent data 

194 tree4 = Tree(tree1, deep=True) 

195 

196 # Custom node class 

197 class MyNode(Node): 

198 def __init__(self, tag, identifier=None): 

199 super().__init__(tag, identifier) 

200 self.custom_attr = "value" 

201 

202 tree5 = Tree(node_class=MyNode) 

203 

204 Raises: 

205 AssertionError: If node_class is not a subclass of Node. 

206 """ 

207 self._identifier: Optional[str] = None 

208 self._set_identifier(identifier) 

209 

210 if node_class: 

211 assert issubclass(node_class, Node) 

212 self.node_class = node_class 

213 

214 #: dictionary, identifier: Node object 

215 self._nodes = {} 

216 

217 #: Get or set the identifier of the root. This attribute can be accessed and modified 

218 #: with ``.`` and ``=`` operator respectively. 

219 self.root: Optional[str] = None 

220 

221 if tree is not None: 

222 self.root = tree.root 

223 for nid, node in iteritems(tree.nodes): 

224 new_node = deepcopy(node) if deep else node 

225 self._nodes[nid] = new_node 

226 if tree.identifier != self._identifier: 

227 new_node.clone_pointers(tree.identifier, self._identifier) 

228 

229 def _clone( 

230 self, 

231 identifier: Optional[str] = None, 

232 with_tree: bool = False, 

233 deep: bool = False, 

234 ): 

235 """ 

236 Create a clone of this tree instance with optional content copying. 

237 

238 This method is designed to be overridden by subclasses to enable 

239 proper cloning of extended tree classes. It provides the foundation 

240 for subtree and remove_subtree operations while maintaining 

241 polymorphic behavior. 

242 

243 Args: 

244 identifier: Unique identifier for the new tree instance. 

245 If None, generates UUID automatically. 

246 with_tree: If True, copy all nodes from current tree. 

247 If False, create empty tree with same class. 

248 deep: If True and with_tree=True, perform deep copy of node data. 

249 If False, create shallow copy sharing node references. 

250 

251 Returns: 

252 Tree: New tree instance of the same class as this tree. 

253 

254 Example: 

255 Subclassing with custom clone behavior:: 

256 

257 class EnhancedTree(Tree): 

258 def __init__(self, metadata=None, **kwargs): 

259 super().__init__(**kwargs) 

260 self.metadata = metadata or {} 

261 

262 def _clone(self, identifier=None, with_tree=False, deep=False): 

263 return EnhancedTree( 

264 metadata=self.metadata.copy(), 

265 identifier=identifier, 

266 tree=self if with_tree else None, 

267 deep=deep 

268 ) 

269 

270 # Custom tree operations preserve metadata 

271 enhanced = EnhancedTree(metadata={"version": "1.0"}) 

272 subtree = enhanced.subtree("node_id") # Preserves metadata 

273 """ 

274 return self.__class__(identifier=identifier, tree=self if with_tree else None, deep=deep) 

275 

276 @property 

277 def identifier(self) -> Optional[str]: 

278 """ 

279 Get the unique identifier of this tree instance. 

280 

281 Each tree has its own unique identifier used to distinguish it from 

282 other trees, especially when nodes exist in multiple trees simultaneously. 

283 This identifier is automatically generated if not provided during creation. 

284 

285 Returns: 

286 str or None: The unique identifier of this tree instance. 

287 

288 Example: 

289 Using tree identifiers:: 

290 

291 tree1 = Tree(identifier="main_tree") 

292 tree2 = Tree() # Auto-generated identifier 

293 

294 print(tree1.identifier) # "main_tree" 

295 print(tree2.identifier) # UUID string like "abc123..." 

296 

297 # Used internally for node relationships 

298 node.predecessor(tree1.identifier) # Parent in tree1 

299 """ 

300 return self._identifier 

301 

302 def _set_identifier(self, nid: Optional[str]) -> None: 

303 """ 

304 Initialize tree identifier with given value or auto-generate one. 

305 

306 Private method used during tree creation to set the unique identifier. 

307 If no identifier is provided, generates a UUID automatically to ensure 

308 uniqueness across tree instances. 

309 

310 Args: 

311 nid: Desired tree identifier, or None to auto-generate. 

312 

313 Note: 

314 This is an internal method used during tree initialization. 

315 The identifier should not be changed after tree creation. 

316 """ 

317 if nid is None: 

318 self._identifier = str(uuid.uuid1()) 

319 else: 

320 self._identifier = nid 

321 

322 def __getitem__(self, key: str) -> Node: 

323 """ 

324 Get node by identifier using bracket notation. 

325 

326 Provides convenient dictionary-style access to nodes. Raises exception 

327 if node doesn't exist, making it clear when invalid identifiers are used. 

328 For safer access that returns None instead of raising, use get_node(). 

329 

330 Args: 

331 key: Node identifier to retrieve. 

332 

333 Returns: 

334 Node: The node object with the specified identifier. 

335 

336 Raises: 

337 NodeIDAbsentError: If no node with the given identifier exists. 

338 

339 Example: 

340 Accessing nodes by identifier:: 

341 

342 # Direct access (raises exception if not found) 

343 user_node = tree["user123"] 

344 profile_node = tree["user123_profile"] 

345 

346 # Use with in operator for safety 

347 if "user123" in tree: 

348 user_node = tree["user123"] 

349 print(user_node.tag) 

350 """ 

351 try: 

352 return self._nodes[key] 

353 except KeyError: 

354 raise NodeIDAbsentError("Node '%s' is not in the tree" % key) 

355 

356 def __len__(self) -> int: 

357 """ 

358 Get the total number of nodes in this tree. 

359 

360 Returns the count of all nodes currently in the tree, including 

361 the root node. Useful for tree size analysis and iteration bounds. 

362 

363 Returns: 

364 int: Total number of nodes in the tree. 

365 

366 Example: 

367 Getting tree size:: 

368 

369 tree_size = len(tree) 

370 print(f"Tree has {tree_size} nodes") 

371 

372 # Empty tree check 

373 if len(tree) == 0: 

374 print("Tree is empty") 

375 

376 # Use in comparisons 

377 if len(tree1) > len(tree2): 

378 print("Tree1 is larger") 

379 """ 

380 return len(self._nodes) 

381 

382 def __str__(self) -> str: 

383 """ 

384 Get string representation of the tree structure. 

385 

386 Returns a formatted tree visualization showing the hierarchical structure 

387 with default display settings. Useful for debugging, logging, and quick 

388 inspection of tree contents. 

389 

390 Returns: 

391 str: Formatted tree structure as string. 

392 

393 Example: 

394 Tree string representation:: 

395 

396 tree = Tree() 

397 tree.create_node("Root", "root") 

398 tree.create_node("Child A", "a", parent="root") 

399 tree.create_node("Child B", "b", parent="root") 

400 

401 print(str(tree)) 

402 # Output: 

403 # Root 

404 # ├── Child A 

405 # └── Child B 

406 

407 # Use in logging 

408 logger.info(f"Current tree structure:\n{tree}") 

409 """ 

410 self._reader = "" 

411 

412 def write(line): 

413 self._reader += line.decode("utf-8") + "\n" 

414 

415 self.__print_backend(func=write) 

416 return self._reader 

417 

418 def __print_backend( 

419 self, 

420 nid: Optional[str] = None, 

421 level: int = ROOT, 

422 idhidden: bool = True, 

423 filter: Optional[Callable[[Node], bool]] = None, 

424 key: Optional[Callable[[Node], Node]] = None, 

425 reverse: bool = False, 

426 line_type="ascii-ex", 

427 data_property=None, 

428 sorting: bool = True, 

429 func: Callable[[bytes], None] = print, 

430 ): 

431 """ 

432 Another implementation of printing tree using Stack 

433 Print tree structure in hierarchy style. 

434 

435 For example: 

436 

437 .. code-block:: bash 

438 

439 Root 

440 |___ C01 

441 | |___ C11 

442 | |___ C111 

443 | |___ C112 

444 |___ C02 

445 |___ C03 

446 | |___ C31 

447 

448 A more elegant way to achieve this function using Stack 

449 structure, for constructing the Nodes Stack push and pop nodes 

450 with additional level info. 

451 

452 UPDATE: the @key @reverse is present to sort node at each 

453 level. 

454 """ 

455 # Factory for proper get_label() function 

456 if data_property: 

457 if idhidden: 

458 

459 def get_label(node: Node) -> str: 

460 return getattr(node.data, data_property) 

461 

462 else: 

463 

464 def get_label(node: Node) -> str: 

465 return "%s[%s]" % ( 

466 getattr(node.data, data_property), 

467 node.identifier, 

468 ) 

469 

470 else: 

471 if idhidden: 

472 

473 def get_label(node: Node) -> str: 

474 return node.tag 

475 

476 else: 

477 

478 def get_label(node: Node) -> str: 

479 return "%s[%s]" % (node.tag, node.identifier) 

480 

481 # legacy ordering 

482 if sorting and key is None: 

483 

484 def key(node): 

485 return node 

486 

487 # iter with func 

488 for pre, node in self.__get(nid, level, filter, key, reverse, line_type, sorting): 

489 label = get_label(node) 

490 func("{0}{1}".format(pre, label).encode("utf-8")) 

491 

492 def __get( 

493 self, 

494 nid: Optional[str], 

495 level: int, 

496 filter_: Optional[Callable[[Node], bool]], 

497 key: Optional[Callable[[Node], Node]], 

498 reverse: bool, 

499 line_type: str, 

500 sorting: bool, 

501 ): 

502 # default filter 

503 if filter_ is None: 

504 

505 def filter_(node): 

506 return True 

507 

508 # render characters 

509 dt = { 

510 "ascii": ("|", "|-- ", "+-- "), 

511 "ascii-ex": ("\u2502", "\u251c\u2500\u2500 ", "\u2514\u2500\u2500 "), 

512 "ascii-exr": ("\u2502", "\u251c\u2500\u2500 ", "\u2570\u2500\u2500 "), 

513 "ascii-em": ("\u2551", "\u2560\u2550\u2550 ", "\u255a\u2550\u2550 "), 

514 "ascii-emv": ("\u2551", "\u255f\u2500\u2500 ", "\u2559\u2500\u2500 "), 

515 "ascii-emh": ("\u2502", "\u255e\u2550\u2550 ", "\u2558\u2550\u2550 "), 

516 }[line_type] 

517 

518 return self.__get_iter(nid, level, filter_, key, reverse, dt, [], sorting) 

519 

520 def __get_iter( 

521 self, 

522 nid: Optional[str], 

523 level: int, 

524 filter_: Callable[[Node], bool], 

525 key: Optional[Callable[[Node], Node]], 

526 reverse: bool, 

527 dt, # tuple[str, str, str] 

528 is_last: list, 

529 sorting: bool, 

530 ): 

531 dt_vertical_line, dt_line_box, dt_line_corner = dt 

532 

533 nid = cast(str, self.root if nid is None else nid) 

534 node = self[nid] 

535 

536 if level == self.ROOT: 

537 yield "", node 

538 else: 

539 leading = "".join( 

540 map( 

541 lambda x: dt_vertical_line + " " * 3 if not x else " " * 4, 

542 is_last[0:-1], 

543 ) 

544 ) 

545 lasting = dt_line_corner if is_last[-1] else dt_line_box 

546 yield leading + lasting, node 

547 

548 if filter_(node) and node.expanded: 

549 children = [self[i] for i in node.successors(self._identifier) if filter_(self[i])] 

550 idxlast = len(children) - 1 

551 if sorting: 

552 if key: 

553 children.sort(key=key, reverse=reverse) 

554 elif reverse: 

555 children = list(reversed(children)) 

556 level += 1 

557 for idx, child in enumerate(children): 

558 is_last.append(idx == idxlast) 

559 for item in self.__get_iter(child.identifier, level, filter_, key, reverse, dt, is_last, sorting): 

560 yield item 

561 is_last.pop() 

562 

563 def __update_bpointer(self, nid, parent_id): 

564 """set self[nid].bpointer""" 

565 self[nid].set_predecessor(parent_id, self._identifier) 

566 

567 def __update_fpointer(self, nid: str, child_id: str, mode: int): 

568 if nid is None: 

569 return 

570 else: 

571 self[nid].update_successors(child_id, mode, tree_id=self._identifier) 

572 

573 def add_node(self, node: Node, parent: Optional[Union[Node, str]] = None) -> None: 

574 """ 

575 Add an existing node object to the tree. 

576 

577 Integrates a pre-created Node instance into the tree structure by 

578 establishing parent-child relationships and updating internal mappings. 

579 For creating and adding nodes simultaneously, use create_node() instead. 

580 

581 Args: 

582 node: Existing Node instance to add to the tree. 

583 Must be an instance of the tree's node_class. 

584 parent: Parent node (Node object or identifier string), or None 

585 to make this node the root. If None, tree must be empty. 

586 

587 Raises: 

588 OSError: If node is not an instance of the expected node class. 

589 DuplicatedNodeIdError: If node identifier already exists in tree. 

590 MultipleRootError: If parent is None but tree already has a root. 

591 NodeIDAbsentError: If parent identifier doesn't exist in tree. 

592 

593 Example: 

594 Adding pre-created nodes:: 

595 

596 # Create nodes first 

597 root_node = Node("Company", "company") 

598 dept_node = Node("Engineering", "eng") 

599 

600 # Add to tree 

601 tree.add_node(root_node) # Root node 

602 tree.add_node(dept_node, parent="company") # Child node 

603 

604 # Adding with Node object as parent 

605 mgr_node = Node("Manager", "mgr") 

606 tree.add_node(mgr_node, parent=dept_node) 

607 """ 

608 if not isinstance(node, self.node_class): 

609 raise OSError("First parameter must be object of {}".format(self.node_class)) 

610 

611 if node.identifier in self._nodes: 

612 raise DuplicatedNodeIdError("Can't create node " "with ID '%s'" % node.identifier) 

613 

614 pid = parent.identifier if isinstance(parent, self.node_class) else parent 

615 

616 if pid is None: 

617 if self.root is not None: 

618 raise MultipleRootError("A tree takes one root merely.") 

619 else: 

620 self.root = node.identifier 

621 elif not self.contains(pid): 

622 raise NodeIDAbsentError("Parent node '%s' " "is not in the tree" % pid) 

623 

624 pid = cast(str, pid) 

625 self._nodes.update({node.identifier: node}) 

626 self.__update_fpointer(pid, node.identifier, self.node_class.ADD) 

627 self.__update_bpointer(node.identifier, pid) 

628 node.set_initial_tree_id(cast(str, self._identifier)) 

629 

630 def all_nodes(self) -> NodeList: 

631 """ 

632 Get all nodes in the tree as a list. 

633 

634 Returns a list containing all Node objects currently in the tree. 

635 The order is not guaranteed. For ordered traversal, use expand_tree(). 

636 Useful for operations that need to process all nodes simultaneously. 

637 

638 Returns: 

639 list[Node]: List of all Node objects in the tree. 

640 

641 Example: 

642 Processing all nodes:: 

643 

644 # Get all nodes 

645 all_nodes = tree.all_nodes() 

646 print(f"Total nodes: {len(all_nodes)}") 

647 

648 # Process each node 

649 for node in all_nodes: 

650 print(f"Node: {node.tag}") 

651 

652 # Filter nodes by property 

653 leaf_nodes = [node for node in tree.all_nodes() 

654 if node.is_leaf(tree.identifier)] 

655 """ 

656 return list(self._nodes.values()) 

657 

658 def all_nodes_itr(self) -> Any: 

659 """ 

660 Get all nodes in the tree as an iterator. 

661 

662 Returns an iterator over all Node objects in the tree, providing 

663 memory-efficient access for large trees. Useful when you need to 

664 process nodes one at a time without loading all into memory. 

665 

666 Returns: 

667 Iterator[Node]: Iterator over all Node objects in the tree. 

668 

669 Example: 

670 Memory-efficient node processing:: 

671 

672 # Iterate without loading all nodes 

673 for node in tree.all_nodes_itr(): 

674 if node.tag.startswith("temp_"): 

675 process_temporary_node(node) 

676 

677 # Use with filter operations 

678 filtered = filter(lambda n: n.data is not None, 

679 tree.all_nodes_itr()) 

680 

681 Note: 

682 Added by William Rusnack for memory efficiency. 

683 """ 

684 return self._nodes.values() 

685 

686 def ancestor(self, nid, level=None): 

687 """ 

688 Get the ancestor node at a specific level above the given node. 

689 

690 Traverses up the tree hierarchy from the specified node to find an 

691 ancestor at the desired level. If no level is specified, returns 

692 the immediate parent. Useful for navigating tree hierarchies. 

693 

694 Args: 

695 nid: Identifier of the node to start from. 

696 level: Target level of the ancestor (0 = root). If None, 

697 returns immediate parent. 

698 

699 Returns: 

700 str or None: Identifier of the ancestor node, or None if not found. 

701 

702 Raises: 

703 NodeIDAbsentError: If nid doesn't exist in the tree. 

704 InvalidLevelNumber: If level is invalid (>= descendant level). 

705 

706 Example: 

707 Finding ancestors:: 

708 

709 # Get immediate parent 

710 parent_id = tree.ancestor("grandchild") 

711 

712 # Get ancestor at specific level 

713 root_id = tree.ancestor("grandchild", level=0) # Root 

714 grandparent_id = tree.ancestor("grandchild", level=1) 

715 

716 # Use in hierarchy navigation 

717 if tree.ancestor("node", level=0) == "root": 

718 print("Node is in main hierarchy") 

719 """ 

720 if not self.contains(nid): 

721 raise NodeIDAbsentError("Node '%s' is not in the tree" % nid) 

722 

723 descendant = self[nid] 

724 ascendant = self[nid]._predecessor[self._identifier] 

725 ascendant_level = self.level(ascendant) 

726 

727 if level is None: 

728 return ascendant 

729 elif nid == self.root: 

730 return self[nid] 

731 elif level >= self.level(descendant.identifier): 

732 raise InvalidLevelNumber( 

733 "Descendant level (level %s) must be greater \ 

734 than its ancestor's level (level %s)" 

735 % (str(self.level(descendant.identifier)), level) 

736 ) 

737 

738 while ascendant is not None: 

739 if ascendant_level == level: 

740 return self[ascendant] 

741 else: 

742 descendant = ascendant 

743 ascendant = self[descendant]._predecessor[self._identifier] 

744 ascendant_level = self.level(ascendant) 

745 return None 

746 

747 def children(self, nid: str) -> NodeList: 

748 """ 

749 Get all direct children of the specified node. 

750 

751 Returns a list of Node objects that are immediate children of the 

752 given node. The order follows the insertion order unless the tree 

753 has been sorted. Returns empty list if node has no children. 

754 

755 Args: 

756 nid: Identifier of the parent node. 

757 

758 Returns: 

759 list[Node]: List of child Node objects. Empty if no children. 

760 

761 Raises: 

762 NodeIDAbsentError: If nid doesn't exist in the tree. 

763 

764 Example: 

765 Working with child nodes:: 

766 

767 # Get all children 

768 child_nodes = tree.children("parent_id") 

769 print(f"Parent has {len(child_nodes)} children") 

770 

771 # Process each child 

772 for child in tree.children("parent_id"): 

773 print(f"Child: {child.tag}") 

774 

775 # Check if node has children 

776 if tree.children("node_id"): 

777 print("Node has children") 

778 else: 

779 print("Node is a leaf") 

780 """ 

781 return [self[i] for i in self.is_branch(nid)] 

782 

783 def contains(self, nid): 

784 """ 

785 Check if the tree contains a node with the given identifier. 

786 

787 Determines whether a node with the specified identifier exists 

788 in this tree. Equivalent to using the 'in' operator but provided 

789 as an explicit method for clarity and consistency. 

790 

791 Args: 

792 nid: Node identifier to check for existence. 

793 

794 Returns: 

795 bool: True if node exists in tree, False otherwise. 

796 

797 Example: 

798 Checking node existence:: 

799 

800 # Explicit method call 

801 if tree.contains("user123"): 

802 print("User node exists") 

803 

804 # Equivalent using 'in' operator 

805 if "user123" in tree: 

806 print("User node exists") 

807 

808 # Use before operations 

809 if tree.contains("node_id"): 

810 tree.move_node("node_id", "new_parent") 

811 """ 

812 return True if nid in self._nodes else False 

813 

814 def create_node( 

815 self, 

816 tag: Optional[str] = None, 

817 identifier: Optional[str] = None, 

818 parent: Optional[Union[Node, str]] = None, 

819 data: Any = None, 

820 ) -> Node: 

821 """ 

822 Create and add a new node to the tree. 

823 

824 This is the primary method for building tree structures. Creates a new node 

825 with the specified properties and attaches it to the given parent. If no 

826 parent is specified, the node becomes the root (only allowed if tree is empty). 

827 

828 Args: 

829 tag: Human-readable label for display. If None, uses identifier. 

830 identifier: Unique ID for the node. If None, generates UUID automatically. 

831 parent: Parent node identifier, Node object, or None for root. 

832 Must be None if tree is empty, must exist if tree has nodes. 

833 data: Optional user data to associate with this node. 

834 

835 Returns: 

836 Node: The newly created Node object. 

837 

838 Raises: 

839 DuplicatedNodeIdError: If identifier already exists in the tree. 

840 MultipleRootError: If parent is None but tree already has a root. 

841 NodeIDAbsentError: If parent identifier doesn't exist in the tree. 

842 

843 Example: 

844 Building a family tree:: 

845 

846 tree = Tree() 

847 

848 # Create root (no parent) 

849 tree.create_node("Grandpa", "grandpa") 

850 

851 # Add children 

852 tree.create_node("Dad", "dad", parent="grandpa") 

853 tree.create_node("Uncle", "uncle", parent="grandpa") 

854 

855 # Add grandchildren 

856 tree.create_node("Me", "me", parent="dad") 

857 tree.create_node("Sister", "sister", parent="dad") 

858 

859 # Add node with custom data 

860 tree.create_node("Baby", "baby", parent="me", 

861 data={"age": 1, "cute": True}) 

862 """ 

863 node = self.node_class(tag=tag, identifier=identifier, data=data) 

864 self.add_node(node, parent) 

865 return node 

866 

867 def depth(self, node: Optional[Node] = None) -> int: 

868 """ 

869 Get the maximum depth of the tree or the level of a specific node. 

870 

871 When called without arguments, returns the maximum depth of the entire 

872 tree (distance from root to deepest leaf). When called with a node, 

873 returns the level of that specific node (distance from root). 

874 

875 Args: 

876 node: Node object or identifier string. If None, returns tree depth. 

877 If provided, returns the level of this specific node. 

878 

879 Returns: 

880 int: Tree depth (max level) or node level. Root is at level 0. 

881 

882 Raises: 

883 NodeIDAbsentError: If specified node doesn't exist in the tree. 

884 

885 Example: 

886 Measuring tree dimensions:: 

887 

888 # Get overall tree depth 

889 max_depth = tree.depth() 

890 print(f"Tree depth: {max_depth}") 

891 

892 # Get specific node level 

893 node_level = tree.depth("some_node") 

894 node_level2 = tree.depth(tree["some_node"]) # Same result 

895 

896 # Use for tree analysis 

897 if tree.depth() > 5: 

898 print("Tree is quite deep") 

899 """ 

900 ret = 0 

901 if node is None: 

902 # Get maximum level of this tree 

903 leaves = self.leaves() 

904 for leave in leaves: 

905 level = self.level(leave.identifier) 

906 ret = level if level >= ret else ret 

907 else: 

908 # Get level of the given node 

909 if not isinstance(node, self.node_class): 

910 nid = node 

911 else: 

912 nid = node.identifier 

913 if not self.contains(nid): 

914 raise NodeIDAbsentError("Node '%s' is not in the tree" % nid) 

915 ret = self.level(nid) 

916 return ret 

917 

918 def expand_tree( 

919 self, 

920 nid: Optional[str] = None, 

921 mode: int = DEPTH, 

922 filter: Optional[Callable[[Node], bool]] = None, 

923 key: Optional[Callable[[Node], Node]] = None, 

924 reverse: bool = False, 

925 sorting: bool = True, 

926 ): 

927 """ 

928 Traverse the tree and yield node identifiers in specified order. 

929 

930 This is the primary method for tree iteration, providing flexible traversal 

931 with multiple algorithms, filtering, and sorting options. Essential for most 

932 tree processing operations. 

933 

934 Args: 

935 nid: Starting node identifier. If None, starts from tree root. 

936 Must exist in the tree if specified. 

937 mode: Traversal algorithm to use: 

938 Tree.DEPTH (0) - Depth-first search (default) 

939 Tree.WIDTH (1) - Breadth-first search 

940 Tree.ZIGZAG (2) - ZigZag traversal (alternating levels) 

941 filter: Optional function to filter nodes during traversal. 

942 Takes Node object, returns bool. If False, node and its 

943 entire subtree are skipped. 

944 key: Sorting function for nodes at each level. Takes Node object, 

945 returns comparison key. If None and sorting=True, sorts by node. 

946 reverse: If True, reverse the sorting order at each level. 

947 sorting: If True, sort nodes at each level using key function. 

948 If False, preserve insertion order (key/reverse ignored). 

949 

950 Yields: 

951 str: Node identifiers in traversal order. 

952 

953 Raises: 

954 NodeIDAbsentError: If nid doesn't exist in the tree. 

955 ValueError: If mode is not a valid traversal constant. 

956 

957 Example: 

958 Different traversal patterns:: 

959 

960 tree = Tree() 

961 tree.create_node("A", "a") 

962 tree.create_node("B", "b", parent="a") 

963 tree.create_node("C", "c", parent="a") 

964 tree.create_node("D", "d", parent="b") 

965 tree.create_node("E", "e", parent="c") 

966 

967 # Depth-first (default): A, B, D, C, E 

968 for node_id in tree.expand_tree(): 

969 print(f"DFS: {tree[node_id].tag}") 

970 

971 # Breadth-first: A, B, C, D, E 

972 for node_id in tree.expand_tree(mode=Tree.WIDTH): 

973 print(f"BFS: {tree[node_id].tag}") 

974 

975 # ZigZag traversal 

976 for node_id in tree.expand_tree(mode=Tree.ZIGZAG): 

977 print(f"ZigZag: {tree[node_id].tag}") 

978 

979 # Filtered traversal (only nodes with vowels) 

980 vowel_filter = lambda node: any(v in node.tag.lower() for v in 'aeiou') 

981 for node_id in tree.expand_tree(filter=vowel_filter): 

982 print(f"Vowels: {tree[node_id].tag}") 

983 

984 # Sorted by tag, reversed 

985 for node_id in tree.expand_tree(key=lambda x: x.tag, reverse=True): 

986 print(f"Sorted: {tree[node_id].tag}") 

987 

988 # From specific subtree 

989 for node_id in tree.expand_tree(nid="b"): 

990 print(f"Subtree: {tree[node_id].tag}") 

991 

992 Common Usage Patterns:: 

993 

994 # Process all nodes 

995 for node_id in tree.expand_tree(): 

996 process_node(tree[node_id]) 

997 

998 # Get all node tags 

999 tags = [tree[nid].tag for nid in tree.expand_tree()] 

1000 

1001 # Find specific nodes 

1002 matching_nodes = [nid for nid in tree.expand_tree() 

1003 if tree[nid].tag.startswith("prefix")] 

1004 

1005 # Level-order processing 

1006 for node_id in tree.expand_tree(mode=Tree.WIDTH): 

1007 level = tree.level(node_id) 

1008 print(f"Level {level}: {tree[node_id].tag}") 

1009 

1010 Performance: 

1011 - Time complexity: O(n) where n is number of visited nodes 

1012 - Memory: O(h) where h is tree height (for traversal stack) 

1013 - Generator-based: memory efficient for large trees 

1014 

1015 Note: 

1016 This is a generator function - it yields results lazily. Perfect for 

1017 large trees where you might not need to visit all nodes, or when 

1018 memory efficiency is important. 

1019 """ 

1020 nid = cast(str, self.root if nid is None else nid) 

1021 if not self.contains(nid): 

1022 raise NodeIDAbsentError("Node '%s' is not in the tree" % nid) 

1023 

1024 filter = (lambda x: True) if (filter is None) else filter 

1025 if filter(self[nid]): 

1026 yield nid 

1027 queue = [self[i] for i in self[nid].successors(self._identifier) if filter(self[i])] 

1028 if mode in [self.DEPTH, self.WIDTH]: 

1029 if sorting: 

1030 queue.sort(key=key, reverse=reverse) 

1031 while queue: 

1032 yield queue[0].identifier 

1033 expansion = [self[i] for i in queue[0].successors(self._identifier) if filter(self[i])] 

1034 if sorting: 

1035 expansion.sort(key=key, reverse=reverse) 

1036 if mode is self.DEPTH: 

1037 queue = expansion + queue[1:] # depth-first 

1038 elif mode is self.WIDTH: 

1039 queue = queue[1:] + expansion # width-first 

1040 

1041 elif mode is self.ZIGZAG: 

1042 # Suggested by Ilya Kuprik (ilya-spy@ynadex.ru). 

1043 stack_fw: NodeList = [] 

1044 queue.reverse() 

1045 stack = stack_bw = queue 

1046 direction = False 

1047 while stack: 

1048 expansion = [self[i] for i in stack[0].successors(self._identifier) if filter(self[i])] 

1049 yield stack.pop(0).identifier 

1050 if direction: 

1051 expansion.reverse() 

1052 stack_bw = expansion + stack_bw 

1053 else: 

1054 stack_fw = expansion + stack_fw 

1055 if not stack: 

1056 direction = not direction 

1057 stack = stack_fw if direction else stack_bw 

1058 

1059 else: 

1060 raise ValueError("Traversal mode '{}' is not supported".format(mode)) 

1061 

1062 def filter_nodes(self, func: Callable[[Node], bool]): 

1063 """ 

1064 Filter all nodes in the tree using a custom function. 

1065 

1066 Applies the given function to every node and returns an iterator over 

1067 nodes where the function returns True. Useful for finding specific 

1068 types of nodes or nodes with certain properties. 

1069 

1070 Args: 

1071 func: Function that takes a Node object and returns bool. 

1072 True means include the node in results. 

1073 

1074 Returns: 

1075 Iterator[Node]: Iterator over nodes where func returns True. 

1076 

1077 Example: 

1078 Filtering nodes by criteria:: 

1079 

1080 # Find all leaf nodes 

1081 leaves = list(tree.filter_nodes(lambda n: n.is_leaf(tree.identifier))) 

1082 

1083 # Find nodes with specific data 

1084 important_nodes = list(tree.filter_nodes( 

1085 lambda n: hasattr(n.data, 'priority') and n.data.priority == 'high' 

1086 )) 

1087 

1088 # Find nodes by tag pattern 

1089 temp_nodes = list(tree.filter_nodes( 

1090 lambda n: n.tag.startswith('temp_') 

1091 )) 

1092 

1093 # Memory efficient processing 

1094 for node in tree.filter_nodes(lambda n: n.data is not None): 

1095 process_node_with_data(node) 

1096 

1097 Note: 

1098 Added by William Rusnack for flexible node filtering. 

1099 """ 

1100 return filter(func, self.all_nodes_itr()) 

1101 

1102 def get_node(self, nid: Optional[str]) -> Optional[Node]: 

1103 """ 

1104 Safely get a node by identifier without raising exceptions. 

1105 

1106 Provides null-safe access to nodes, returning None if the node 

1107 doesn't exist instead of raising an exception. Preferred over 

1108 bracket notation when node existence is uncertain. 

1109 

1110 Args: 

1111 nid: Node identifier to retrieve, or None. 

1112 

1113 Returns: 

1114 Node or None: The node object if found, None otherwise. 

1115 

1116 Example: 

1117 Safe node access:: 

1118 

1119 # Safe access (no exception) 

1120 node = tree.get_node("might_not_exist") 

1121 if node is not None: 

1122 print(f"Found: {node.tag}") 

1123 else: 

1124 print("Node not found") 

1125 

1126 # Compare with bracket notation 

1127 try: 

1128 node = tree["might_not_exist"] # Raises exception 

1129 except NodeIDAbsentError: 

1130 node = None 

1131 

1132 # Use in conditional logic 

1133 user_node = tree.get_node("user123") 

1134 if user_node and user_node.data.active: 

1135 process_active_user(user_node) 

1136 """ 

1137 if nid is None or not self.contains(nid): 

1138 return None 

1139 return self._nodes[nid] 

1140 

1141 def is_branch(self, nid): 

1142 """ 

1143 Get the list of child node identifiers for the specified node. 

1144 

1145 Returns the identifiers of all direct children of the given node. 

1146 Despite the name suggesting a boolean, this method actually returns 

1147 the list of child identifiers. Use children() for Node objects. 

1148 

1149 Args: 

1150 nid: Identifier of the parent node. Cannot be None. 

1151 

1152 Returns: 

1153 list[str]: List of child node identifiers. Empty if no children. 

1154 

1155 Raises: 

1156 OSError: If nid is None. 

1157 NodeIDAbsentError: If nid doesn't exist in the tree. 

1158 

1159 Example: 

1160 Getting child identifiers:: 

1161 

1162 # Get child IDs 

1163 child_ids = tree.is_branch("parent_id") 

1164 for child_id in child_ids: 

1165 print(f"Child ID: {child_id}") 

1166 

1167 # Check if node has children 

1168 if tree.is_branch("node_id"): 

1169 print("Node has children") 

1170 

1171 # Use with node objects 

1172 child_nodes = [tree[child_id] for child_id in tree.is_branch("parent")] 

1173 """ 

1174 if nid is None: 

1175 raise OSError("First parameter can't be None") 

1176 if not self.contains(nid): 

1177 raise NodeIDAbsentError("Node '%s' is not in the tree" % nid) 

1178 

1179 try: 

1180 fpointer = self[nid].successors(self._identifier) 

1181 except KeyError: 

1182 fpointer = [] 

1183 return fpointer 

1184 

1185 def leaves(self, nid: Optional[str] = None) -> NodeList: 

1186 """ 

1187 Get all leaf nodes in the tree or subtree. 

1188 

1189 Returns all nodes that have no children. If a starting node is specified, 

1190 only considers leaves within that subtree. Leaf nodes are terminal nodes 

1191 in the tree structure and often represent end points in hierarchies. 

1192 

1193 Args: 

1194 nid: Root of subtree to search. If None, searches entire tree. 

1195 

1196 Returns: 

1197 list[Node]: List of all leaf Node objects in the specified scope. 

1198 

1199 Example: 

1200 Finding leaf nodes:: 

1201 

1202 # Get all leaves in tree 

1203 all_leaves = tree.leaves() 

1204 print(f"Tree has {len(all_leaves)} leaf nodes") 

1205 

1206 # Get leaves in specific subtree 

1207 subtree_leaves = tree.leaves("department_root") 

1208 

1209 # Process leaf nodes 

1210 for leaf in tree.leaves(): 

1211 print(f"Leaf: {leaf.tag}") 

1212 

1213 # Find deepest nodes 

1214 max_level = max(tree.level(leaf.identifier) for leaf in tree.leaves()) 

1215 deepest_leaves = [leaf for leaf in tree.leaves() 

1216 if tree.level(leaf.identifier) == max_level] 

1217 """ 

1218 leaves = [] 

1219 if nid is None: 

1220 for node in self._nodes.values(): 

1221 if node.is_leaf(self._identifier): 

1222 leaves.append(node) 

1223 else: 

1224 for node in self.expand_tree(nid): 

1225 if self[node].is_leaf(self._identifier): 

1226 leaves.append(self[node]) 

1227 return leaves 

1228 

1229 def level(self, nid, filter=None): 

1230 """ 

1231 Get the level (depth) of a node in the tree hierarchy. 

1232 

1233 Returns the distance from the root to the specified node, where 

1234 the root is at level 0. Optionally filters the path calculation 

1235 by excluding certain nodes. 

1236 

1237 Args: 

1238 nid: Identifier of the node to get level for. 

1239 filter: Optional function to filter nodes during path calculation. 

1240 Takes Node object, returns bool. Filtered nodes are excluded. 

1241 

1242 Returns: 

1243 int: Level of the node (0 for root, 1 for root's children, etc.) 

1244 

1245 Example: 

1246 Getting node levels:: 

1247 

1248 # Basic level calculation 

1249 root_level = tree.level("root") # 0 

1250 child_level = tree.level("child") # 1 

1251 grandchild_level = tree.level("gc") # 2 

1252 

1253 # Use for tree analysis 

1254 max_depth = max(tree.level(node.identifier) 

1255 for node in tree.all_nodes()) 

1256 

1257 # Filtered level calculation 

1258 level = tree.level("node", filter=lambda n: n.tag != "skip") 

1259 """ 

1260 return len([n for n in self.rsearch(nid, filter)]) - 1 

1261 

1262 def link_past_node(self, nid: str): 

1263 """ 

1264 Remove a node while preserving connections between its parent and children. 

1265 

1266 Deletes the specified node but maintains the tree structure by directly 

1267 connecting the node's parent to all of its children. This operation 

1268 effectively "links past" the node, removing it from the hierarchy 

1269 without breaking the tree connections. 

1270 

1271 Args: 

1272 nid: Identifier of the node to link past and remove. 

1273 

1274 Raises: 

1275 NodeIDAbsentError: If nid doesn't exist in the tree. 

1276 LinkPastRootNodeError: If attempting to link past the root node. 

1277 

1278 Example: 

1279 Linking past nodes:: 

1280 

1281 # Before: Root -> Manager -> Employee1, Employee2 

1282 tree.link_past_node("Manager") 

1283 # After: Root -> Employee1, Employee2 

1284 

1285 # Useful for flattening hierarchies 

1286 middle_managers = ["mgr1", "mgr2", "mgr3"] 

1287 for mgr in middle_managers: 

1288 if tree.contains(mgr): 

1289 tree.link_past_node(mgr) 

1290 

1291 # Cannot link past root 

1292 try: 

1293 tree.link_past_node(tree.root) 

1294 except LinkPastRootNodeError: 

1295 print("Cannot link past root node") 

1296 """ 

1297 if not self.contains(nid): 

1298 raise NodeIDAbsentError("Node '%s' is not in the tree" % nid) 

1299 if self.root == nid: 

1300 raise LinkPastRootNodeError("Cannot link past the root node, " "delete it with remove_node()") 

1301 # Get the parent of the node we are linking past 

1302 parent = self[self[nid].predecessor(self._identifier)] 

1303 # Set the children of the node to the parent 

1304 for child in self[nid].successors(self._identifier): 

1305 self[child].set_predecessor(parent.identifier, self._identifier) 

1306 # Link the children to the parent 

1307 for id_ in self[nid].successors(self._identifier) or []: 

1308 parent.update_successors(id_, tree_id=self._identifier) 

1309 # Delete the node 

1310 parent.update_successors(nid, mode=parent.DELETE, tree_id=self._identifier) 

1311 del self._nodes[nid] 

1312 

1313 def move_node(self, source, destination): 

1314 """ 

1315 Move a node from its current parent to a new parent. 

1316 

1317 Changes the parent-child relationship by moving the source node 

1318 (and its entire subtree) from its current parent to the specified 

1319 destination parent. This operation preserves the subtree structure 

1320 while reorganizing the tree hierarchy. 

1321 

1322 Args: 

1323 source: Identifier of the node to move. 

1324 destination: Identifier of the new parent node. 

1325 

1326 Raises: 

1327 NodeIDAbsentError: If source or destination node doesn't exist. 

1328 LoopError: If moving would create a circular reference 

1329 (destination is a descendant of source). 

1330 

1331 Example: 

1332 Reorganizing tree structure:: 

1333 

1334 # Move employee between departments 

1335 tree.move_node("employee123", "sales_dept") 

1336 

1337 # Move entire department 

1338 tree.move_node("engineering_dept", "new_division") 

1339 

1340 # Prevent circular moves (this would raise LoopError) 

1341 try: 

1342 tree.move_node("parent", "child_of_parent") 

1343 except LoopError: 

1344 print("Cannot create circular reference") 

1345 

1346 # Bulk reorganization 

1347 employees = ["emp1", "emp2", "emp3"] 

1348 for emp in employees: 

1349 tree.move_node(emp, "new_manager") 

1350 """ 

1351 if not self.contains(source) or not self.contains(destination): 

1352 raise NodeIDAbsentError 

1353 elif self.is_ancestor(source, destination): 

1354 raise LoopError 

1355 

1356 parent = self[source].predecessor(self._identifier) 

1357 self.__update_fpointer(parent, source, self.node_class.DELETE) 

1358 self.__update_fpointer(destination, source, self.node_class.ADD) 

1359 self.__update_bpointer(source, destination) 

1360 

1361 def is_ancestor(self, ancestor, grandchild): 

1362 """ 

1363 Check if one node is an ancestor of another node. 

1364 

1365 Determines whether the ancestor node appears in the path from 

1366 the grandchild node to the root. An ancestor is any node that 

1367 appears above another node in the tree hierarchy. 

1368 

1369 Args: 

1370 ancestor: Identifier of the potential ancestor node. 

1371 grandchild: Identifier of the potential descendant node. 

1372 

1373 Returns: 

1374 bool: True if ancestor is indeed an ancestor of grandchild, 

1375 False otherwise. 

1376 

1377 Example: 

1378 Checking ancestry relationships:: 

1379 

1380 # Direct parent-child 

1381 is_parent = tree.is_ancestor("parent", "child") # True 

1382 

1383 # Multi-level ancestry 

1384 is_grandparent = tree.is_ancestor("grandparent", "grandchild") # True 

1385 

1386 # Not related 

1387 is_ancestor = tree.is_ancestor("sibling1", "sibling2") # False 

1388 

1389 # Prevent circular moves 

1390 if not tree.is_ancestor("node_to_move", "new_parent"): 

1391 tree.move_node("node_to_move", "new_parent") 

1392 else: 

1393 print("Cannot create circular reference") 

1394 """ 

1395 parent = self[grandchild].predecessor(self._identifier) 

1396 child = grandchild 

1397 while parent is not None: 

1398 if parent == ancestor: 

1399 return True 

1400 else: 

1401 child = self[child].predecessor(self._identifier) 

1402 parent = self[child].predecessor(self._identifier) 

1403 return False 

1404 

1405 @property 

1406 def nodes(self): 

1407 """ 

1408 Get the internal dictionary mapping node identifiers to Node objects. 

1409 

1410 Returns the underlying dictionary that stores all nodes in the tree, 

1411 mapping from node identifiers to Node instances. Useful for direct 

1412 access to the node collection and bulk operations. 

1413 

1414 Returns: 

1415 dict[str, Node]: Dictionary mapping node IDs to Node objects. 

1416 

1417 Example: 

1418 Working with the nodes dictionary:: 

1419 

1420 # Get all node identifiers 

1421 all_ids = list(tree.nodes.keys()) 

1422 

1423 # Get all node objects 

1424 all_nodes = list(tree.nodes.values()) 

1425 

1426 # Direct access to nodes dict 

1427 nodes_dict = tree.nodes 

1428 for node_id, node in nodes_dict.items(): 

1429 print(f"ID: {node_id}, Tag: {node.tag}") 

1430 

1431 # Bulk operations 

1432 total_nodes = len(tree.nodes) 

1433 has_node = "some_id" in tree.nodes 

1434 """ 

1435 return self._nodes 

1436 

1437 def parent(self, nid: str) -> Optional[Node]: 

1438 """ 

1439 Get the parent Node object of the specified node. 

1440 

1441 Returns the immediate parent of the given node, or None if the node 

1442 is the root or has no parent. Provides object-based access to the 

1443 parent, unlike ancestor() which returns identifiers. 

1444 

1445 Args: 

1446 nid: Identifier of the node whose parent to retrieve. 

1447 

1448 Returns: 

1449 Node or None: Parent Node object, or None if node is root. 

1450 

1451 Raises: 

1452 NodeIDAbsentError: If nid doesn't exist in the tree. 

1453 

1454 Example: 

1455 Working with parent relationships:: 

1456 

1457 # Get parent node 

1458 parent_node = tree.parent("child_id") 

1459 if parent_node: 

1460 print(f"Parent: {parent_node.tag}") 

1461 else: 

1462 print("Node is root or orphaned") 

1463 

1464 # Navigate up the hierarchy 

1465 current = "leaf_node" 

1466 while tree.parent(current): 

1467 current = tree.parent(current).identifier 

1468 print(f"Ancestor: {tree[current].tag}") 

1469 

1470 # Check parent properties 

1471 parent = tree.parent("employee") 

1472 if parent and hasattr(parent.data, 'department'): 

1473 print(f"Department: {parent.data.department}") 

1474 """ 

1475 if not self.contains(nid): 

1476 raise NodeIDAbsentError("Node '%s' is not in the tree" % nid) 

1477 

1478 pid = self[nid].predecessor(self._identifier) 

1479 if pid is None or not self.contains(pid): 

1480 return None 

1481 

1482 return self[pid] 

1483 

1484 def merge(self, nid: str, new_tree, deep: bool = False): 

1485 """Patch @new_tree on current tree by pasting new_tree root children on current tree @nid node. 

1486 

1487 Consider the following tree: 

1488 >>> current.show() 

1489 root 

1490 ├── A 

1491 └── B 

1492 >>> new_tree.show() 

1493 root2 

1494 ├── C 

1495 └── D 

1496 └── D1 

1497 Merging new_tree on B node: 

1498 >>>current.merge('B', new_tree) 

1499 >>>current.show() 

1500 root 

1501 ├── A 

1502 └── B 

1503 ├── C 

1504 └── D 

1505 └── D1 

1506 

1507 Note: if current tree is empty and nid is None, the new_tree root will be used as root on current tree. In all 

1508 other cases new_tree root is not pasted. 

1509 """ 

1510 if new_tree.root is None: 

1511 return 

1512 

1513 if nid is None: 

1514 if self.root is None: 

1515 new_tree_root = new_tree[new_tree.root] 

1516 self.add_node(new_tree_root) 

1517 nid = new_tree.root 

1518 else: 

1519 raise ValueError('Must define "nid" under which new tree is merged.') 

1520 for child in new_tree.children(new_tree.root): 

1521 self.paste(nid=nid, new_tree=new_tree.subtree(child.identifier), deep=deep) 

1522 

1523 def paste(self, nid: str, new_tree, deep: bool = False): 

1524 """ 

1525 Paste a @new_tree to the original one by linking the root 

1526 of new tree to given node (nid). 

1527 

1528 Update: add @deep copy of pasted tree. 

1529 """ 

1530 assert isinstance(new_tree, Tree) 

1531 

1532 if new_tree.root is None: 

1533 return 

1534 

1535 if nid is None: 

1536 raise ValueError('Must define "nid" under which new tree is pasted.') 

1537 

1538 if not self.contains(nid): 

1539 raise NodeIDAbsentError("Node '%s' is not in the tree" % nid) 

1540 

1541 set_joint = set(new_tree._nodes) & set(self._nodes) # joint keys 

1542 if set_joint: 

1543 raise ValueError("Duplicated nodes %s exists." % list(map(text, set_joint))) 

1544 

1545 for cid, node in iteritems(new_tree.nodes): 

1546 if deep: 

1547 node = deepcopy(new_tree[node]) 

1548 self._nodes.update({cid: node}) 

1549 node.clone_pointers(new_tree.identifier, self._identifier) 

1550 

1551 self.__update_bpointer(new_tree.root, nid) 

1552 self.__update_fpointer(nid, new_tree.root, self.node_class.ADD) 

1553 

1554 def paths_to_leaves(self) -> StrLList: 

1555 """ 

1556 Use this function to get the identifiers allowing to go from the root 

1557 nodes to each leaf. 

1558 

1559 :return: a list of list of identifiers, root being not omitted. 

1560 

1561 For example: 

1562 

1563 .. code-block:: python 

1564 

1565 Harry 

1566 |___ Bill 

1567 |___ Jane 

1568 | |___ Diane 

1569 | |___ George 

1570 | |___ Jill 

1571 | |___ Mary 

1572 | |___ Mark 

1573 

1574 Expected result: 

1575 

1576 .. code-block:: python 

1577 

1578 [['harry', 'jane', 'diane', 'mary'], 

1579 ['harry', 'jane', 'mark'], 

1580 ['harry', 'jane', 'diane', 'george', 'jill'], 

1581 ['harry', 'bill']] 

1582 

1583 """ 

1584 res = [] 

1585 

1586 for leaf in self.leaves(): 

1587 res.append([nid for nid in self.rsearch(leaf.identifier)][::-1]) 

1588 

1589 return res 

1590 

1591 def remove_node(self, identifier: str) -> int: 

1592 """Remove a node indicated by 'identifier' with all its successors. 

1593 Return the number of removed nodes. 

1594 """ 

1595 if not self.contains(identifier): 

1596 raise NodeIDAbsentError("Node '%s' " "is not in the tree" % identifier) 

1597 

1598 parent = self[identifier].predecessor(self._identifier) 

1599 

1600 # Remove node and its children 

1601 removed = list(self.expand_tree(identifier)) 

1602 

1603 for id_ in removed: 

1604 if id_ == self.root: 

1605 self.root = None 

1606 self.__update_bpointer(id_, None) 

1607 for cid in self[id_].successors(self._identifier) or []: 

1608 self.__update_fpointer(id_, cid, self.node_class.DELETE) 

1609 

1610 # Update parent info 

1611 self.__update_fpointer(parent, identifier, self.node_class.DELETE) 

1612 self.__update_bpointer(identifier, None) 

1613 

1614 for id_ in removed: 

1615 self.nodes.pop(id_) 

1616 return len(removed) 

1617 

1618 def remove_subtree(self, nid: str, identifier: Optional[str] = None): 

1619 """ 

1620 Get a subtree with ``nid`` being the root. If nid is None, an 

1621 empty tree is returned. 

1622 

1623 For the original tree, this method is similar to 

1624 `remove_node(self,nid)`, because given node and its children 

1625 are removed from the original tree in both methods. 

1626 For the returned value and performance, these two methods are 

1627 different: 

1628 

1629 * `remove_node` returns the number of deleted nodes; 

1630 * `remove_subtree` returns a subtree of deleted nodes; 

1631 

1632 You are always suggested to use `remove_node` if your only to 

1633 delete nodes from a tree, as the other one need memory 

1634 allocation to store the new tree. 

1635 

1636 :return: a :class:`Tree` object. 

1637 """ 

1638 st = self._clone(identifier) 

1639 if nid is None: 

1640 return st 

1641 

1642 if not self.contains(nid): 

1643 raise NodeIDAbsentError("Node '%s' is not in the tree" % nid) 

1644 st.root = nid 

1645 

1646 # in original tree, the removed nid will be unreferenced from its 

1647 # parents children 

1648 parent = self[nid].predecessor(self._identifier) 

1649 

1650 removed = list(self.expand_tree(nid)) 

1651 for id_ in removed: 

1652 if id_ == self.root: 

1653 self.root = None 

1654 st._nodes.update({id_: self._nodes.pop(id_)}) 

1655 st[id_].clone_pointers(cast(str, self._identifier), cast(str, st.identifier)) 

1656 st[id_].reset_pointers(self._identifier) 

1657 if id_ == nid: 

1658 st[id_].set_predecessor(None, st.identifier) 

1659 self.__update_fpointer(parent, nid, self.node_class.DELETE) 

1660 return st 

1661 

1662 def rsearch(self, nid: str, filter: Optional[Callable[[Node], bool]] = None): 

1663 """ 

1664 Traverse the tree branch along the branch from nid to its 

1665 ancestors (until root). 

1666 

1667 :param filter: the function of one variable to act on the :class:`Node` object. 

1668 """ 

1669 if nid is None: 

1670 return 

1671 

1672 if not self.contains(nid): 

1673 raise NodeIDAbsentError("Node '%s' is not in the tree" % nid) 

1674 

1675 filter = (lambda x: True) if (filter is None) else filter 

1676 

1677 current = nid 

1678 while current is not None: 

1679 if filter(self[current]): 

1680 yield current 

1681 # subtree() hasn't update the bpointer 

1682 current = self[current].predecessor(self._identifier) if self.root != current else None 

1683 

1684 def save2file( 

1685 self, 

1686 filename: str, 

1687 nid: Optional[str] = None, 

1688 level: int = ROOT, 

1689 idhidden: bool = True, 

1690 filter: Optional[Callable[[Node], bool]] = None, 

1691 key: Optional[Callable[[Node], Node]] = None, 

1692 reverse: bool = False, 

1693 line_type: str = "ascii-ex", 

1694 data_property: Optional[str] = None, 

1695 sorting: bool = True, 

1696 ): 

1697 """ 

1698 Save the tree into file for offline analysis. 

1699 """ 

1700 

1701 def _write_line(line, f): 

1702 f.write(line + b"\n") 

1703 

1704 def handler(x): 

1705 return _write_line(x, open(filename, "ab")) 

1706 

1707 self.__print_backend( 

1708 nid, 

1709 level, 

1710 idhidden, 

1711 filter, 

1712 key, 

1713 reverse, 

1714 line_type, 

1715 data_property, 

1716 sorting, 

1717 func=handler, 

1718 ) 

1719 

1720 def show( 

1721 self, 

1722 nid: Optional[str] = None, 

1723 level: int = ROOT, 

1724 idhidden: bool = True, 

1725 filter: Optional[Callable[[Node], bool]] = None, 

1726 key: Optional[Callable[[Node], Node]] = None, 

1727 reverse: bool = False, 

1728 line_type: str = "ascii-ex", 

1729 data_property: Optional[str] = None, 

1730 stdout: bool = True, 

1731 sorting: bool = True, 

1732 ): 

1733 """ 

1734 Display the tree structure in a beautiful hierarchical format. 

1735 

1736 This method provides rich visualization options for tree structures, allowing 

1737 customization of appearance, content filtering, and display properties. Perfect 

1738 for debugging, documentation, and user interfaces. 

1739 

1740 Args: 

1741 nid: Starting node identifier. If None, starts from root. 

1742 level: Starting level (0 for root). Mainly for internal use. 

1743 idhidden: If True, hide node identifiers in output (cleaner display). 

1744 If False, show both tag and identifier as "tag[id]". 

1745 filter: Function to selectively display nodes. Takes Node object, 

1746 returns bool. Filtered nodes and their subtrees are hidden. 

1747 key: Sorting function for nodes at each level. Takes Node object, 

1748 returns comparison key. If None and sorting=True, sorts by Node. 

1749 reverse: If True, reverse the sorting order at each level. 

1750 line_type: Visual style for tree connections. Options: 

1751 'ascii' - Simple |-- style 

1752 'ascii-ex' - Unicode ├── style (default) 

1753 'ascii-exr' - Unicode with rounded corners 

1754 'ascii-em' - Double-line Unicode ╠══ style 

1755 'ascii-emv' - Mixed vertical Unicode style 

1756 'ascii-emh' - Mixed horizontal Unicode style 

1757 data_property: If specified, display this property from node.data 

1758 instead of node.tag. Useful for rich data objects. 

1759 stdout: If True, print to console. If False, return as string. 

1760 sorting: If True, sort nodes at each level. If False, preserve 

1761 insertion order (key and reverse are ignored). 

1762 

1763 Returns: 

1764 str or None: If stdout=False, returns formatted string. 

1765 If stdout=True, prints to console and returns None. 

1766 

1767 Example: 

1768 Different display styles:: 

1769 

1770 tree = Tree() 

1771 tree.create_node("Root", "root") 

1772 tree.create_node("Child A", "a", parent="root") 

1773 tree.create_node("Child B", "b", parent="root") 

1774 

1775 # Basic display 

1776 tree.show() 

1777 

1778 # Fancy style with IDs 

1779 tree.show(idhidden=False, line_type="ascii-em") 

1780 

1781 # Filtered display (only nodes starting with 'C') 

1782 tree.show(filter=lambda x: x.tag.startswith('C')) 

1783 

1784 # Sorted by tag, reversed 

1785 tree.show(key=lambda x: x.tag, reverse=True) 

1786 

1787 # Show custom data property 

1788 tree.show(data_property="name") # If node.data.name exists 

1789 

1790 # Return as string instead of printing 

1791 tree_str = tree.show(stdout=False) 

1792 

1793 Output styles:: 

1794 

1795 ascii: Root 

1796 |-- Child A 

1797 |-- Child B 

1798 

1799 ascii-ex: Root 

1800 ├── Child A 

1801 └── Child B 

1802 

1803 ascii-em: Root 

1804 ╠══ Child A 

1805 ╚══ Child B 

1806 

1807 Note: 

1808 The tree display automatically handles complex hierarchies with proper 

1809 indentation and connection lines. Very large trees may be truncated 

1810 for performance reasons. 

1811 """ 

1812 self._reader = "" 

1813 

1814 def write(line): 

1815 self._reader += line.decode("utf-8") + "\n" 

1816 

1817 try: 

1818 self.__print_backend( 

1819 nid, 

1820 level, 

1821 idhidden, 

1822 filter, 

1823 key, 

1824 reverse, 

1825 line_type, 

1826 data_property, 

1827 sorting, 

1828 func=write, 

1829 ) 

1830 except NodeIDAbsentError: 

1831 print("Tree is empty") 

1832 

1833 if stdout: 

1834 print(self._reader) 

1835 else: 

1836 return self._reader 

1837 

1838 def siblings(self, nid: str) -> NodeList: 

1839 """ 

1840 Return the siblings of given @nid. 

1841 

1842 If @nid is root or there are no siblings, an empty list is returned. 

1843 """ 

1844 siblings = [] 

1845 

1846 if nid != self.root: 

1847 pid = self[nid].predecessor(self._identifier) 

1848 siblings = [self[i] for i in self[pid].successors(self._identifier) if i != nid] 

1849 

1850 return siblings 

1851 

1852 def size(self, level: Optional[int] = None) -> int: 

1853 """ 

1854 Get the number of nodes of the whole tree if @level is not 

1855 given. Otherwise, the total number of nodes at specific level 

1856 is returned. 

1857 

1858 @param level The level number in the tree. It must be between 

1859 [0, tree.depth]. 

1860 

1861 Otherwise, InvalidLevelNumber exception will be raised. 

1862 """ 

1863 if level is None: 

1864 return len(self._nodes) 

1865 else: 

1866 try: 

1867 level = int(level) 

1868 return len([node for node in self.all_nodes_itr() if self.level(node.identifier) == level]) 

1869 except Exception: 

1870 raise TypeError("level should be an integer instead of '%s'" % type(level)) 

1871 

1872 def subtree(self, nid: str, identifier: Optional[str] = None): 

1873 """ 

1874 Return a shallow COPY of subtree with nid being the new root. 

1875 If nid is None, return an empty tree. 

1876 If you are looking for a deepcopy, please create a new tree 

1877 with this shallow copy, e.g., 

1878 

1879 .. code-block:: python 

1880 

1881 new_tree = Tree(t.subtree(t.root), deep=True) 

1882 

1883 This line creates a deep copy of the entire tree. 

1884 """ 

1885 st = self._clone(identifier) 

1886 if nid is None: 

1887 return st 

1888 

1889 if not self.contains(nid): 

1890 raise NodeIDAbsentError("Node '%s' is not in the tree" % nid) 

1891 

1892 st.root = nid 

1893 for node_n in self.expand_tree(nid): 

1894 st._nodes.update({self[node_n].identifier: self[node_n]}) 

1895 # define nodes parent/children in this tree 

1896 # all pointers are the same as copied tree, except the root 

1897 st[node_n].clone_pointers(cast(str, self._identifier), cast(str, st.identifier)) 

1898 if node_n == nid: 

1899 # reset root parent for the new tree 

1900 st[node_n].set_predecessor(None, st.identifier) 

1901 return st 

1902 

1903 def update_node(self, nid: str, **attrs) -> None: 

1904 """ 

1905 Update node's attributes. 

1906 

1907 :param nid: the identifier of modified node 

1908 :param attrs: attribute pairs recognized by Node object 

1909 :return: None 

1910 """ 

1911 cn = self[nid] 

1912 for attr, val in iteritems(attrs): 

1913 if attr == "identifier": 

1914 # Updating node id meets following contraints: 

1915 # * Update node identifier property 

1916 # * Update parent's followers 

1917 # * Update children's parents 

1918 # * Update tree registration of var _nodes 

1919 # * Update tree root if necessary 

1920 cn = self._nodes.pop(nid) 

1921 setattr(cn, "identifier", val) 

1922 self._nodes[val] = cn 

1923 

1924 if cn.predecessor(self._identifier) is not None: 

1925 self[cn.predecessor(self._identifier)].update_successors( 

1926 nid, 

1927 mode=self.node_class.REPLACE, 

1928 replace=val, 

1929 tree_id=self._identifier, 

1930 ) 

1931 

1932 for fp in cn.successors(self._identifier): 

1933 self[fp].set_predecessor(val, self._identifier) 

1934 

1935 if self.root == nid: 

1936 self.root = val 

1937 else: 

1938 setattr(cn, attr, val) 

1939 

1940 def to_dict(self, nid=None, key=None, sort=True, reverse=False, with_data=False): 

1941 """Transform the whole tree into a dict.""" 

1942 

1943 nid = self.root if (nid is None) else nid 

1944 ntag = self[nid].tag 

1945 tree_dict = {ntag: {"children": []}} 

1946 if with_data: 

1947 tree_dict[ntag]["data"] = self[nid].data 

1948 

1949 if self[nid].expanded: 

1950 queue = [self[i] for i in self[nid].successors(self._identifier)] 

1951 key = (lambda x: x) if (key is None) else key 

1952 if sort: 

1953 queue.sort(key=key, reverse=reverse) 

1954 

1955 for elem in queue: 

1956 tree_dict[ntag]["children"].append( 

1957 self.to_dict(elem.identifier, with_data=with_data, sort=sort, reverse=reverse) 

1958 ) 

1959 if len(tree_dict[ntag]["children"]) == 0: 

1960 tree_dict = self[nid].tag if not with_data else {ntag: {"data": self[nid].data}} 

1961 return tree_dict 

1962 

1963 def to_json(self, with_data: bool = False, sort: bool = True, reverse: bool = False): 

1964 """ 

1965 Export the tree structure as a JSON string. 

1966 

1967 Converts the entire tree into a JSON representation that can be easily 

1968 saved, transmitted, or reconstructed. Perfect for data persistence, 

1969 API responses, and cross-system integration. 

1970 

1971 Args: 

1972 with_data: If True, include node.data in the JSON output. 

1973 If False, only include tree structure and tags. 

1974 Default False for smaller output size. 

1975 sort: If True, sort children at each level by node comparison. 

1976 If False, preserve insertion order. 

1977 reverse: If True, reverse the sorting order at each level. 

1978 Only applies when sort=True. 

1979 

1980 Returns: 

1981 str: JSON string representation of the tree. 

1982 

1983 Example: 

1984 Basic JSON export:: 

1985 

1986 tree = Tree() 

1987 tree.create_node("Root", "root") 

1988 tree.create_node("Child A", "a", parent="root") 

1989 tree.create_node("Child B", "b", parent="root") 

1990 

1991 # Basic structure only 

1992 json_str = tree.to_json() 

1993 print(json_str) 

1994 # Output: {"Root": {"children": [{"Child A": {}}, {"Child B": {}}]}} 

1995 

1996 # Include node data 

1997 tree.create_node("Data Node", "data", parent="root", 

1998 data={"type": "important", "value": 42}) 

1999 json_with_data = tree.to_json(with_data=True) 

2000 print(json_with_data) 

2001 # Output includes: "data": {"type": "important", "value": 42} 

2002 

2003 # Sorted output 

2004 sorted_json = tree.to_json(sort=True, reverse=True) 

2005 

2006 JSON Structure:: 

2007 

2008 The output follows this hierarchical format: 

2009 

2010 { 

2011 "root_tag": { 

2012 "children": [ 

2013 { 

2014 "child1_tag": { 

2015 "children": [...], 

2016 "data": {...} // if with_data=True 

2017 } 

2018 }, 

2019 { 

2020 "child2_tag": {} // leaf node 

2021 } 

2022 ], 

2023 "data": {...} // if with_data=True 

2024 } 

2025 } 

2026 

2027 Usage with APIs:: 

2028 

2029 # Save to file 

2030 with open('tree.json', 'w') as f: 

2031 f.write(tree.to_json(with_data=True)) 

2032 

2033 # Send via HTTP 

2034 import requests 

2035 response = requests.post('/api/trees', 

2036 json={'tree': tree.to_json()}) 

2037 

2038 # Pretty print 

2039 import json 

2040 formatted = json.dumps(json.loads(tree.to_json()), indent=2) 

2041 print(formatted) 

2042 

2043 Note: 

2044 The JSON format preserves the complete tree structure and can be 

2045 used to reconstruct equivalent trees. However, complex data objects 

2046 in node.data must be JSON-serializable (no functions, custom classes 

2047 without __dict__, etc.). 

2048 """ 

2049 return json.dumps(self.to_dict(with_data=with_data, sort=sort, reverse=reverse)) 

2050 

2051 def to_graphviz( 

2052 self, 

2053 filename: Optional[str] = None, 

2054 shape: str = "circle", 

2055 graph: str = "digraph", 

2056 filter=None, 

2057 key=None, 

2058 reverse: bool = False, 

2059 sorting: bool = True, 

2060 ): 

2061 """Exports the tree in the dot format of the graphviz software""" 

2062 nodes, connections = [], [] 

2063 if self.nodes: 

2064 for n in self.expand_tree( 

2065 mode=self.WIDTH, 

2066 filter=filter, 

2067 key=key, 

2068 reverse=reverse, 

2069 sorting=sorting, 

2070 ): 

2071 nid = self[n].identifier 

2072 label = str(self[n].tag).translate(str.maketrans({'"': r"\""})) 

2073 state = '"{0}" [label="{1}", shape={2}]'.format(nid, label, shape) 

2074 nodes.append(state) 

2075 

2076 for c in self.children(nid): 

2077 cid = c.identifier 

2078 edge = "->" if graph == "digraph" else "--" 

2079 connections.append(('"{0}" ' + edge + ' "{1}"').format(nid, cid)) 

2080 

2081 # write nodes and connections to dot format 

2082 is_plain_file = filename is not None 

2083 if is_plain_file: 

2084 f = codecs.open(cast(str, filename), "w", "utf-8") 

2085 else: 

2086 f = StringIO() 

2087 

2088 f.write(graph + " tree {\n") 

2089 for n in nodes: 

2090 f.write("\t" + n + "\n") 

2091 

2092 if len(connections) > 0: 

2093 f.write("\n") 

2094 

2095 for cns in connections: 

2096 f.write("\t" + cns + "\n") 

2097 

2098 f.write("}") 

2099 

2100 if not is_plain_file: 

2101 print(f.getvalue()) 

2102 

2103 f.close() 

2104 

2105 @classmethod 

2106 def from_map(cls, child_parent_dict, id_func=None, data_func=None): 

2107 """ 

2108 takes a dict with child:parent, and form a tree 

2109 """ 

2110 tree = Tree() 

2111 if tree is None or tree.size() > 0: 

2112 raise ValueError("need to pass in an empty tree") 

2113 id_func = id_func if id_func else lambda x: x 

2114 data_func = data_func if data_func else lambda x: None 

2115 parent_child_dict = {} 

2116 root_node = None 

2117 for k, v in child_parent_dict.items(): 

2118 if v is None and root_node is None: 

2119 root_node = k 

2120 elif v is None and root_node is not None: 

2121 raise ValueError("invalid input, more than 1 child has no parent") 

2122 else: 

2123 if v in parent_child_dict: 

2124 parent_child_dict[v].append(k) 

2125 else: 

2126 parent_child_dict[v] = [k] 

2127 if root_node is None: 

2128 raise ValueError("cannot find root") 

2129 

2130 tree.create_node(root_node, id_func(root_node), data=data_func(root_node)) 

2131 queue = [root_node] 

2132 while len(queue) > 0: 

2133 parent_node = queue.pop() 

2134 for child in parent_child_dict.get(parent_node, []): 

2135 tree.create_node( 

2136 child, 

2137 id_func(child), 

2138 parent=id_func(parent_node), 

2139 data=data_func(child), 

2140 ) 

2141 queue.append(child) 

2142 return tree