From 584f3fcaa6e2e336e2a0a8229f15c506deacfb3e Mon Sep 17 00:00:00 2001 From: Josh Duncan <44387852+joshbduncan@users.noreply.github.com> Date: Tue, 26 Sep 2023 22:51:49 -0400 Subject: [PATCH 1/2] fix Tree(disabled=True) breaking app Fixes #3407 where `Tree` widget initialized/mounted with `disabled=True` would break it's parent app --- CHANGELOG.md | 1 + src/textual/widgets/_tree.py | 4 +- tests/tree/test_tree_availability.py | 116 +++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 tests/tree/test_tree_availability.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 330dac5bf7..6fbc58b44f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed - `Pilot.click`/`Pilot.hover` can't use `Screen` as a selector https://github.com/Textualize/textual/issues/3395 +- App exception when a `Tree` is initialized/mounted with `disabled=True` https://github.com/Textualize/textual/issues/3407 ### Added diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index b094f75681..c413d79109 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -597,8 +597,6 @@ def __init__( disabled: Whether the tree is disabled or not. """ - super().__init__(name=name, id=id, classes=classes, disabled=disabled) - text_label = self.process_label(label) self._updates = 0 @@ -610,6 +608,8 @@ def __init__( self._tree_lines_cached: list[_TreeLine] | None = None self._cursor_node: TreeNode[TreeDataType] | None = None + super().__init__(name=name, id=id, classes=classes, disabled=disabled) + @property def cursor_node(self) -> TreeNode[TreeDataType] | None: """The currently selected node, or ``None`` if no selection.""" diff --git a/tests/tree/test_tree_availability.py b/tests/tree/test_tree_availability.py new file mode 100644 index 0000000000..c6b58a5ae2 --- /dev/null +++ b/tests/tree/test_tree_availability.py @@ -0,0 +1,116 @@ +from typing import Any + +from textual import on +from textual.app import App, ComposeResult +from textual.widgets import Tree + + +class TreeApp(App[None]): + """Test tree app.""" + + def __init__(self, disabled: bool, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.disabled = disabled + self.messages: list[tuple[str, str]] = [] + + def compose(self) -> ComposeResult: + """Compose the child widgets.""" + yield Tree("Root", disabled=self.disabled, id="test-tree") + + def on_mount(self) -> None: + self.query_one(Tree).root.add("Child") + self.query_one(Tree).focus() + + def record( + self, + event: Tree.NodeSelected[None] + | Tree.NodeExpanded[None] + | Tree.NodeCollapsed[None] + | Tree.NodeHighlighted[None], + ) -> None: + self.messages.append( + (event.__class__.__name__, event.node.tree.id or "Unknown") + ) + + @on(Tree.NodeSelected) + def node_selected(self, event: Tree.NodeSelected) -> None: + self.record(event) + + @on(Tree.NodeExpanded) + def node_expanded(self, event: Tree.NodeExpanded) -> None: + self.record(event) + + @on(Tree.NodeCollapsed) + def node_collapsed(self, event: Tree.NodeCollapsed) -> None: + self.record(event) + + @on(Tree.NodeHighlighted) + def node_highlighted(self, event: Tree.NodeHighlighted) -> None: + self.record(event) + + +async def test_creating_disabled_tree(): + """Mounting a disabled `Tree` should result in the base `Widget` + having a `disabled` property equal to `True`""" + app = TreeApp(disabled=True) + async with app.run_test() as pilot: + tree = app.query_one(Tree) + assert not tree.focusable + assert tree.disabled + assert tree.cursor_line == 0 + await pilot.click("#test-tree") + await pilot.pause() + await pilot.press("down") + await pilot.pause() + assert tree.cursor_line == 0 + + +async def test_creating_enabled_tree(): + """Mounting an enabled `Tree` should result in the base `Widget` + having a `disabled` property equal to `False`""" + app = TreeApp(disabled=False) + async with app.run_test() as pilot: + tree = app.query_one(Tree) + assert tree.focusable + assert not tree.disabled + assert tree.cursor_line == 0 + await pilot.click("#test-tree") + await pilot.pause() + await pilot.press("down") + await pilot.pause() + assert tree.cursor_line == 1 + + +async def test_disabled_tree_node_selected_message() -> None: + """Clicking the root node disclosure triangle on a disabled tree + should result in no messages being emitted.""" + app = TreeApp(disabled=True) + async with app.run_test() as pilot: + tree = app.query_one(Tree) + # try clicking on a disabled tree + await pilot.click("#test-tree") + await pilot.pause() + assert not pilot.app.messages + # make sure messages DO flow after enabling a disabled tree + tree.disabled = False + await pilot.click("#test-tree") + await pilot.pause() + assert pilot.app.messages == [("NodeExpanded", "test-tree")] + + +async def test_enabled_tree_node_selected_message() -> None: + """Clicking the root node disclosure triangle on an enabled tree + should result in an `NodeExpanded` message being emitted.""" + app = TreeApp(disabled=False) + async with app.run_test() as pilot: + tree = app.query_one(Tree) + # try clicking on an enabled tree + await pilot.click("#test-tree") + await pilot.pause() + assert pilot.app.messages == [("NodeExpanded", "test-tree")] + tree.disabled = True + # make sure messages DO NOT flow after disabling an enabled tree + app.messages = [] + await pilot.click("#test-tree") + await pilot.pause() + assert not pilot.app.messages From 6698bbb3bc5c45baa471f1032b63461b0bfb7012 Mon Sep 17 00:00:00 2001 From: Josh Duncan <44387852+joshbduncan@users.noreply.github.com> Date: Tue, 26 Sep 2023 23:07:14 -0400 Subject: [PATCH 2/2] fix type error for GenericAlias --- tests/tree/test_tree_availability.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/tree/test_tree_availability.py b/tests/tree/test_tree_availability.py index c6b58a5ae2..c3f509446e 100644 --- a/tests/tree/test_tree_availability.py +++ b/tests/tree/test_tree_availability.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import Any from textual import on @@ -33,19 +35,19 @@ def record( ) @on(Tree.NodeSelected) - def node_selected(self, event: Tree.NodeSelected) -> None: + def node_selected(self, event: Tree.NodeSelected[None]) -> None: self.record(event) @on(Tree.NodeExpanded) - def node_expanded(self, event: Tree.NodeExpanded) -> None: + def node_expanded(self, event: Tree.NodeExpanded[None]) -> None: self.record(event) @on(Tree.NodeCollapsed) - def node_collapsed(self, event: Tree.NodeCollapsed) -> None: + def node_collapsed(self, event: Tree.NodeCollapsed[None]) -> None: self.record(event) @on(Tree.NodeHighlighted) - def node_highlighted(self, event: Tree.NodeHighlighted) -> None: + def node_highlighted(self, event: Tree.NodeHighlighted[None]) -> None: self.record(event)