Repository: Textualize/textual

Issue:
Tree.clear doesn't respect root expanded state
This is quite pedantic, but caused me trouble which will make more sense in the context of an upcoming discussion r.e. 'refreshing' a Tree widget and preserving which nodes are expanded vs collapsed.

---

TLDR:

When you create a `Tree` it creates a root node with:
https://github.com/Textualize/textual/blob/3b170b074802fab825a698ca66be2196871700cf/src/textual/widgets/_tree.py#L605

which has `expand` as `False` by default:
https://github.com/Textualize/textual/blob/3b170b074802fab825a698ca66be2196871700cf/src/textual/widgets/_tree.py#L641-L646

But when you `Tree.clear` it sets `expand` to `True`:

https://github.com/Textualize/textual/blob/3b170b074802fab825a698ca66be2196871700cf/src/textual/widgets/_tree.py#L710-L716


---


This is a problem for me because I'd _theorized_ that I could capture the `is_expanded` state of all nodes, then `clear()` and recreate it, then `expand()` the relevant nodes back to restore the previous 'expansion' state...

... but this doesn't _quite_ work because the `clear()` will `expand()` the root node regardless of whether it was expanded before.

Edge case, but I _presume_ was accidental?


Discussion:
I don't think it was intentional. The root should probably maintain its expanded state.

Resolving patch:
diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py
--- a/src/textual/widgets/_tree.py
+++ b/src/textual/widgets/_tree.py
@@ -717,13 +717,14 @@ def clear(self) -> Self:
         self._current_id = 0
         root_label = self.root._label
         root_data = self.root.data
+        root_expanded = self.root.is_expanded
         self.root = TreeNode(
             self,
             None,
             self._new_id(),
             root_label,
             root_data,
-            expanded=True,
+            expanded=root_expanded,
         )
         self._updates += 1
         self.refresh()


Test patch:
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
--- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
@@ -37109,6 +37109,163 @@
   
   '''
 # ---
+# name: test_tree_clearing_and_expansion
+  '''
+  <svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
+      <!-- Generated with Rich https://www.textualize.io -->
+      <style>
+  
+      @font-face {
+          font-family: "Fira Code";
+          src: local("FiraCode-Regular"),
+                  url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
+                  url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
+          font-style: normal;
+          font-weight: 400;
+      }
+      @font-face {
+          font-family: "Fira Code";
+          src: local("FiraCode-Bold"),
+                  url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
+                  url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
+          font-style: bold;
+          font-weight: 700;
+      }
+  
+      .terminal-3765519511-matrix {
+          font-family: Fira Code, monospace;
+          font-size: 20px;
+          line-height: 24.4px;
+          font-variant-east-asian: full-width;
+      }
+  
+      .terminal-3765519511-title {
+          font-size: 18px;
+          font-weight: bold;
+          font-family: arial;
+      }
+  
+      .terminal-3765519511-r1 { fill: #e2e3e3 }
+  .terminal-3765519511-r2 { fill: #211505;font-weight: bold }
+  .terminal-3765519511-r3 { fill: #1a1000;font-weight: bold }
+  .terminal-3765519511-r4 { fill: #c5c8c6 }
+      </style>
+  
+      <defs>
+      <clipPath id="terminal-3765519511-clip-terminal">
+        <rect x="0" y="0" width="975.0" height="584.5999999999999" />
+      </clipPath>
+      <clipPath id="terminal-3765519511-line-0">
+      <rect x="0" y="1.5" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3765519511-line-1">
+      <rect x="0" y="25.9" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3765519511-line-2">
+      <rect x="0" y="50.3" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3765519511-line-3">
+      <rect x="0" y="74.7" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3765519511-line-4">
+      <rect x="0" y="99.1" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3765519511-line-5">
+      <rect x="0" y="123.5" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3765519511-line-6">
+      <rect x="0" y="147.9" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3765519511-line-7">
+      <rect x="0" y="172.3" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3765519511-line-8">
+      <rect x="0" y="196.7" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3765519511-line-9">
+      <rect x="0" y="221.1" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3765519511-line-10">
+      <rect x="0" y="245.5" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3765519511-line-11">
+      <rect x="0" y="269.9" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3765519511-line-12">
+      <rect x="0" y="294.3" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3765519511-line-13">
+      <rect x="0" y="318.7" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3765519511-line-14">
+      <rect x="0" y="343.1" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3765519511-line-15">
+      <rect x="0" y="367.5" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3765519511-line-16">
+      <rect x="0" y="391.9" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3765519511-line-17">
+      <rect x="0" y="416.3" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3765519511-line-18">
+      <rect x="0" y="440.7" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3765519511-line-19">
+      <rect x="0" y="465.1" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3765519511-line-20">
+      <rect x="0" y="489.5" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3765519511-line-21">
+      <rect x="0" y="513.9" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-3765519511-line-22">
+      <rect x="0" y="538.3" width="976" height="24.65"/>
+              </clipPath>
+      </defs>
+  
+      <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="633.6" rx="8"/><text class="terminal-3765519511-title" fill="#c5c8c6" text-anchor="middle" x="496" y="27">TreeClearingSnapshotApp</text>
+              <g transform="translate(26,22)">
+              <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
+              <circle cx="22" cy="0" r="7" fill="#febc2e"/>
+              <circle cx="44" cy="0" r="7" fill="#28c840"/>
+              </g>
+          
+      <g transform="translate(9, 41)" clip-path="url(#terminal-3765519511-clip-terminal)">
+      <rect fill="#24292f" x="0" y="1.5" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#fea62b" x="24.4" y="1.5" width="48.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="73.2" y="1.5" width="414.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="488" y="1.5" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#cf7e00" x="512.4" y="1.5" width="61" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="573.4" y="1.5" width="402.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="25.9" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="488" y="25.9" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="50.3" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="488" y="50.3" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="74.7" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="488" y="74.7" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="99.1" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="488" y="99.1" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="123.5" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="488" y="123.5" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="147.9" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="488" y="147.9" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="172.3" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="488" y="172.3" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="196.7" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="488" y="196.7" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="221.1" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="488" y="221.1" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="245.5" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="488" y="245.5" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="269.9" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="488" y="269.9" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="294.3" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="488" y="294.3" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="318.7" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="488" y="318.7" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="343.1" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="488" y="343.1" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="367.5" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="488" y="367.5" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="391.9" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="488" y="391.9" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="416.3" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="488" y="416.3" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="440.7" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="488" y="440.7" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="465.1" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="488" y="465.1" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="489.5" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="488" y="489.5" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="513.9" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="488" y="513.9" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="538.3" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="488" y="538.3" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="562.7" width="488" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="488" y="562.7" width="488" height="24.65" shape-rendering="crispEdges"/>
+      <g class="terminal-3765519511-matrix">
+      <text class="terminal-3765519511-r1" x="0" y="20" textLength="24.4" clip-path="url(#terminal-3765519511-line-0)">▼&#160;</text><text class="terminal-3765519511-r2" x="24.4" y="20" textLength="48.8" clip-path="url(#terminal-3765519511-line-0)">Left</text><text class="terminal-3765519511-r1" x="488" y="20" textLength="24.4" clip-path="url(#terminal-3765519511-line-0)">▶&#160;</text><text class="terminal-3765519511-r3" x="512.4" y="20" textLength="61" clip-path="url(#terminal-3765519511-line-0)">Right</text><text class="terminal-3765519511-r4" x="976" y="20" textLength="12.2" clip-path="url(#terminal-3765519511-line-0)">
+  </text><text class="terminal-3765519511-r4" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-3765519511-line-1)">
+  </text><text class="terminal-3765519511-r4" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-3765519511-line-2)">
+  </text><text class="terminal-3765519511-r4" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-3765519511-line-3)">
+  </text><text class="terminal-3765519511-r4" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-3765519511-line-4)">
+  </text><text class="terminal-3765519511-r4" x="976" y="142" textLength="12.2" clip-path="url(#terminal-3765519511-line-5)">
+  </text><text class="terminal-3765519511-r4" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-3765519511-line-6)">
+  </text><text class="terminal-3765519511-r4" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-3765519511-line-7)">
+  </text><text class="terminal-3765519511-r4" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-3765519511-line-8)">
+  </text><text class="terminal-3765519511-r4" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-3765519511-line-9)">
+  </text><text class="terminal-3765519511-r4" x="976" y="264" textLength="12.2" clip-path="url(#terminal-3765519511-line-10)">
+  </text><text class="terminal-3765519511-r4" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-3765519511-line-11)">
+  </text><text class="terminal-3765519511-r4" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-3765519511-line-12)">
+  </text><text class="terminal-3765519511-r4" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-3765519511-line-13)">
+  </text><text class="terminal-3765519511-r4" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-3765519511-line-14)">
+  </text><text class="terminal-3765519511-r4" x="976" y="386" textLength="12.2" clip-path="url(#terminal-3765519511-line-15)">
+  </text><text class="terminal-3765519511-r4" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-3765519511-line-16)">
+  </text><text class="terminal-3765519511-r4" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-3765519511-line-17)">
+  </text><text class="terminal-3765519511-r4" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-3765519511-line-18)">
+  </text><text class="terminal-3765519511-r4" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-3765519511-line-19)">
+  </text><text class="terminal-3765519511-r4" x="976" y="508" textLength="12.2" clip-path="url(#terminal-3765519511-line-20)">
+  </text><text class="terminal-3765519511-r4" x="976" y="532.4" textLength="12.2" clip-path="url(#terminal-3765519511-line-21)">
+  </text><text class="terminal-3765519511-r4" x="976" y="556.8" textLength="12.2" clip-path="url(#terminal-3765519511-line-22)">
+  </text>
+      </g>
+      </g>
+  </svg>
+  
+  '''
+# ---
 # name: test_tree_example
   '''
   <svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
