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

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

138 statements  

1#!/usr/bin/env python 

2# Copyright (C) 2011 

3# Brett Alistair Kromkamp - brettkromkamp@gmail.com 

4# Copyright (C) 2012-2025 

5# Xiaming Chen - chenxm35@gmail.com 

6# and other contributors. 

7# All rights reserved. 

8""" 

9Node structure in treelib. 

10 

11A :class:`Node` object contains basic properties such as node identifier, 

12node tag, parent node, children nodes etc., and some operations for a node. 

13 

14The Node class is the fundamental building block of a Tree. Each node can store: 

15- A unique identifier for tree operations 

16- A human-readable tag for display 

17- Custom data payload 

18- Parent-child relationships within one or more trees 

19 

20Note: 

21 Nodes are typically created through Tree.create_node() rather than 

22 directly instantiated, as the Tree class manages the parent-child 

23 relationships automatically. 

24""" 

25from __future__ import unicode_literals 

26 

27import copy 

28import sys 

29import uuid 

30from collections import defaultdict 

31from typing import Any, List, Optional, Union, cast 

32from warnings import warn 

33 

34from .exceptions import NodePropertyError 

35from .misc import deprecated 

36 

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

38 StrList = list[str] 

39else: 

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

41 

42 

43class Node(object): 

44 """ 

45 Elementary node object stored in Tree structures. 

46 

47 A Node represents a single element in a tree hierarchy. Each node maintains 

48 references to its parent and children within specific tree contexts, along 

49 with a human-readable tag and optional data payload. 

50 

51 Attributes: 

52 identifier (str): Unique identifier for this node within a tree. 

53 tag (str): Human-readable label for display purposes. 

54 expanded (bool): Controls visibility of children in tree displays. 

55 data (Any): User-defined payload associated with this node. 

56 

57 Example: 

58 Creating nodes with different configurations:: 

59 

60 # Basic node 

61 node = Node("Root", "root") 

62 

63 # Node with custom data 

64 node = Node("File", "file1", data={"size": 1024, "type": "txt"}) 

65 

66 # Node that starts collapsed 

67 node = Node("Folder", "folder1", expanded=False) 

68 

69 Warning: 

70 While nodes can be created directly, it's recommended to use 

71 Tree.create_node() which properly manages tree relationships. 

72 """ 

73 

74 #: Mode constants for routine `update_fpointer()`. 

75 (ADD, DELETE, INSERT, REPLACE) = list(range(4)) 

76 

77 def __init__( 

78 self, 

79 tag: Optional[str] = None, 

80 identifier: Optional[str] = None, 

81 expanded: bool = True, 

82 data: Any = None, 

83 ) -> None: 

84 """ 

85 Create a new Node object to be placed inside a Tree object. 

86 

87 Args: 

88 tag: Human-readable label for the node. If None, uses identifier. 

89 identifier: Unique ID for the node. If None, generates a UUID. 

90 expanded: Whether node children are visible in tree displays. 

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

92 

93 Example: 

94 Creating different types of nodes:: 

95 

96 # Auto-generated identifier 

97 node1 = Node("My Node") 

98 

99 # Explicit identifier 

100 node2 = Node("Root", "root") 

101 

102 # With custom data 

103 node3 = Node("User", "user1", data={"name": "Alice", "role": "admin"}) 

104 

105 # Initially collapsed 

106 node4 = Node("Large Folder", "folder", expanded=False) 

107 """ 

108 

109 #: if given as a parameter, must be unique 

110 self._identifier: Optional[str] = None 

111 self._set_identifier(identifier) 

112 

113 #: None or something else 

114 #: if None, self._identifier will be set to the identifier's value. 

115 if tag is None: 

116 self._tag = self._identifier 

117 else: 

118 self._tag = tag 

119 

120 #: boolean 

121 self.expanded = expanded 

122 

123 #: identifier of the parent's node : 

124 self._predecessor: dict = {} 

125 #: identifier(s) of the soons' node(s) : 

126 self._successors: dict = defaultdict(list) 

127 

128 #: User payload associated with this node. 

129 self.data = data 

130 

131 # for retro-compatibility on bpointer/fpointer 

132 self._initial_tree_id: Optional[str] = None 

133 

134 def __lt__(self, other) -> bool: 

