diff --git a/CHANGELOG.md b/CHANGELOG.md index aa2923fac..21b136f51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). + +## Unrealeased + +### Changed + +- App `title` and `sub_title` attributes can be set to any type https://github.com/Textualize/textual/issues/2521 + +### Fixed + +- Fixed `ZeroDivisionError` in `resolve_fraction_unit` https://github.com/Textualize/textual/issues/2502 +- Fixed `TreeNode.expand` and `TreeNode.expand_all` not posting a `Tree.NodeExpanded` message https://github.com/Textualize/textual/issues/2535 +- Fixed `TreeNode.collapse` and `TreeNode.collapse_all` not posting a `Tree.NodeCollapsed` message https://github.com/Textualize/textual/issues/2535 +- Fixed `TreeNode.toggle` and `TreeNode.toggle_all` not posting a `Tree.NodeExpanded` or `Tree.NodeCollapsed` message https://github.com/Textualize/textual/issues/2535 + ## [0.24.1] - 2023-05-08 ### Fixed diff --git a/src/textual/_resolve.py b/src/textual/_resolve.py index ddbdd1d5c..79a1a1e3e 100644 --- a/src/textual/_resolve.py +++ b/src/textual/_resolve.py @@ -143,7 +143,7 @@ def resolve_fraction_unit( resolved: list[Fraction | None] = [None] * len(resolve) remaining_fraction = Fraction(sum(scalar.value for scalar, _, _ in resolve)) - while True: + while remaining_fraction > 0: remaining_space_changed = False resolve_fraction = Fraction(remaining_space, remaining_fraction) for index, (scalar, min_value, max_value) in enumerate(resolve): @@ -166,7 +166,7 @@ def resolve_fraction_unit( return ( Fraction(remaining_space, remaining_fraction) - if remaining_space + if remaining_space > 0 else Fraction(1) ) diff --git a/src/textual/app.py b/src/textual/app.py index 113a7a220..12f66cf04 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -316,6 +316,7 @@ class App(Generic[ReturnType], DOMNode): the name of the app if it doesn't. Assign a new value to this attribute to change the title. + The new value is always converted to string. """ self.sub_title = self.SUB_TITLE if self.SUB_TITLE is not None else "" @@ -328,6 +329,7 @@ class App(Generic[ReturnType], DOMNode): the file being worker on. Assign a new value to this attribute to change the sub-title. + The new value is always converted to string. """ self._logger = Logger(self._log) @@ -406,6 +408,14 @@ class App(Generic[ReturnType], DOMNode): self.set_class(self.dark, "-dark-mode") self.set_class(not self.dark, "-light-mode") + def validate_title(self, title: Any) -> str: + """Make sure the title is set to a string.""" + return str(title) + + def validate_sub_title(self, sub_title: Any) -> str: + """Make sure the sub-title is set to a string.""" + return str(sub_title) + @property def workers(self) -> WorkerManager: """The [worker](guide/workers/) manager. diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index aab6e9cc5..01cf2f180 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -207,6 +207,7 @@ class TreeNode(Generic[TreeDataType]): """ self._expanded = True self._updates += 1 + self._tree.post_message(Tree.NodeExpanded(self._tree, self)) if expand_all: for child in self.children: child._expand(expand_all) @@ -239,6 +240,7 @@ class TreeNode(Generic[TreeDataType]): """ self._expanded = False self._updates += 1 + self._tree.post_message(Tree.NodeCollapsed(self._tree, self)) if collapse_all: for child in self.children: child._collapse(collapse_all) @@ -1157,10 +1159,8 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): return if node.is_expanded: node.collapse() - self.post_message(self.NodeCollapsed(self, node)) else: node.expand() - self.post_message(self.NodeExpanded(self, node)) async def _on_click(self, event: events.Click) -> None: meta = event.style.meta diff --git a/tests/test_app.py b/tests/test_app.py index e529cfbd7..54bde8221 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -36,3 +36,33 @@ async def test_hover_update_styles(): # We've hovered, so ensure the pseudoclass is present and background changed assert button.pseudo_classes == {"enabled", "hover"} assert button.styles.background != initial_background + + +def test_setting_title(): + app = MyApp() + app.title = None + assert app.title == "None" + + app.title = "" + assert app.title == "" + + app.title = 0.125 + assert app.title == "0.125" + + app.title = [True, False, 2] + assert app.title == "[True, False, 2]" + + +def test_setting_sub_title(): + app = MyApp() + app.sub_title = None + assert app.sub_title == "None" + + app.sub_title = "" + assert app.sub_title == "" + + app.sub_title = 0.125 + assert app.sub_title == "0.125" + + app.sub_title = [True, False, 2] + assert app.sub_title == "[True, False, 2]" diff --git a/tests/test_resolve.py b/tests/test_resolve.py index c36e1a406..202299747 100644 --- a/tests/test_resolve.py +++ b/tests/test_resolve.py @@ -1,4 +1,5 @@ from fractions import Fraction +from itertools import chain import pytest @@ -122,3 +123,22 @@ def test_resolve_fraction_unit(): Fraction(32), resolve_dimension="width", ) == Fraction(2) + + +def test_resolve_issue_2502(): + """Test https://github.com/Textualize/textual/issues/2502""" + + widget = Widget() + widget.styles.width = "1fr" + widget.styles.min_width = 11 + + assert isinstance( + resolve_fraction_unit( + (widget.styles,), + Size(80, 24), + Size(80, 24), + Fraction(10), + resolve_dimension="width", + ), + Fraction, + ) diff --git a/tests/tree/test_tree_messages.py b/tests/tree/test_tree_messages.py index ef742319a..bc55a7d81 100644 --- a/tests/tree/test_tree_messages.py +++ b/tests/tree/test_tree_messages.py @@ -78,6 +78,20 @@ async def test_tree_node_expanded_message() -> None: assert pilot.app.messages == [("NodeExpanded", "test-tree")] +async def tree_node_expanded_by_code_message() -> None: + """Expanding a node via the API should result in an expanded message being posted.""" + async with TreeApp().run_test() as pilot: + pilot.app.query_one(Tree).root.children[0].expand() + assert pilot.app.messages == [("NodeExpanded", "test-tree")] + + +async def tree_node_all_expanded_by_code_message() -> None: + """Expanding all nodes via the API should result in expanded messages being posted.""" + async with TreeApp().run_test() as pilot: + pilot.app.query_one(Tree).root.children[0].expand_all() + assert pilot.app.messages == [("NodeExpanded", "test-tree")] + + async def test_tree_node_collapsed_message() -> None: """Collapsing a node should result in a collapsed message being emitted.""" async with TreeApp().run_test() as pilot: @@ -89,6 +103,46 @@ async def test_tree_node_collapsed_message() -> None: ] +async def tree_node_collapsed_by_code_message() -> None: + """Collapsing a node via the API should result in a collapsed message being posted.""" + async with TreeApp().run_test() as pilot: + pilot.app.query_one(Tree).root.children[0].expand().collapse() + assert pilot.app.messages == [ + ("NodeExpanded", "test-tree"), + ("NodeCollapsed", "test-tree"), + ] + + +async def tree_node_all_collapsed_by_code_message() -> None: + """Collapsing all nodes via the API should result in collapsed messages being posted.""" + async with TreeApp().run_test() as pilot: + pilot.app.query_one(Tree).root.children[0].expand_all().collapse_all() + assert pilot.app.messages == [ + ("NodeExpanded", "test-tree"), + ("NodeCollapsed", "test-tree"), + ] + + +async def tree_node_toggled_by_code_message() -> None: + """Toggling a node twice via the API should result in expanded and collapsed messages.""" + async with TreeApp().run_test() as pilot: + pilot.app.query_one(Tree).root.children[0].toggle().toggle() + assert pilot.app.messages == [ + ("NodeExpanded", "test-tree"), + ("NodeCollapsed", "test-tree"), + ] + + +async def tree_node_all_toggled_by_code_message() -> None: + """Toggling all nodes twice via the API should result in expanded and collapsed messages.""" + async with TreeApp().run_test() as pilot: + pilot.app.query_one(Tree).root.children[0].toggle_all().toggle_all() + assert pilot.app.messages == [ + ("NodeExpanded", "test-tree"), + ("NodeCollapsed", "test-tree"), + ] + + async def test_tree_node_highlighted_message() -> None: """Highlighting a node should result in a highlighted message being emitted.""" async with TreeApp().run_test() as pilot: