Tree.clear()/reset() don't clear or repopulate _tree_nodes
AI disclosure (per AI_POLICY.md): this issue was investigated and written with the assistance of Claude (Anthropic). I read and independently reproduced the underlying code myself before filing — see the repro below, which you can run yourself.
Summary
Tree.clear() and Tree.reset() don't clear (or repopulate) the internal _tree_nodes: dict[NodeID, TreeNode] index, so get_node_by_id() can silently return a stale, discarded node after a clear(), and node IDs can collide with pre-clear entries.
This is the same diagnosis as #6383 (closed for skipping the AI-policy approval step, not because the fix was wrong). I independently reproduced it against a fresh clone of main today before filing, and I'm opening this issue first per the policy, rather than going straight to a PR.
Root cause
Tree.__init__ registers the root correctly, through _add_node():
self._tree_nodes: dict[NodeID, TreeNode[TreeDataType]] = {}
self.root = self._add_node(None, text_label, data) # inserts into _tree_nodesTree.clear() (src/textual/widgets/_tree.py) does not follow the same path:
self._current_id = 0
self.root = TreeNode(self, None, self._new_id(), root_label, root_data, expanded=root_expanded)This constructs the new root directly instead of via _add_node(), so:
- the old
_tree_nodesdict is never cleared — discarded nodes leak in it forever - the new root is never inserted into
_tree_nodes
Tree.reset() just calls clear(), so it inherits the same gap.
Reproduction
from textual.widgets import Tree
t = Tree("root")
child_a = t.root.add("a")
child_b = t.root.add("b")
print("before:", sorted(t._tree_nodes.keys())) # [0, 1, 2]
t.clear()
print("after: ", sorted(t._tree_nodes.keys())) # still [0, 1, 2] -- nothing cleared
resolved = t.get_node_by_id(t.root.id)
print(resolved is t.root) # False -- resolves to the DISCARDED pre-clear rootReal output from running this against today's main:
before: [0, 1, 2]
after: [0, 1, 2]
get_node_by_id(new_root.id) is new_root -> FalseNo live App/terminal needed to reproduce — plain construction + clear() is enough.
Impact
get_node_by_id()can silently resolve to a stale, detached node instead of the current one — anything holding aNodeIDacross aclear()/reset()(a scheduled callback, a persisted "last selected" id) gets the wrong object back, silently, no exception.- Nodes from before a
clear()that don't get their ID slot reused leak in_tree_nodesforever — unbounded growth for any UI that repeatedly clears and repopulates aTree(a live log view, a refreshing listing, etc.). DirectoryTreeis unaffected, since it defines its own correctclear_node()/reset_node()rather than relying on the baseclear()/reset().
Not covered by existing tests: tests/tree/test_tree_clearing.py only checks root.children/label/data after clearing, never _tree_nodes or get_node_by_id() afterward.
Proposed fix
Clear _tree_nodes before rebuilding the root, and construct the new root via _add_node() instead of TreeNode(...) directly, mirroring __init__'s own pattern (same fix shape as #6383). Happy to open a PR once this has a green light, per the AI policy.
Source: Textualize/textual