135 """ 

136 Compare nodes for sorting based on their tags. 

137 

138 Enables sorting of Node objects in collections by comparing their tag attributes. 

139 This allows natural ordering when displaying or processing collections of nodes. 

140 

141 Args: 

142 other: Another Node object to compare with. 

143 

144 Returns: 

145 bool: True if this node's tag is lexicographically less than other's tag. 

146 

147 Example: 

148 Sorting nodes by tag:: 

149 

150 nodes = [Node("Charlie", "c"), Node("Alice", "a"), Node("Bob", "b")] 

151 sorted_nodes = sorted(nodes) # Alice, Bob, Charlie 

152 """ 

153 return self.tag < other.tag 

154 

155 def set_initial_tree_id(self, tree_id: str) -> None: 

156 """ 

157 Set the initial tree identifier for backward compatibility. 

158 

159 This method is used internally to maintain compatibility with older 

160 node pointer systems. Each node remembers the first tree it was added to 

161 for legacy bpointer/fpointer access. 

162 

163 Args: 

164 tree_id: Unique identifier of the tree this node was first added to. 

165 

166 Note: 

167 This is an internal method used for legacy compatibility. 

168 Modern code should use the tree-specific pointer methods instead. 

169 """ 

170 if self._initial_tree_id is None: 

171 self._initial_tree_id = tree_id 

172 

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

174 """ 

175 Initialize node identifier with given value or auto-generate one. 

176 

177 Private method used during node creation to set the unique identifier. 

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

179 uniqueness within the tree. 

180 

181 Args: 

182 nid: Desired node identifier, or None to auto-generate. 

183 

184 Note: 

185 This is an internal method. Use the identifier property for 

186 accessing or modifying the identifier after node creation. 

187 """ 

188 if nid is None: 

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

190 else: 

191 self._identifier = nid 

192 

193 @property 

194 @deprecated(alias="node.predecessor") 

195 def bpointer(self): 

196 if self._initial_tree_id not in self._predecessor.keys(): 

197 return None 

198 return self._predecessor[self._initial_tree_id] 

199 

200 @bpointer.setter 

201 @deprecated(alias="node.set_predecessor") 

202 def bpointer(self, value) -> None: 

203 self.set_predecessor(value, self._initial_tree_id) 

204 

205 @deprecated(alias="node.set_predecessor") 

206 def update_bpointer(self, nid) -> None: 

207 self.set_predecessor(nid, self._initial_tree_id) 

208 

209 @property 

210 @deprecated(alias="node.successors") 

211 def fpointer(self): 

212 if self._initial_tree_id not in self._successors: 

213 return [] 

214 return self._successors[self._initial_tree_id] 

215 

216 @fpointer.setter 

217 @deprecated(alias="node.update_successors") 

218 def fpointer(self, value: Union[None, list, dict, set]) -> None: 

219 self.set_successors(value, tree_id=self._initial_tree_id) 

220 

221 @deprecated(alias="node.update_successors") 

222 def update_fpointer(self, nid, mode=ADD, replace=None): 

223 self.update_successors(nid, mode, replace, self._initial_tree_id) 

224 

225 def predecessor(self, tree_id): 

226 """ 

227 Get the parent node identifier in the specified tree. 

228 

229 Since nodes can exist in multiple trees simultaneously, this method 

230 returns the parent identifier for this node within a specific tree context. 

231 

232 Args: 

233 tree_id: Identifier of the tree to query parent relationship in. 

234 

235 Returns: 

236 str or None: Parent node identifier, or None if this is a root node. 

237 

238 Example: 

239 Accessing parent in different trees:: 

240 

241 # Node can have different parents in different trees 

242 parent_id = node.predecessor("tree1") 

243 if parent_id: 

244 print(f"Parent in tree1: {parent_id}") 

245 """ 

246 return self._predecessor[tree_id] 

247 

248 def set_predecessor(self, nid: Optional[str], tree_id: Optional[str]) -> None: 

249 """ 

250 Set the parent node identifier for this node in a specific tree. 

251 

252 Establishes or updates the parent-child relationship for this node 

253 within the context of a particular tree. Used internally by tree 

254 operations to maintain proper hierarchy. 

255 

256 Args: 

257 nid: Parent node identifier, or None to make this a root node. 

258 tree_id: Identifier of the tree to set parent relationship in. 

259 

260 Example: 

261 Setting parent relationship:: 

262 

263 node.set_predecessor("parent_id", "tree1") 

264 node.set_predecessor(None, "tree1") # Make root 

265 """ 

266 self._predecessor[tree_id] = nid 

267 

268 def successors(self, tree_id: Optional[str]) -> StrList: 

