Skip to content

Commit 22d0d42

Browse files
feat(tree): enable add at location (#4772)
* enable add before or after index * enable add before or after node * try to improve error message wording * raise typerror if invalid argument * add new params to docstring * add raises and note to docstring * add before and after params to add_leaf * update changelog * fix copypasta in docstring * improve error message wording Co-authored by: Darren Burns <[email protected]> --------- Co-authored-by: Will McGugan <[email protected]>
1 parent 35409c3 commit 22d0d42

File tree

4 files changed

+199
-3
lines changed

4 files changed

+199
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1919
- Add `Tree.move_cursor` to programmatically move the cursor without selecting the node https://github.com/Textualize/textual/pull/4753
2020
- Added `Footer` component style handling of padding for the key/description https://github.com/Textualize/textual/pull/4651
2121
- `StringKey` is now exported from `data_table` https://github.com/Textualize/textual/pull/4760
22+
- `TreeNode.add` and `TreeNode.add_leaf` now accepts `before` and `after` arguments to position a new node https://github.com/Textualize/textual/pull/4772
2223
- Added a `gradient` parameter to the `ProgressBar` widget https://github.com/Textualize/textual/pull/4774
2324

2425
### Fixed

src/textual/widgets/_tree.py

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ class UnknownNodeID(Exception):
5050
"""Exception raised when referring to an unknown [`TreeNode`][textual.widgets.tree.TreeNode] ID."""
5151

5252

53+
class AddNodeError(Exception):
54+
"""Exception raised when there is an error with a request to add a node."""
55+
56+
5357
@dataclass
5458
class _TreeLine(Generic[TreeDataType]):
5559
path: list[TreeNode[TreeDataType]]
@@ -322,6 +326,8 @@ def add(
322326
label: TextType,
323327
data: TreeDataType | None = None,
324328
*,
329+
before: int | TreeNode[TreeDataType] | None = None,
330+
after: int | TreeNode[TreeDataType] | None = None,
325331
expand: bool = False,
326332
allow_expand: bool = True,
327333
) -> TreeNode[TreeDataType]:
@@ -330,34 +336,101 @@ def add(
330336
Args:
331337
label: The new node's label.
332338
data: Data associated with the new node.
339+
before: Optional index or `TreeNode` to add the node before.
340+
after: Optional index or `TreeNode` to add the node after.
333341
expand: Node should be expanded.
334342
allow_expand: Allow use to expand the node via keyboard or mouse.
335343
336344
Returns:
337345
A new Tree node
346+
347+
Raises:
348+
AddNodeError: If there is a problem with the addition request.
349+
350+
Note:
351+
Only one of `before` or `after` can be provided. If both are
352+
provided a `AddNodeError` will be raised.
338353
"""
354+
if before is not None and after is not None:
355+
raise AddNodeError("Unable to add a node both before and after a node")
356+
357+
insert_index: int = len(self.children)
358+
359+
if before is not None:
360+
if isinstance(before, int):
361+
insert_index = before
362+
elif isinstance(before, TreeNode):
363+
try:
364+
insert_index = self.children.index(before)
365+
except ValueError:
366+
raise AddNodeError(
367+
"The node specified for `before` is not a child of this node"
368+
)
369+
else:
370+
raise TypeError(
371+
"`before` argument must be an index or a TreeNode object to add before"
372+
)
373+
374+
if after is not None:
375+
if isinstance(after, int):
376+
insert_index = after + 1
377+
if after < 0:
378+
insert_index += len(self.children)
379+
elif isinstance(after, TreeNode):
380+
try:
381+
insert_index = self.children.index(after) + 1
382+
except ValueError:
383+
raise AddNodeError(
384+
"The node specified for `after` is not a child of this node"
385+
)
386+
else:
387+
raise TypeError(
388+
"`after` argument must be an index or a TreeNode object to add after"
389+
)
390+
339391
text_label = self._tree.process_label(label)
340392
node = self._tree._add_node(self, text_label, data)
341393
node._expanded = expand
342394
node._allow_expand = allow_expand
343395
self._updates += 1
344-
self._children.append(node)
396+
self._children.insert(insert_index, node)
345397
self._tree._invalidate()
346398
return node
347399

348400
def add_leaf(
349-
self, label: TextType, data: TreeDataType | None = None
401+
self,
402+
label: TextType,
403+
data: TreeDataType | None = None,
404+
*,
405+
before: int | TreeNode[TreeDataType] | None = None,
406+
after: int | TreeNode[TreeDataType] | None = None,
350407
) -> TreeNode[TreeDataType]:
351408
"""Add a 'leaf' node (a node that can not expand).
352409
353410
Args:
354411
label: Label for the node.
355412
data: Optional data.
413+
before: Optional index or `TreeNode` to add the node before.
414+
after: Optional index or `TreeNode` to add the node after.
356415
357416
Returns:
358417
New node.
418+
419+
Raises:
420+
AddNodeError: If there is a problem with the addition request.
421+
422+
Note:
423+
Only one of `before` or `after` can be provided. If both are
424+
provided a `AddNodeError` will be raised.
359425
"""
360-
node = self.add(label, data, expand=False, allow_expand=False)
426+
node = self.add(
427+
label,
428+
data,
429+
before=before,
430+
after=after,
431+
expand=False,
432+
allow_expand=False,
433+
)
361434
return node
362435

363436
def _remove_children(self) -> None:

src/textual/widgets/tree.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Make non-widget Tree support classes available."""
22

33
from ._tree import (
4+
AddNodeError,
45
EventTreeDataType,
56
NodeID,
67
RemoveRootError,
@@ -10,6 +11,7 @@
1011
)
1112

1213
__all__ = [
14+
"AddNodeError",
1315
"EventTreeDataType",
1416
"NodeID",
1517
"RemoveRootError",

tests/tree/test_tree_node_add.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import pytest
2+
3+
from textual.widgets import Tree
4+
from textual.widgets.tree import AddNodeError
5+
6+
7+
def test_tree_node_add_before_and_after_raises_exception():
8+
tree = Tree[None]("root")
9+
with pytest.raises(AddNodeError):
10+
tree.root.add("error", before=99, after=0)
11+
12+
13+
def test_tree_node_add_before_or_after_with_invalid_type_raises_exception():
14+
tree = Tree[None]("root")
15+
tree.root.add("node")
16+
with pytest.raises(TypeError):
17+
tree.root.add("before node", before="node")
18+
with pytest.raises(TypeError):
19+
tree.root.add("after node", after="node")
20+
21+
22+
def test_tree_node_add_before_index():
23+
tree = Tree[None]("root")
24+
tree.root.add("node")
25+
tree.root.add("before node", before=0)
26+
tree.root.add("first", before=-99)
27+
tree.root.add("after first", before=-2)
28+
tree.root.add("last", before=99)
29+
tree.root.add("after node", before=4)
30+
tree.root.add("before last", before=-1)
31+
32+
assert str(tree.root.children[0].label) == "first"
33+
assert str(tree.root.children[1].label) == "after first"
34+
assert str(tree.root.children[2].label) == "before node"
35+
assert str(tree.root.children[3].label) == "node"
36+
assert str(tree.root.children[4].label) == "after node"
37+
assert str(tree.root.children[5].label) == "before last"
38+
assert str(tree.root.children[6].label) == "last"
39+
40+
41+
def test_tree_node_add_after_index():
42+
tree = Tree[None]("root")
43+
tree.root.add("node")
44+
tree.root.add("after node", after=0)
45+
tree.root.add("first", after=-99)
46+
tree.root.add("after first", after=-3)
47+
tree.root.add("before node", after=1)
48+
tree.root.add("before last", after=99)
49+
tree.root.add("last", after=-1)
50+
51+
assert str(tree.root.children[0].label) == "first"
52+
assert str(tree.root.children[1].label) == "after first"
53+
assert str(tree.root.children[2].label) == "before node"
54+
assert str(tree.root.children[3].label) == "node"
55+
assert str(tree.root.children[4].label) == "after node"
56+
assert str(tree.root.children[5].label) == "before last"
57+
assert str(tree.root.children[6].label) == "last"
58+
59+
60+
def test_tree_node_add_relative_to_unknown_node_raises_exception():
61+
tree = Tree[None]("root")
62+
removed_node = tree.root.add("removed node")
63+
removed_node.remove()
64+
with pytest.raises(AddNodeError):
65+
tree.root.add("node", before=removed_node)
66+
with pytest.raises(AddNodeError):
67+
tree.root.add("node", after=removed_node)
68+
69+
70+
def test_tree_node_add_before_node():
71+
tree = Tree[None]("root")
72+
node = tree.root.add("node")
73+
before_node = tree.root.add("before node", before=node)
74+
tree.root.add("first", before=before_node)
75+
tree.root.add("after first", before=before_node)
76+
last = tree.root.add("last", before=4)
77+
before_last = tree.root.add("before last", before=last)
78+
tree.root.add("after node", before=before_last)
79+
80+
assert str(tree.root.children[0].label) == "first"
81+
assert str(tree.root.children[1].label) == "after first"
82+
assert str(tree.root.children[2].label) == "before node"
83+
assert str(tree.root.children[3].label) == "node"
84+
assert str(tree.root.children[4].label) == "after node"
85+
assert str(tree.root.children[5].label) == "before last"
86+
assert str(tree.root.children[6].label) == "last"
87+
88+
89+
def test_tree_node_add_after_node():
90+
tree = Tree[None]("root")
91+
node = tree.root.add("node")
92+
after_node = tree.root.add("after node", after=node)
93+
first = tree.root.add("first", after=-3)
94+
after_first = tree.root.add("after first", after=first)
95+
tree.root.add("before node", after=after_first)
96+
before_last = tree.root.add("before last", after=after_node)
97+
tree.root.add("last", after=before_last)
98+
99+
assert str(tree.root.children[0].label) == "first"
100+
assert str(tree.root.children[1].label) == "after first"
101+
assert str(tree.root.children[2].label) == "before node"
102+
assert str(tree.root.children[3].label) == "node"
103+
assert str(tree.root.children[4].label) == "after node"
104+
assert str(tree.root.children[5].label) == "before last"
105+
assert str(tree.root.children[6].label) == "last"
106+
107+
108+
def test_tree_node_add_leaf_before_or_after():
109+
tree = Tree[None]("root")
110+
leaf = tree.root.add_leaf("leaf")
111+
tree.root.add_leaf("before leaf", before=leaf)
112+
tree.root.add_leaf("after leaf", after=leaf)
113+
tree.root.add_leaf("first", before=0)
114+
tree.root.add_leaf("last", after=-1)
115+
116+
assert str(tree.root.children[0].label) == "first"
117+
assert str(tree.root.children[1].label) == "before leaf"
118+
assert str(tree.root.children[2].label) == "leaf"
119+
assert str(tree.root.children[3].label) == "after leaf"
120+
assert str(tree.root.children[4].label) == "last"

0 commit comments

Comments
 (0)