diff --git a/tests/snapshot_tests/snapshot_apps/tree_clearing.py b/tests/snapshot_tests/snapshot_apps/tree_clearing.py
new file mode 100644
--- /dev/null
+++ b/tests/snapshot_tests/snapshot_apps/tree_clearing.py
@@ -0,0 +1,30 @@
+from textual.app import App, ComposeResult
+from textual.widgets import Tree
+
+class TreeClearingSnapshotApp(App[None]):
+
+    CSS = """
+    Screen {
+        layout: horizontal;
+    }
+    """
+
+    @staticmethod
+    def _populate(tree: Tree) -> Tree:
+        for n in range(5):
+            branch = tree.root.add(str(n))
+            for m in range(5):
+                branch.add_leaf(f"{n}-{m}")
+        return tree
+
+    def compose(self) -> ComposeResult:
+        yield self._populate(Tree("Left", id="left"))
+        yield self._populate(Tree("Right", id="right"))
+
+    def on_mount(self) -> None:
+        self.query_one("#left", Tree).root.expand()
+        self.query_one("#left", Tree).clear()
+        self.query_one("#right", Tree).clear()
+
+if __name__ == "__main__":
+    TreeClearingSnapshotApp().run()
diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py
--- a/tests/snapshot_tests/test_snapshots.py
+++ b/tests/snapshot_tests/test_snapshots.py
@@ -967,3 +967,10 @@ def test_zero_scrollbar_size(snap_compare):
     """Regression test for missing content with 0 sized scrollbars"""
     # https://github.com/Textualize/textual/issues/3886
     assert snap_compare(SNAPSHOT_APPS_DIR / "zero_scrollbar_size.py")
+
+
+def test_tree_clearing_and_expansion(snap_compare):
+    """Test the Tree.root.is_expanded state after a Tree.clear"""
+    # https://github.com/Textualize/textual/issues/3557
+    assert snap_compare(SNAPSHOT_APPS_DIR / "tree_clearing.py")
+