269 """ 

270 Get the list of child node identifiers in the specified tree. 

271 

272 Returns all direct children of this node within a specific tree context. 

273 Children are maintained as an ordered list to preserve insertion order 

274 and support custom sorting. 

275 

276 Args: 

277 tree_id: Identifier of the tree to query children in. 

278 

279 Returns: 

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

281 

282 Example: 

283 Accessing children in different trees:: 

284 

285 children = node.successors("tree1") 

286 for child_id in children: 

287 print(f"Child: {child_id}") 

288 

289 # Node can have different children in different trees 

290 tree1_children = node.successors("tree1") 

291 tree2_children = node.successors("tree2") 

292 """ 

293 return self._successors[tree_id] 

294 

295 def set_successors(self, value: Union[None, list, dict, set], tree_id: Optional[str] = None) -> None: 

296 """ 

297 Set the complete list of child node identifiers for a specific tree. 

298 

299 Replaces the entire children list with new values. Accepts multiple 

300 input formats for flexibility: list, set, dict (uses keys), or None. 

301 

302 Args: 

303 value: New children collection. Can be: 

304 - list: Used directly as child identifiers 

305 - set: Converted to list of identifiers 

306 - dict: Uses dictionary keys as identifiers 

307 - None: Sets empty children list 

308 tree_id: Identifier of the tree to set children in. 

309 

310 Raises: 

311 NotImplementedError: If value type is not supported. 

312 

313 Example: 

314 Setting children from different sources:: 

315 

316 # From list 

317 node.set_successors(["child1", "child2"], "tree1") 

318 

319 # From set 

320 node.set_successors({"child1", "child2"}, "tree1") 

321 

322 # From dict (uses keys) 

323 node.set_successors({"child1": data1, "child2": data2}, "tree1") 

324 

325 # Clear children 

326 node.set_successors(None, "tree1") 

327 """ 

328 setter_lookup = { 

329 "NoneType": lambda x: list(), 

330 "list": lambda x: x, 

331 "dict": lambda x: list(x.keys()), 

332 "set": lambda x: list(x), 

333 } 

334 

335 t = value.__class__.__name__ 

336 if t in setter_lookup: 

337 f_setter = setter_lookup[t] 

338 self._successors[tree_id] = f_setter(value) 

339 else: 

340 raise NotImplementedError("Unsupported value type %s" % t) 

341 

342 def update_successors( 

343 self, 

344 nid: Optional[str], 

345 mode: int = ADD, 

346 replace: Optional[str] = None, 

347 tree_id: Optional[str] = None, 

348 ) -> None: 

349 """ 

350 Update the children list with different modification modes. 

351 

352 Provides granular control over child node list modifications without 

353 replacing the entire list. Supports adding, removing, and replacing 

354 individual child references. 

355 

356 Args: 

357 nid: Child node identifier to operate on. 

358 mode: Operation type using Node constants: 

359 - Node.ADD: Append child to end of list 

360 - Node.DELETE: Remove child from list 

361 - Node.INSERT: Deprecated, same as ADD 

362 - Node.REPLACE: Replace child with another identifier 

363 replace: New identifier when mode=REPLACE. Required for replace mode. 

364 tree_id: Identifier of the tree to modify children in. 

365 

366 Raises: 

367 NodePropertyError: If replace is None when mode=REPLACE. 

368 NotImplementedError: If mode is not supported. 

369 

370 Example: 

371 Different update operations:: 

372 

373 # Add a child 

374 node.update_successors("new_child", Node.ADD, tree_id="tree1") 

375 

376 # Remove a child 

377 node.update_successors("old_child", Node.DELETE, tree_id="tree1") 

378 

379 # Replace a child 

380 node.update_successors("old_child", Node.REPLACE, 

381 replace="new_child", tree_id="tree1") 

382 """ 

383 if nid is None: 

384 return 

385 

386 def _manipulator_append() -> None: 

387 self.successors(tree_id).append(nid) 

388 

389 def _manipulator_delete() -> None: 

390 if nid in self.successors(tree_id): 

391 self.successors(tree_id).remove(nid) 

392 else: 

393 warn("Nid %s wasn't present in fpointer" % nid) 

394 

395 def _manipulator_insert() -> None: 

396 warn("WARNING: INSERT is deprecated to ADD mode") 

397 self.update_successors(nid, tree_id=tree_id) 

398 

399 def _manipulator_replace() -> None: 

400 if replace is None: 

401 raise NodePropertyError('Argument "replace" should be provided when mode is {}'.format(mode)) 

402 ind = self.successors(tree_id).index(nid) 

403 self.successors(tree_id)[ind] = replace 

404 

405 manipulator_lookup = { 

406 self.ADD: "_manipulator_append", 

407 self.DELETE: "_manipulator_delete", 

408 self.INSERT: "_manipulator_insert", 

409 self.REPLACE: "_manipulator_replace", 

410 } 

411 

412 if mode not in manipulator_lookup: 

413 raise NotImplementedError("Unsupported node updating mode %s" % str(mode)) 

414 

415 f_name = cast(str, manipulator_lookup.get(mode)) 

416 f = locals()[f_name] 

417 return f() 

418 

419 @property 

420 def identifier(self) -> str: 

421 """ 

422 Get the unique identifier of this node. 

423 

424 The identifier serves as the primary key for this node within tree structures. 

425 It must be unique within each tree that contains this node. Used for all 

426 tree operations including lookup, traversal, and relationship management. 

427 

428 Returns: 

429 str: The unique node identifier. 

430 

431 Example: 

432 Accessing node identifier:: 

433 

434 node = Node("My Node", "unique_id") 

435 print(node.identifier) # "unique_id" 

436 

437 # Use in tree operations 

438 tree[node.identifier] # Access via identifier 

439 """ 

440 return cast(str, self._identifier) 

441 

442 def clone_pointers(self, former_tree_id: str, new_tree_id: str) -> None: 

443 """ 

444 Clone parent-child relationships from one tree context to another. 

445 

446 Copies the complete relationship structure (parent and children) from 

447 one tree context to a new tree context. Used when moving or copying 

448 nodes between trees while preserving their hierarchical relationships. 

449 

450 Args: 

451 former_tree_id: Source tree identifier to copy relationships from. 

452 new_tree_id: Target tree identifier to copy relationships to. 

453 

454 Example: 

455 Cloning relationships between trees:: 

456 

457 # Copy node relationships from tree1 to tree2 

458 node.clone_pointers("tree1", "tree2") 

459 

460 # Now node has same relationships in both trees 

461 assert node.predecessor("tree1") == node.predecessor("tree2") 

462 """ 

463 former_bpointer = self.predecessor(former_tree_id) 

464 self.set_predecessor(former_bpointer, new_tree_id) 

465 former_fpointer = self.successors(former_tree_id) 

466 # fpointer is a list and would be copied by reference without deepcopy 

467 self.set_successors(copy.deepcopy(former_fpointer), tree_id=new_tree_id) 

468 

469 def reset_pointers(self, tree_id) -> None: 

470 """ 

471 Reset all parent-child relationships for a specific tree context. 

472 

473 Clears both parent and children references for this node within the 

474 specified tree, effectively making it an isolated node. Used during 

475 tree restructuring operations. 

476 

477 Args: 

478 tree_id: Identifier of the tree to reset relationships in. 

479 

480 Example: 

481 Resetting node relationships:: 

482 

483 # Clear all relationships in tree1 

484 node.reset_pointers("tree1") 

485 

486 # Node now has no parent or children in tree1 

487 assert node.predecessor("tree1") is None 

488 assert len(node.successors("tree1")) == 0 

489 """ 

490 self.set_predecessor(None, tree_id) 

491 self.set_successors([], tree_id=tree_id) 

492 

493 @identifier.setter # type: ignore 

494 def identifier(self, value: str) -> None: 

495 """ 

496 Set the unique identifier of this node. 

497 

498 Updates the node's identifier while maintaining data integrity. 

499 The new identifier must be unique within any trees containing this node. 

500 

501 Args: 

502 value: New unique identifier for this node. Cannot be None. 

503 

504 Warning: 

505 Changing a node's identifier while it exists in trees can break 

506 tree integrity. Use Tree.update_node() for safe identifier changes. 

507 

508 Example: 

509 Updating node identifier:: 

510 

511 node.identifier = "new_unique_id" 

512 

513 # Better approach when node is in a tree: 

514 tree.update_node("old_id", identifier="new_id") 

515 """ 

516 if value is None: 

517 print("WARNING: node ID can not be None") 

518 else: 

519 self._set_identifier(value) 

520 

521 def is_leaf(self, tree_id: Optional[str] = None) -> bool: 

