From e94f85fec422616f8b6c944d311d5c1a35945982 Mon Sep 17 00:00:00 2001 From: Patrick Totzke Date: Sun, 25 Oct 2026 09:41:38 +0000 Subject: [PATCH] Route NestedTree's collapse calls through one helper collapsible, is_collapsed, toggle_collapsed, collapse and expand each looked up the tree managing the position and checked that it can collapse; _on_subtree now does that once, replacing _get_subtree_for. It walks down through NestedTrees nested directly in each other in a loop, where the methods used to recurse once per level. Co-Authored-By: Claude Opus 5.5 --- tests/test_nested.py | 11 +++++++++++ urwidtrees/nested.py | 39 +++++++++++++++++---------------------- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/tests/test_nested.py b/tests/test_nested.py index 0dfed02..e995994 100644 --- a/tests/test_nested.py +++ b/tests/test_nested.py @@ -161,6 +161,17 @@ def test_collapse_all_reaches_hidden_subtrees(self): tree.expand_all() self.assertEqual(len(list(tree.positions())), 4) + def test_collapse_deeply_nested(self): + # no recursion per NestedTree wrapped directly in another + tree, x = self.inner, (0,) + for _ in range(1100): + tree, x = NestedTree(tree), (x,) + self.assertTrue(tree.collapsible(x)) + tree.collapse(x) + self.assertTrue(self.inner.is_collapsed((0,))) + tree.expand(x) + self.assertFalse(tree.is_collapsed(x)) + def test_collapse_all_wide(self): outer = CollapsibleTree(SimpleTree([(Node('R'), [ (Node(str(i)), [(Node('c'), None)]) for i in range(1500)])])) diff --git a/urwidtrees/nested.py b/urwidtrees/nested.py index fd71a87..a05845c 100644 --- a/urwidtrees/nested.py +++ b/urwidtrees/nested.py @@ -98,33 +98,30 @@ def decorate(self, pos, widget, is_first=True): return self._get_decorated_entry(pos, widget, is_first) # Collapse API - def _get_subtree_for(self, pos): - """returns Tree that manages pos[-1]""" - return self._trees(pos)[-1] + def _on_subtree(self, pos, method, default=None): + """ + call `method` with pos[-1] on the tree that manages it, if that tree + can collapse; otherwise return `default`. A NestedTree nested + directly in another hands on to its own managing tree. + """ + tree = self + while isinstance(tree, NestedTree): + tree, pos = tree._trees(pos)[-1], pos[-1] + if isinstance(tree, CollapseMixin): + return getattr(tree, method)(pos) + return default def collapsible(self, pos): - res = False - subtree = self._get_subtree_for(pos) - if isinstance(subtree, (CollapseMixin, NestedTree)): - res = subtree.collapsible(pos[-1]) - return res + return self._on_subtree(pos, 'collapsible', False) def is_collapsed(self, pos): - res = False - subtree = self._get_subtree_for(pos) - if isinstance(subtree, (CollapseMixin, NestedTree)): - res = subtree.is_collapsed(pos[-1]) - return res + return self._on_subtree(pos, 'is_collapsed', False) def toggle_collapsed(self, pos): - subtree = self._get_subtree_for(pos) - if isinstance(subtree, (CollapseMixin, NestedTree)): - subtree.toggle_collapsed(pos[-1]) + self._on_subtree(pos, 'toggle_collapsed') def collapse(self, pos): - subtree = self._get_subtree_for(pos) - if isinstance(subtree, (CollapseMixin, NestedTree)): - subtree.collapse(pos[-1]) + self._on_subtree(pos, 'collapse') def collapse_all(self): for tree in self._expand_all(): @@ -132,9 +129,7 @@ def collapse_all(self): tree.collapse_all() def expand(self, pos): - subtree = self._get_subtree_for(pos) - if isinstance(subtree, (CollapseMixin, NestedTree)): - subtree.expand(pos[-1]) + self._on_subtree(pos, 'expand') def expand_all(self): self._expand_all()