522 """ 

523 Check if this node is a leaf (has no children) in the specified tree. 

524 

525 A leaf node is one that has no child nodes. This is a fundamental 

526 property used in tree algorithms and can vary between different 

527 tree contexts if the node exists in multiple trees. 

528 

529 Args: 

530 tree_id: Identifier of the tree to check leaf status in. 

531 If None, uses the initial tree for backward compatibility. 

532 

533 Returns: 

534 bool: True if node has no children in the specified tree, False otherwise. 

535 

536 Example: 

537 Checking leaf status:: 

538 

539 if node.is_leaf("tree1"): 

540 print("This is a leaf node") 

541 

542 # Different trees may have different leaf status 

543 is_leaf_tree1 = node.is_leaf("tree1") 

544 is_leaf_tree2 = node.is_leaf("tree2") # May differ 

545 """ 

546 if tree_id is None: 

547 # for retro-compatilibity 

548 if self._initial_tree_id not in self._successors.keys(): 

549 return True 

550 else: 

551 tree_id = self._initial_tree_id 

552 

553 if len(self.successors(tree_id)) == 0: 

554 return True 

555 else: 

556 return False 

557 

558 def is_root(self, tree_id: Optional[str] = None) -> bool: 

559 """ 

560 Check if this node is a root (has no parent) in the specified tree. 

561 

562 A root node is the topmost node in a tree hierarchy with no parent. 

563 Every tree has exactly one root node. This status can vary between 

564 different tree contexts if the node exists in multiple trees. 

565 

566 Args: 

567 tree_id: Identifier of the tree to check root status in. 

568 If None, uses the initial tree for backward compatibility. 

569 

570 Returns: 

571 bool: True if node has no parent in the specified tree, False otherwise. 

572 

573 Example: 

574 Checking root status:: 

575 

576 if node.is_root("tree1"): 

577 print("This is the root node") 

578 

579 # Same node might be root in one tree but not another 

580 is_root_tree1 = node.is_root("tree1") 

581 is_root_tree2 = node.is_root("tree2") # May differ 

582 """ 

583 if tree_id is None: 

584 # for retro-compatilibity 

585 if self._initial_tree_id not in self._predecessor.keys(): 

586 return True 

587 else: 

588 tree_id = self._initial_tree_id 

589 

590 return self.predecessor(tree_id) is None 

591 

592 @property 

593 def tag(self) -> str: 

594 """ 

595 Get the human-readable display name of this node. 

596 

597 The tag serves as the display label for this node in tree visualizations, 

598 printouts, and user interfaces. Unlike the identifier, the tag doesn't 

599 need to be unique and is primarily for human readability. 

600 

601 Returns: 

602 str: The display name/label for this node. 

603 

604 Example: 

605 Using node tags for display:: 

606 

607 node = Node("User Profile", "user123") 

608 print(node.tag) # "User Profile" 

609 print(node.identifier) # "user123" 

610 

611 # Tags are used in tree display 

612 tree.show() # Shows "User Profile" not "user123" 

613 """ 

614 return cast(str, self._tag) 

615 

616 @tag.setter 

617 def tag(self, value: Optional[str]) -> None: 

618 """ 

619 Set the human-readable display name of this node. 

620 

621 Updates the display label used in tree visualizations and string 

622 representations. The tag can be changed freely without affecting 

623 tree structure or node relationships. 

624 

625 Args: 

626 value: New display name for the node. Can be None. 

627 

628 Example: 

629 Updating node display name:: 

630 

631 node.tag = "Updated Profile" 

632 print(node.tag) # "Updated Profile" 

633 

634 # Useful for dynamic labeling 

635 node.tag = f"User: {user.name} (Active)" 

636 """ 

637 self._tag = value if value is not None else None 

638 

639 def __repr__(self) -> str: 

640 """ 

641 Return detailed string representation of this node for debugging. 

642 

643 Provides a comprehensive view of the node's properties including 

644 tag, identifier, and data. Useful for debugging, logging, and 

645 development work. 

646 

647 Returns: 

648 str: Detailed string representation in format: 

649 Node(tag=<tag>, identifier=<id>, data=<data>) 

650 

651 Example: 

652 Node representation output:: 

653 

654 node = Node("Profile", "user123", data={"age": 30}) 

655 print(repr(node)) 

656 # Output: Node(tag=Profile, identifier=user123, data={'age': 30}) 

657 

658 # Useful in lists and debugging 

659 print([node1, node2]) # Shows detailed info for each node 

660 """ 

661 name = self.__class__.__name__ 

662 kwargs = [ 

663 "tag={0}".format(self.tag), 

664 "identifier={0}".format(self.identifier), 

665 "data={0}".format(self.data), 

666 ] 

667 return "%s(%s)" % (name, ", ".join(kwargs))