From 3604fc8c7dca4f6b81ad3d348556ac7cab8a544e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Fri, 20 Jan 2023 13:48:53 +0000 Subject: [PATCH 01/34] Typo. --- src/textual/widget.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/textual/widget.py b/src/textual/widget.py index 1088e3b42..511319fda 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -1,7 +1,6 @@ from __future__ import annotations from asyncio import Lock, wait -from asyncio import Lock, create_task, wait from collections import Counter from fractions import Fraction from itertools import islice @@ -908,7 +907,7 @@ class Widget(DOMNode): show_horizontal = self.show_horizontal_scrollbar if overflow_x == "hidden": show_horizontal = False - if overflow_x == "scroll": + elif overflow_x == "scroll": show_horizontal = True elif overflow_x == "auto": show_horizontal = self.virtual_size.width > width From c74b81a8a68b29d8964f6f1368d5024d80ada09f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Fri, 20 Jan 2023 14:23:41 +0000 Subject: [PATCH 02/34] Potential fix for #1616. --- src/textual/widget.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/textual/widget.py b/src/textual/widget.py index 511319fda..2fd144876 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -1959,13 +1959,23 @@ class Widget(DOMNode): """ show_vertical_scrollbar, show_horizontal_scrollbar = self.scrollbars_enabled - scrollbar_size_horizontal = self.styles.scrollbar_size_horizontal - scrollbar_size_vertical = self.styles.scrollbar_size_vertical + styles = self.styles + scrollbar_size_horizontal = styles.scrollbar_size_horizontal + scrollbar_size_vertical = styles.scrollbar_size_vertical - if self.styles.scrollbar_gutter == "stable": + if styles.scrollbar_gutter == "stable": # Let's _always_ reserve some space, whether the scrollbar is actually displayed or not: show_vertical_scrollbar = True - scrollbar_size_vertical = self.styles.scrollbar_size_vertical + scrollbar_size_vertical = styles.scrollbar_size_vertical + + overflow_x = styles.overflow_x + show_horizontal_scrollbar = ( + show_horizontal_scrollbar or overflow_x == "scroll" + ) and overflow_x != "hidden" + overflow_y = styles.overflow_y + show_vertical_scrollbar = ( + show_vertical_scrollbar or overflow_y == "scroll" + ) and overflow_y != "hidden" if show_horizontal_scrollbar and show_vertical_scrollbar: (region, _, _, _) = region.split( From 58ad2cb15028549371d1f3e117fb5d76b1256d37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Fri, 20 Jan 2023 14:25:10 +0000 Subject: [PATCH 03/34] Add regression test for #1616. --- tests/test_overflow_change.py | 38 +++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 tests/test_overflow_change.py diff --git a/tests/test_overflow_change.py b/tests/test_overflow_change.py new file mode 100644 index 000000000..5f9d4e67b --- /dev/null +++ b/tests/test_overflow_change.py @@ -0,0 +1,38 @@ +"""Regression test for #1616 https://github.com/Textualize/textual/issues/1616""" +import pytest + + +from textual.app import App +from textual.containers import Vertical + + +@pytest.mark.xfail("Needs #1610 so that overflow changes trigger layout recomputation.") +async def test_overflow_change_updates_virtual_size_appropriately(): + class MyApp(App): + def compose(self): + yield Vertical() + + app = MyApp() + + async with app.run_test() as pilot: + vertical = app.query_one(Vertical) + + height = vertical.virtual_size.height + + vertical.styles.overflow_x = "scroll" + await pilot.pause() # Let changes propagate. + assert vertical.virtual_size.height < height + + vertical.styles.overflow_x = "hidden" + await pilot.pause() + assert vertical.virtual_size.height == height + + width = vertical.virtual_size.width + + vertical.styles.overflow_y = "scroll" + await pilot.pause() + assert vertical.virtual_size.width < width + + vertical.styles.overflow_y = "hidden" + await pilot.pause() + assert vertical.virtual_size.width == width From c26c04d1f6c8fd49d315fc3b872ee2b0f8d47c29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Fri, 20 Jan 2023 14:35:35 +0000 Subject: [PATCH 04/34] Update changelog. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3ffc3dbe..16e848ef0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed - Fixed `textual diagnose` crash on older supported Python versions. https://github.com/Textualize/textual/issues/1622 +- Programmatically setting `overflow_x`/`overflow_y` refreshes the layout correctly https://github.com/Textualize/textual/issues/1616 ### Changed From 60faf9f8e2f20bae2f3917cb623ee08d5cc8acab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Fri, 20 Jan 2023 14:47:41 +0000 Subject: [PATCH 05/34] Fix xfail usage. --- tests/test_overflow_change.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_overflow_change.py b/tests/test_overflow_change.py index 5f9d4e67b..da2f3d915 100644 --- a/tests/test_overflow_change.py +++ b/tests/test_overflow_change.py @@ -6,7 +6,9 @@ from textual.app import App from textual.containers import Vertical -@pytest.mark.xfail("Needs #1610 so that overflow changes trigger layout recomputation.") +@pytest.mark.xfail( + reason="Needs #1610 so that overflow changes trigger layout recomputation." +) async def test_overflow_change_updates_virtual_size_appropriately(): class MyApp(App): def compose(self): From 731f4fc93ba784c6c972984bdfc1f5a07bfa05d9 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 23 Jan 2023 10:50:24 +0000 Subject: [PATCH 06/34] Make Column and Row available for import via widgets.data_table See #1589. --- src/textual/widgets/data_table.py | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/textual/widgets/data_table.py diff --git a/src/textual/widgets/data_table.py b/src/textual/widgets/data_table.py new file mode 100644 index 000000000..d0316f387 --- /dev/null +++ b/src/textual/widgets/data_table.py @@ -0,0 +1,5 @@ +"""Make non-widget DataTable support classes available.""" + +from ._data_table import Column, Row + +__all__ = ["Column", "Row"] From d4c71588a7638e12a3f31bad751f1860fb4151a9 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 23 Jan 2023 10:57:55 +0000 Subject: [PATCH 07/34] Make TreeNode available for import via widgets.tree See #1589. --- src/textual/widgets/tree.py | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/textual/widgets/tree.py diff --git a/src/textual/widgets/tree.py b/src/textual/widgets/tree.py new file mode 100644 index 000000000..2e315bc23 --- /dev/null +++ b/src/textual/widgets/tree.py @@ -0,0 +1,5 @@ +"""Make non-widget Tree support classes available.""" + +from ._tree import TreeNode + +__all__ = ["TreeNode"] From 33724e469749a65e653bfccf7aaa0cbfcee90735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Mon, 23 Jan 2023 17:26:57 +0000 Subject: [PATCH 08/34] Revert "Potential fix for #1616." This reverts commit c74b81a8a68b29d8964f6f1368d5024d80ada09f. --- src/textual/widget.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/src/textual/widget.py b/src/textual/widget.py index 2fd144876..511319fda 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -1959,23 +1959,13 @@ class Widget(DOMNode): """ show_vertical_scrollbar, show_horizontal_scrollbar = self.scrollbars_enabled - styles = self.styles - scrollbar_size_horizontal = styles.scrollbar_size_horizontal - scrollbar_size_vertical = styles.scrollbar_size_vertical + scrollbar_size_horizontal = self.styles.scrollbar_size_horizontal + scrollbar_size_vertical = self.styles.scrollbar_size_vertical - if styles.scrollbar_gutter == "stable": + if self.styles.scrollbar_gutter == "stable": # Let's _always_ reserve some space, whether the scrollbar is actually displayed or not: show_vertical_scrollbar = True - scrollbar_size_vertical = styles.scrollbar_size_vertical - - overflow_x = styles.overflow_x - show_horizontal_scrollbar = ( - show_horizontal_scrollbar or overflow_x == "scroll" - ) and overflow_x != "hidden" - overflow_y = styles.overflow_y - show_vertical_scrollbar = ( - show_vertical_scrollbar or overflow_y == "scroll" - ) and overflow_y != "hidden" + scrollbar_size_vertical = self.styles.scrollbar_size_vertical if show_horizontal_scrollbar and show_vertical_scrollbar: (region, _, _, _) = region.split( From 4adfe69ec90499a50a966ae655c05276f14a6acf Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 23 Jan 2023 17:37:58 +0000 Subject: [PATCH 09/34] Remove TreeNode as a pseudo-widget This encourages importing it from `textual.widgets.tree` instead, keeping it in line with the other changes made for #1637. Note this is a breaking change. --- examples/json_tree.py | 3 ++- src/textual/widgets/__init__.py | 2 -- src/textual/widgets/_tree_node.py | 1 - tests/tree/test_tree_node_children.py | 3 ++- tests/tree/test_tree_node_label.py | 3 ++- tests/tree/test_tree_node_parent.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 src/textual/widgets/_tree_node.py diff --git a/examples/json_tree.py b/examples/json_tree.py index 7380fcc25..45cede3c5 100644 --- a/examples/json_tree.py +++ b/examples/json_tree.py @@ -4,7 +4,8 @@ from pathlib import Path from rich.text import Text from textual.app import App, ComposeResult -from textual.widgets import Header, Footer, Tree, TreeNode +from textual.widgets import Header, Footer, Tree +from textual.widgets.tree import TreeNode class TreeApp(App): diff --git a/src/textual/widgets/__init__.py b/src/textual/widgets/__init__.py index 4628cbe92..295b8bf12 100644 --- a/src/textual/widgets/__init__.py +++ b/src/textual/widgets/__init__.py @@ -23,7 +23,6 @@ if typing.TYPE_CHECKING: from ._static import Static from ._text_log import TextLog from ._tree import Tree - from ._tree_node import TreeNode from ._welcome import Welcome from ..widget import Widget @@ -44,7 +43,6 @@ __all__ = [ "Static", "TextLog", "Tree", - "TreeNode", "Welcome", ] diff --git a/src/textual/widgets/_tree_node.py b/src/textual/widgets/_tree_node.py deleted file mode 100644 index e6c57fb61..000000000 --- a/src/textual/widgets/_tree_node.py +++ /dev/null @@ -1 +0,0 @@ -from ._tree import TreeNode as TreeNode diff --git a/tests/tree/test_tree_node_children.py b/tests/tree/test_tree_node_children.py index eb5c949c0..d6c5c7e4e 100644 --- a/tests/tree/test_tree_node_children.py +++ b/tests/tree/test_tree_node_children.py @@ -1,5 +1,6 @@ import pytest -from textual.widgets import Tree, TreeNode +from textual.widgets import Tree +from textual.widgets.tree import TreeNode def label_of(node: TreeNode[None]): diff --git a/tests/tree/test_tree_node_label.py b/tests/tree/test_tree_node_label.py index e64fcf24d..7d7a04329 100644 --- a/tests/tree/test_tree_node_label.py +++ b/tests/tree/test_tree_node_label.py @@ -1,4 +1,5 @@ -from textual.widgets import Tree, TreeNode +from textual.widgets import Tree +from textual.widgets.tree import TreeNode from rich.text import Text diff --git a/tests/tree/test_tree_node_parent.py b/tests/tree/test_tree_node_parent.py index b9d85af43..87e66ebc4 100644 --- a/tests/tree/test_tree_node_parent.py +++ b/tests/tree/test_tree_node_parent.py @@ -1,4 +1,4 @@ -from textual.widgets import TreeNode, Tree +from textual.widgets import Tree def test_tree_node_parent() -> None: From 289135a1c9400259f3316d70c92d5c2d10cc4a3d Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 23 Jan 2023 17:42:08 +0000 Subject: [PATCH 10/34] Update the CHANGELOG This aims to make it clear that a small but significant breaking change has taken place. See #1637. --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3ffc3dbe..74196cae6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ 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/). +## [0.11.0] - Unreleased + +### Changed + +- Breaking change: `TreeNode` can no longer be imported from `textual.widgets`; it is now available via `from textual.widgets.tree import TreeNode`. https://github.com/Textualize/textual/pull/1637 + ## [0.10.1] - 2023-01-20 ### Added From bb1e8d2eec2cead36970ecf3ea62aba4c715d937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Mon, 23 Jan 2023 18:00:37 +0000 Subject: [PATCH 11/34] Refresh scrollbars when overflow changes. --- src/textual/css/_style_properties.py | 15 +++++++++++++++ src/textual/css/styles.py | 5 +++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py index bfc9f90cb..77ddf1e62 100644 --- a/src/textual/css/_style_properties.py +++ b/src/textual/css/_style_properties.py @@ -701,6 +701,9 @@ class StringEnumProperty: """ return obj.get_rule(self.name, self._default) + def _before_refresh(self, obj: StylesBase, value: str | None) -> None: + """Do any housekeeping before asking for a layout refresh after a value change.""" + def __set__(self, obj: StylesBase, value: str | None = None): """Set the string property and ensure it is in the set of allowed values. @@ -714,6 +717,7 @@ class StringEnumProperty: _rich_traceback_omit = True if value is None: if obj.clear_rule(self.name): + self._before_refresh(obj, value) obj.refresh(layout=self._layout) else: if value not in self._valid_values: @@ -726,9 +730,20 @@ class StringEnumProperty: ), ) if obj.set_rule(self.name, value): + self._before_refresh(obj, value) obj.refresh(layout=self._layout) +class OverflowProperty(StringEnumProperty): + """Descriptor for overflow styles that forces widgets to refresh scrollbars.""" + + def _before_refresh(self, obj: StylesBase, value: str | None) -> None: + from ..widget import Widget # Avoid circular import + + if isinstance(obj.node, Widget): + obj.node._refresh_scrollbars() + + class NameProperty: """Descriptor for getting and setting name properties.""" diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py index c04b24a48..1477107f8 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -26,6 +26,7 @@ from ._style_properties import ( NameListProperty, NameProperty, OffsetProperty, + OverflowProperty, ScalarListProperty, ScalarProperty, SpacingProperty, @@ -246,8 +247,8 @@ class StylesBase(ABC): dock = DockProperty() - overflow_x = StringEnumProperty(VALID_OVERFLOW, "hidden") - overflow_y = StringEnumProperty(VALID_OVERFLOW, "hidden") + overflow_x = OverflowProperty(VALID_OVERFLOW, "hidden") + overflow_y = OverflowProperty(VALID_OVERFLOW, "hidden") layer = NameProperty() layers = NameListProperty() From cb73dd4ba9c061b980cd1dc9f5fed9f179d8742c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Mon, 23 Jan 2023 18:00:44 +0000 Subject: [PATCH 12/34] Remove xfail marker. --- tests/test_overflow_change.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/test_overflow_change.py b/tests/test_overflow_change.py index da2f3d915..1ebd39765 100644 --- a/tests/test_overflow_change.py +++ b/tests/test_overflow_change.py @@ -6,9 +6,6 @@ from textual.app import App from textual.containers import Vertical -@pytest.mark.xfail( - reason="Needs #1610 so that overflow changes trigger layout recomputation." -) async def test_overflow_change_updates_virtual_size_appropriately(): class MyApp(App): def compose(self): From f93e14028d59a7001abc43c64df253d355ad895c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Mon, 23 Jan 2023 18:02:57 +0000 Subject: [PATCH 13/34] Lift styles. --- src/textual/widget.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/textual/widget.py b/src/textual/widget.py index 511319fda..da446f8c9 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -1959,13 +1959,14 @@ class Widget(DOMNode): """ show_vertical_scrollbar, show_horizontal_scrollbar = self.scrollbars_enabled - scrollbar_size_horizontal = self.styles.scrollbar_size_horizontal - scrollbar_size_vertical = self.styles.scrollbar_size_vertical + styles = self.styles + scrollbar_size_horizontal = styles.scrollbar_size_horizontal + scrollbar_size_vertical = styles.scrollbar_size_vertical - if self.styles.scrollbar_gutter == "stable": + if styles.scrollbar_gutter == "stable": # Let's _always_ reserve some space, whether the scrollbar is actually displayed or not: show_vertical_scrollbar = True - scrollbar_size_vertical = self.styles.scrollbar_size_vertical + scrollbar_size_vertical = styles.scrollbar_size_vertical if show_horizontal_scrollbar and show_vertical_scrollbar: (region, _, _, _) = region.split( From c1bac9cd8fd647daa671ce61c1bc1f03891a16ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Mon, 23 Jan 2023 18:03:55 +0000 Subject: [PATCH 14/34] Update changelog. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65a72f241..ff8783150 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fixed stuck screen https://github.com/Textualize/textual/issues/1632 - Fixed relative units in `grid-rows` and `grid-columns` being computed with respect to the wrong dimension https://github.com/Textualize/textual/issues/1406 +- Programmatically setting `overflow_x`/`overflow_y` refreshes the layout correctly https://github.com/Textualize/textual/issues/1616 ## [0.10.1] - 2023-01-20 @@ -21,7 +22,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed - Fixed `textual diagnose` crash on older supported Python versions. https://github.com/Textualize/textual/issues/1622 -- Programmatically setting `overflow_x`/`overflow_y` refreshes the layout correctly https://github.com/Textualize/textual/issues/1616 ### Changed From b0d46287e10b85290df8c5405eb1e4aa302b4060 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 24 Jan 2023 14:44:16 +0100 Subject: [PATCH 15/34] Fix hang when removing current widget --- src/textual/_context.py | 2 ++ src/textual/app.py | 11 +++++++---- src/textual/await_remove.py | 3 +++ src/textual/message_pump.py | 14 ++++++++++---- src/textual/widget.py | 27 +++++++++++++++++++++++++++ 5 files changed, 49 insertions(+), 8 deletions(-) diff --git a/src/textual/_context.py b/src/textual/_context.py index 04b264d33..625152a95 100644 --- a/src/textual/_context.py +++ b/src/textual/_context.py @@ -4,6 +4,7 @@ from contextvars import ContextVar if TYPE_CHECKING: from .app import App + from .message_pump import MessagePump class NoActiveAppError(RuntimeError): @@ -11,3 +12,4 @@ class NoActiveAppError(RuntimeError): active_app: ContextVar["App"] = ContextVar("active_app") +active_message_pump: ContextVar["MessagePump"] = ContextVar("active_message_pump") diff --git a/src/textual/app.py b/src/textual/app.py index 068b94e0b..d11bccc79 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -46,7 +46,7 @@ from ._animator import DEFAULT_EASING, Animatable, Animator, EasingFunction from ._ansi_sequences import SYNC_END, SYNC_START from ._asyncio import create_task from ._callback import invoke -from ._context import active_app +from ._context import active_app, active_message_pump from ._event_broker import NoHandler, extract_handler_actions from ._filter import LineFilter, Monochrome from ._path import _make_path_object_relative @@ -1113,6 +1113,7 @@ class App(Generic[ReturnType], DOMNode): def mount_all( self, widgets: Iterable[Widget], + *, before: int | str | Widget | None = None, after: int | str | Widget | None = None, ) -> AwaitMount: @@ -2103,7 +2104,7 @@ class App(Generic[ReturnType], DOMNode): """Remove nodes from DOM, and return an awaitable that awaits cleanup. Args: - widgets: List of nodes to remvoe. + widgets: List of nodes to remove. Returns: Awaitable that returns when the nodes have been fully removed. @@ -2131,13 +2132,15 @@ class App(Generic[ReturnType], DOMNode): prune_widgets_task(removed_widgets, finished_event), name="prune nodes" ) - return AwaitRemove(finished_event) + await_remove = AwaitRemove(finished_event) + self.call_next(await_remove) + return await_remove async def _prune_nodes(self, widgets: list[Widget]) -> None: """Remove nodes and children. Args: - widgets: _description_ + widgets: Widgets to remove. """ async with self._dom_lock: for widget in widgets: diff --git a/src/textual/await_remove.py b/src/textual/await_remove.py index cd794d8c6..7b7716fca 100644 --- a/src/textual/await_remove.py +++ b/src/textual/await_remove.py @@ -15,6 +15,9 @@ class AwaitRemove: """ self.finished_flag = finished_flag + async def __call__(self) -> None: + return await self + def __await__(self) -> Generator[None, None, None]: async def await_prune() -> None: """Wait for the prune operation to finish.""" diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 7cbaf2875..5c80d9274 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -17,7 +17,7 @@ from weakref import WeakSet from . import Logger, events, log, messages from ._asyncio import create_task from ._callback import invoke -from ._context import NoActiveAppError, active_app +from ._context import NoActiveAppError, active_app, active_message_pump from ._time import time from .case import camel_to_snake from .errors import DuplicateKeyHandlers @@ -313,8 +313,13 @@ class MessagePump(metaclass=MessagePumpMeta): Reactive._reset_object(self) await self._message_queue.put(None) if wait and self._task is not None and asyncio.current_task() != self._task: - # Ensure everything is closed before returning - await self._task + try: + running_widget = active_message_pump.get() + except LookupError: + await self._task + else: + if running_widget is not self: + await self._task def _start_messages(self) -> None: """Start messages task.""" @@ -382,7 +387,7 @@ class MessagePump(metaclass=MessagePumpMeta): break self._active_message = message - + context_token = active_message_pump.set(self) try: try: await self._dispatch_message(message) @@ -415,6 +420,7 @@ class MessagePump(metaclass=MessagePumpMeta): self.app._handle_exception(error) break finally: + active_message_pump.reset(context_token) self._active_message = None async def _flush_next_callbacks(self) -> None: diff --git a/src/textual/widget.py b/src/textual/widget.py index e6c9c2626..e48f0a69a 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -618,6 +618,33 @@ class Widget(DOMNode): self.call_next(await_mount) return await_mount + def mount_all( + self, + widgets: Iterable[Widget], + *, + before: int | str | Widget | None = None, + after: int | str | Widget | None = None, + ) -> AwaitMount: + """Mount widgets from an iterable. + + Args: + widgets: An iterable of widgets. + before: Optional location to mount before. + after: Optional location to mount after. + + Returns: + An awaitable object that waits for widgets to be mounted. + + Raises: + MountError: If there is a problem with the mount request. + + Note: + Only one of ``before`` or ``after`` can be provided. If both are + provided a ``MountError`` will be raised. + """ + await_mount = self.mount(*widgets, before=before, after=after) + return await_mount + def move_child( self, child: int | Widget, From f97392ac8bfa4302c881b4d6b095728d60a7ca4e Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 24 Jan 2023 15:24:55 +0100 Subject: [PATCH 16/34] added test --- src/textual/message_pump.py | 22 +++++++++++++--------- tests/test_widget.py | 29 +++++++++++++++++++++++++++-- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 5c80d9274..7d21948da 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -440,16 +440,20 @@ class MessagePump(metaclass=MessagePumpMeta): Args: message: A message object """ - _rich_traceback_guard = True - if message.no_dispatch: - return + context_token = active_message_pump.set(self) + try: + _rich_traceback_guard = True + if message.no_dispatch: + return - # Allow apps to treat events and messages separately - if isinstance(message, Event): - await self.on_event(message) - else: - await self._on_message(message) - await self._flush_next_callbacks() + # Allow apps to treat events and messages separately + if isinstance(message, Event): + await self.on_event(message) + else: + await self._on_message(message) + await self._flush_next_callbacks() + finally: + active_message_pump.reset(context_token) def _get_dispatch_methods( self, method_name: str, message: Message diff --git a/tests/test_widget.py b/tests/test_widget.py index e3c0a618c..1c9caca1c 100644 --- a/tests/test_widget.py +++ b/tests/test_widget.py @@ -1,13 +1,12 @@ import pytest -import rich from textual._node_list import DuplicateIds from textual.app import App, ComposeResult from textual.css.errors import StyleValueError from textual.css.query import NoMatches -from textual.dom import DOMNode from textual.geometry import Size from textual.widget import Widget, MountError +from textual.widgets import Label @pytest.mark.parametrize( @@ -157,3 +156,29 @@ def test_widget_mount_ids_must_be_unique_mounting_multiple_calls(parent): parent.mount(widget1) with pytest.raises(DuplicateIds): parent.mount(widget2) + + +# Regression test for https://github.com/Textualize/textual/issues/1634 +async def test_remove(): + class RemoveMeLabel(Label): + async def on_mount(self) -> None: + await self.action("app.remove_all") + + class Container(Widget): + async def clear(self) -> None: + await self.query("*").remove() + + class RemoveApp(App): + def compose(self) -> ComposeResult: + yield Container(RemoveMeLabel()) + + async def action_remove_all(self) -> None: + await self.query_one(Container).clear() + self.exit(123) + + app = RemoveApp() + async with app.run_test() as pilot: + await pilot.press("r") + await pilot.pause() + assert app.return_value == 123 + assert True From d9dd1dcbe890ce2ffd065eb7ff2b4d918558df83 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 24 Jan 2023 15:53:53 +0100 Subject: [PATCH 17/34] simplify --- src/textual/message_pump.py | 33 ++++++++++++++------------------- tests/test_widget.py | 2 +- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 7d21948da..c469b381d 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -316,14 +316,15 @@ class MessagePump(metaclass=MessagePumpMeta): try: running_widget = active_message_pump.get() except LookupError: + running_widget = None + + if running_widget is None or running_widget is not self: await self._task - else: - if running_widget is not self: - await self._task def _start_messages(self) -> None: """Start messages task.""" if self.app._running: + active_message_pump.set(self) self._task = create_task( self._process_messages(), name=f"message pump {self}" ) @@ -362,7 +363,6 @@ class MessagePump(metaclass=MessagePumpMeta): async def _process_messages_loop(self) -> None: """Process messages until the queue is closed.""" _rich_traceback_guard = True - while not self._closed: try: message = await self._get_message() @@ -387,7 +387,7 @@ class MessagePump(metaclass=MessagePumpMeta): break self._active_message = message - context_token = active_message_pump.set(self) + try: try: await self._dispatch_message(message) @@ -420,7 +420,6 @@ class MessagePump(metaclass=MessagePumpMeta): self.app._handle_exception(error) break finally: - active_message_pump.reset(context_token) self._active_message = None async def _flush_next_callbacks(self) -> None: @@ -440,20 +439,16 @@ class MessagePump(metaclass=MessagePumpMeta): Args: message: A message object """ - context_token = active_message_pump.set(self) - try: - _rich_traceback_guard = True - if message.no_dispatch: - return + _rich_traceback_guard = True + if message.no_dispatch: + return - # Allow apps to treat events and messages separately - if isinstance(message, Event): - await self.on_event(message) - else: - await self._on_message(message) - await self._flush_next_callbacks() - finally: - active_message_pump.reset(context_token) + # Allow apps to treat events and messages separately + if isinstance(message, Event): + await self.on_event(message) + else: + await self._on_message(message) + await self._flush_next_callbacks() def _get_dispatch_methods( self, method_name: str, message: Message diff --git a/tests/test_widget.py b/tests/test_widget.py index 1c9caca1c..d0ab0ea55 100644 --- a/tests/test_widget.py +++ b/tests/test_widget.py @@ -170,7 +170,7 @@ async def test_remove(): class RemoveApp(App): def compose(self) -> ComposeResult: - yield Container(RemoveMeLabel()) + yield Container(RemoveMeLabel(), RemoveMeLabel()) async def action_remove_all(self) -> None: await self.query_one(Container).clear() From 3673b0ff48c5764254fdd9bbc935d378bf08dc59 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 24 Jan 2023 16:01:37 +0100 Subject: [PATCH 18/34] keep reference to remove task --- src/textual/app.py | 4 ++-- src/textual/await_remove.py | 5 +++-- tests/test_widget.py | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/textual/app.py b/src/textual/app.py index d11bccc79..29c231a83 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -2128,11 +2128,11 @@ class App(Generic[ReturnType], DOMNode): removed_widgets = self._detach_from_dom(widgets) finished_event = asyncio.Event() - create_task( + remove_task = create_task( prune_widgets_task(removed_widgets, finished_event), name="prune nodes" ) - await_remove = AwaitRemove(finished_event) + await_remove = AwaitRemove(finished_event, remove_task) self.call_next(await_remove) return await_remove diff --git a/src/textual/await_remove.py b/src/textual/await_remove.py index 7b7716fca..cb9bc7b2b 100644 --- a/src/textual/await_remove.py +++ b/src/textual/await_remove.py @@ -1,19 +1,20 @@ """Provides the type of an awaitable remove.""" -from asyncio import Event +from asyncio import Event, Task from typing import Generator class AwaitRemove: """An awaitable returned by App.remove and DOMQuery.remove.""" - def __init__(self, finished_flag: Event) -> None: + def __init__(self, finished_flag: Event, task: Task) -> None: """Initialise the instance of ``AwaitRemove``. Args: finished_flag: The asyncio event to wait on. """ self.finished_flag = finished_flag + self._task = task async def __call__(self) -> None: return await self diff --git a/tests/test_widget.py b/tests/test_widget.py index d0ab0ea55..1c9caca1c 100644 --- a/tests/test_widget.py +++ b/tests/test_widget.py @@ -170,7 +170,7 @@ async def test_remove(): class RemoveApp(App): def compose(self) -> ComposeResult: - yield Container(RemoveMeLabel(), RemoveMeLabel()) + yield Container(RemoveMeLabel()) async def action_remove_all(self) -> None: await self.query_one(Container).clear() From 1cba4006b93873a2aefebf65cb1c16eec54c9979 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 24 Jan 2023 16:02:48 +0100 Subject: [PATCH 19/34] docstring --- src/textual/await_remove.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/textual/await_remove.py b/src/textual/await_remove.py index cb9bc7b2b..f8d61e3ff 100644 --- a/src/textual/await_remove.py +++ b/src/textual/await_remove.py @@ -12,6 +12,7 @@ class AwaitRemove: Args: finished_flag: The asyncio event to wait on. + task: The task which does the remove (required to keep a reference). """ self.finished_flag = finished_flag self._task = task From d10f0cbc79016d8682ce95ca41c6272a476001a8 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 24 Jan 2023 16:04:33 +0100 Subject: [PATCH 20/34] docstrings --- src/textual/app.py | 16 ++++++++++++---- src/textual/widget.py | 8 ++++++-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/textual/app.py b/src/textual/app.py index 29c231a83..8712353e1 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -1095,8 +1095,12 @@ class App(Generic[ReturnType], DOMNode): Args: *widgets: The widget(s) to mount. - before: Optional location to mount before. - after: Optional location to mount after. + before: Optional location to mount before. An `int` is the index + of the child to mount before, a `str` is a `query_one` query to + find the widget to mount before. + after: Optional location to mount after. An `int` is the index + of the child to mount after, a `str` is a `query_one` query to + find the widget to mount after. Returns: An awaitable object that waits for widgets to be mounted. @@ -1121,8 +1125,12 @@ class App(Generic[ReturnType], DOMNode): Args: widgets: An iterable of widgets. - before: Optional location to mount before. - after: Optional location to mount after. + before: Optional location to mount before. An `int` is the index + of the child to mount before, a `str` is a `query_one` query to + find the widget to mount before. + after: Optional location to mount after. An `int` is the index + of the child to mount after, a `str` is a `query_one` query to + find the widget to mount after. Returns: An awaitable object that waits for widgets to be mounted. diff --git a/src/textual/widget.py b/src/textual/widget.py index e48f0a69a..46708f6cd 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -629,8 +629,12 @@ class Widget(DOMNode): Args: widgets: An iterable of widgets. - before: Optional location to mount before. - after: Optional location to mount after. + before: Optional location to mount before. An `int` is the index + of the child to mount before, a `str` is a `query_one` query to + find the widget to mount before. + after: Optional location to mount after. An `int` is the index + of the child to mount after, a `str` is a `query_one` query to + find the widget to mount after. Returns: An awaitable object that waits for widgets to be mounted. From 92c03b0e11ebfe521a097ce96f06e8aebbb595ac Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 24 Jan 2023 15:15:46 +0000 Subject: [PATCH 21/34] Explain the significance of the before/after types (redux) Follows on from https://github.com/Textualize/textual/pull/1641 where I did the update for mounting via Widget but forgot about App. --- src/textual/app.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/textual/app.py b/src/textual/app.py index 068b94e0b..e08a434ff 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -1095,8 +1095,12 @@ class App(Generic[ReturnType], DOMNode): Args: *widgets: The widget(s) to mount. - before: Optional location to mount before. - after: Optional location to mount after. + before: Optional location to mount before. An `int` is the index + of the child to mount before, a `str` is a `query_one` query to + find the widget to mount before. + after: Optional location to mount after. An `int` is the index + of the child to mount after, a `str` is a `query_one` query to + find the widget to mount after. Returns: An awaitable object that waits for widgets to be mounted. @@ -1120,8 +1124,12 @@ class App(Generic[ReturnType], DOMNode): Args: widgets: An iterable of widgets. - before: Optional location to mount before. - after: Optional location to mount after. + before: Optional location to mount before. An `int` is the index + of the child to mount before, a `str` is a `query_one` query to + find the widget to mount before. + after: Optional location to mount after. An `int` is the index + of the child to mount after, a `str` is a `query_one` query to + find the widget to mount after. Returns: An awaitable object that waits for widgets to be mounted. From d101e3d4e72ae1ad7dbc21a1f854fcfa88a3983a Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 24 Jan 2023 20:16:50 +0000 Subject: [PATCH 22/34] Fix double-paste into `Input` See #1657. --- CHANGELOG.md | 1 + src/textual/widgets/_input.py | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b014c4fbb..74703fded 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fixed stuck screen https://github.com/Textualize/textual/issues/1632 - Fixed relative units in `grid-rows` and `grid-columns` being computed with respect to the wrong dimension https://github.com/Textualize/textual/issues/1406 +- Fixed double-paste into `Input` https://github.com/Textualize/textual/issues/1657 ## [0.10.1] - 2023-01-20 diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index 497b7c671..1046c732e 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -256,6 +256,7 @@ class Input(Widget, can_focus=True): def on_paste(self, event: events.Paste) -> None: line = event.text.splitlines()[0] self.insert_text_at_cursor(line) + event.stop() def on_click(self, event: events.Click) -> None: offset = event.get_content_offset(self) From aba2633f4457992e42160bdbb092706373c8a728 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 25 Jan 2023 12:00:51 +0100 Subject: [PATCH 23/34] Update tests/test_widget.py Co-authored-by: darrenburns --- tests/test_widget.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_widget.py b/tests/test_widget.py index 1c9caca1c..a06cf7857 100644 --- a/tests/test_widget.py +++ b/tests/test_widget.py @@ -181,4 +181,3 @@ async def test_remove(): await pilot.press("r") await pilot.pause() assert app.return_value == 123 - assert True From 9caf0cf5363e6e4081a98f01e62d9dbcda3726f2 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 25 Jan 2023 13:26:10 +0100 Subject: [PATCH 24/34] don't request sync on macOS app --- src/textual/drivers/linux_driver.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/textual/drivers/linux_driver.py b/src/textual/drivers/linux_driver.py index f5f90ae0b..bbdfbd564 100644 --- a/src/textual/drivers/linux_driver.py +++ b/src/textual/drivers/linux_driver.py @@ -146,8 +146,9 @@ class LinuxDriver(Driver): self._enable_bracketed_paste() def _request_terminal_sync_mode_support(self): - self.console.file.write("\033[?2026$p") - self.console.file.flush() + if self.console._environ.get("TERM", "") != "iTerm.app": + self.console.file.write("\033[?2026$p") + self.console.file.flush() @classmethod def _patch_lflag(cls, attrs: int) -> int: From 15af0cd2c62ae866b83ddef312783d2b12dca402 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 25 Jan 2023 13:28:37 +0100 Subject: [PATCH 25/34] wrong env var --- src/textual/drivers/linux_driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/drivers/linux_driver.py b/src/textual/drivers/linux_driver.py index bbdfbd564..35812d856 100644 --- a/src/textual/drivers/linux_driver.py +++ b/src/textual/drivers/linux_driver.py @@ -146,7 +146,7 @@ class LinuxDriver(Driver): self._enable_bracketed_paste() def _request_terminal_sync_mode_support(self): - if self.console._environ.get("TERM", "") != "iTerm.app": + if self.console._environ.get("TERM_PROGRAM", "") != "Apple_Terminal": self.console.file.write("\033[?2026$p") self.console.file.flush() From a292086a622f7cebc2d50ded9c550aa61e75a655 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 25 Jan 2023 13:39:24 +0100 Subject: [PATCH 26/34] comment and docstring --- src/textual/drivers/linux_driver.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/textual/drivers/linux_driver.py b/src/textual/drivers/linux_driver.py index 35812d856..2d1b5e1a0 100644 --- a/src/textual/drivers/linux_driver.py +++ b/src/textual/drivers/linux_driver.py @@ -145,7 +145,11 @@ class LinuxDriver(Driver): self._request_terminal_sync_mode_support() self._enable_bracketed_paste() - def _request_terminal_sync_mode_support(self): + def _request_terminal_sync_mode_support(self) -> None: + """Writes an escape sequence to query the terminal support for the sync protocol.""" + # Terminals should ignore this sequence if not supported. + # Apple terminal doesn't, and writes a single 'p' in to the terminal, + # so we will make a special case for Apple terminal (which doesn't support sync anyway). if self.console._environ.get("TERM_PROGRAM", "") != "Apple_Terminal": self.console.file.write("\033[?2026$p") self.console.file.flush() From 32bb79362c29714e4b351000de153e8b175dbf62 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Wed, 25 Jan 2023 14:24:37 +0000 Subject: [PATCH 27/34] Strip NULs from bracketed paste text as a Windows workaround See #1661 for lots of context. Long story short, in Windows Terminal it looks like any character that would requite the press of a modifier key causes a NUL to appear in the pasted text for that character. This feels like it could be a bug in Windows Terminal and we will investigate and report at some point. Meanwhile though this provides a workaround that has the paste experience work the same as I'm seeing on macOS (and I would imagine in most terminals on GNU/Linux too). --- src/textual/_xterm_parser.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/textual/_xterm_parser.py b/src/textual/_xterm_parser.py index 1bbec555d..7e1e719a2 100644 --- a/src/textual/_xterm_parser.py +++ b/src/textual/_xterm_parser.py @@ -118,7 +118,10 @@ class XTermParser(Parser[events.Event]): # ESC from the closing bracket, since at that point we didn't know what # the full escape code was. pasted_text = "".join(paste_buffer[:-1]) - on_token(events.Paste(self.sender, text=pasted_text)) + # Note the removal of NUL characters: https://github.com/Textualize/textual/issues/1661 + on_token( + events.Paste(self.sender, text=pasted_text.replace("\x000", "")) + ) paste_buffer.clear() character = ESC if use_prior_escape else (yield read1()) From 8145c08007f0a090f11bd9f9cbd5c2df84a83591 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Wed, 25 Jan 2023 14:30:56 +0000 Subject: [PATCH 28/34] Update the CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index df3fadf4f..40d10bbdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fixed relative units in `grid-rows` and `grid-columns` being computed with respect to the wrong dimension https://github.com/Textualize/textual/issues/1406 - Programmatically setting `overflow_x`/`overflow_y` refreshes the layout correctly https://github.com/Textualize/textual/issues/1616 - Fixed double-paste into `Input` https://github.com/Textualize/textual/issues/1657 +- Added a workaround for an apparent Windows Terminal paste issue https://github.com/Textualize/textual/issues/1661 ## [0.10.1] - 2023-01-20 From 44d48e244ef840bf327f4b0d8876e1ff6fcdd59c Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Wed, 25 Jan 2023 14:43:41 +0000 Subject: [PATCH 29/34] Fix typo in replace AKA: don't mix octal and hex Dave! --- src/textual/_xterm_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/_xterm_parser.py b/src/textual/_xterm_parser.py index 7e1e719a2..b97a1abd1 100644 --- a/src/textual/_xterm_parser.py +++ b/src/textual/_xterm_parser.py @@ -120,7 +120,7 @@ class XTermParser(Parser[events.Event]): pasted_text = "".join(paste_buffer[:-1]) # Note the removal of NUL characters: https://github.com/Textualize/textual/issues/1661 on_token( - events.Paste(self.sender, text=pasted_text.replace("\x000", "")) + events.Paste(self.sender, text=pasted_text.replace("\x00", "")) ) paste_buffer.clear() From dc5041f1e114cb124405727655a613cad27c3f7b Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 26 Jan 2023 12:21:49 +0100 Subject: [PATCH 30/34] update to intro page, fix for API docs --- docs/api/tree_node.md | 2 +- docs/index.md | 15 +++-- docs/widgets/tree.md | 22 ++++---- mkdocs.yml | 1 + poetry.lock | 124 +++++++++++++++++++++--------------------- pyproject.toml | 2 +- 6 files changed, 87 insertions(+), 79 deletions(-) diff --git a/docs/api/tree_node.md b/docs/api/tree_node.md index ad122443e..b136c4a6a 100644 --- a/docs/api/tree_node.md +++ b/docs/api/tree_node.md @@ -1 +1 @@ -::: textual.widgets.TreeNode +::: textual.widgets.tree.TreeNode diff --git a/docs/index.md b/docs/index.md index 1d776e820..6a065b413 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,16 +2,23 @@ Welcome to the [Textual](https://github.com/Textualize/textual) framework documentation. Built with ❤️ by [Textualize.io](https://www.textualize.io) +!!! tip -## In a hurry? + See the navigation links in the header or side-bars. Click the :octicons-three-bars-16: button (top left) on mobile. + + +[Get started](./getting_started.md){ .md-button .md-button--primary } or go straight to the [Tutorial](./tutorial.md) -See the navigation links in the header or side-bars. Click the :octicons-three-bars-16: button (top left) on mobile. -[Get started](./getting_started.md){ .md-button .md-button--primary } or [Tutorial](./tutorial.md){ .md-button .md-button--secondary } ## What is Textual? -Textual is a framework for building applications that run within your terminal. Text User Interfaces (TUIs) have a number of advantages over web and desktop apps. +Textual is a *Rapid Application Development* framework for Python. + + +Build sophisticated user interfaces with a simple Python API. Run your apps in the terminal and (*coming soon*) a web browser. + +
diff --git a/docs/widgets/tree.md b/docs/widgets/tree.md index 801d993f0..1667e11e1 100644 --- a/docs/widgets/tree.md +++ b/docs/widgets/tree.md @@ -21,7 +21,7 @@ The example below creates a simple tree. --8<-- "docs/examples/widgets/tree.py" ``` -A each tree widget has a "root" attribute which is an instance of a [TreeNode][textual.widgets.TreeNode]. Call [add()][textual.widgets.TreeNode.add] or [add_leaf()][textual.widgets.TreeNode.add_leaf] to add new nodes underneath the root. Both these methods return a TreeNode for the child, so you can add more levels. +A each tree widget has a "root" attribute which is an instance of a [TreeNode][textual.widgets.tree.TreeNode]. Call [add()][textual.widgets.tree.TreeNode.add] or [add_leaf()][textual.widgets.tree,TreeNode.add_leaf] to add new nodes underneath the root. Both these methods return a TreeNode for the child, so you can add more levels. ## Reactive Attributes @@ -43,9 +43,9 @@ The `Tree.NodeSelected` message is sent when the user selects a tree node. #### Attributes -| attribute | type | purpose | -| --------- | ------------------------------------ | -------------- | -| `node` | [TreeNode][textual.widgets.TreeNode] | Selected node. | +| attribute | type | purpose | +| --------- | ----------------------------------------- | -------------- | +| `node` | [TreeNode][textual.widgets.tree.TreeNode] | Selected node. | ### NodeExpanded @@ -54,9 +54,9 @@ The `Tree.NodeExpanded` message is sent when the user expands a node in the tree #### Attributes -| attribute | type | purpose | -| --------- | ------------------------------------ | -------------- | -| `node` | [TreeNode][textual.widgets.TreeNode] | Expanded node. | +| attribute | type | purpose | +| --------- | ----------------------------------------- | -------------- | +| `node` | [TreeNode][textual.widgets.tree.TreeNode] | Expanded node. | ### NodeCollapsed @@ -67,9 +67,9 @@ The `Tree.NodeCollapsed` message is sent when the user expands a node in the tre #### Attributes -| attribute | type | purpose | -| --------- | ------------------------------------ | --------------- | -| `node` | [TreeNode][textual.widgets.TreeNode] | Collapsed node. | +| attribute | type | purpose | +| --------- | ----------------------------------------- | --------------- | +| `node` | [TreeNode][textual.widgets.tree.TreeNode] | Collapsed node. | @@ -77,4 +77,4 @@ The `Tree.NodeCollapsed` message is sent when the user expands a node in the tre ## See Also * [Tree][textual.widgets.Tree] code reference -* [TreeNode][textual.widgets.TreeNode] code reference +* [TreeNode][textual.widgets.tree.TreeNode] code reference diff --git a/mkdocs.yml b/mkdocs.yml index 5c29f469c..a9b8de413 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -221,6 +221,7 @@ theme: - navigation.tabs - navigation.indexes - navigation.tabs.sticky + - navigation.footer - content.code.annotate - content.code.copy palette: diff --git a/poetry.lock b/poetry.lock index 885b92cd0..1f4cd039d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -171,7 +171,7 @@ python-versions = "*" [[package]] name = "coverage" -version = "7.0.5" +version = "7.1.0" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -322,7 +322,7 @@ socks = ["socksio (>=1.0.0,<2.0.0)"] [[package]] name = "identify" -version = "2.5.13" +version = "2.5.15" description = "File identification library for Python" category = "dev" optional = false @@ -521,7 +521,7 @@ doc = ["mkdocs-bootswatch (>=1,<2)", "mkdocs-minify-plugin (>=0.5.0,<0.6.0)", "p [[package]] name = "mkdocstrings" -version = "0.19.1" +version = "0.20.0" description = "Automatic documentation from sources, for MkDocs." category = "dev" optional = false @@ -626,7 +626,7 @@ python-versions = ">=3.7" [[package]] name = "pathspec" -version = "0.10.3" +version = "0.11.0" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false @@ -1028,7 +1028,7 @@ dev = ["aiohttp", "click", "msgpack"] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "0e3bcf48b37c16096a3c2b2f7d3f548494f9a22ebdee2e2c5d8ac74b80ab344e" +content-hash = "d76445ef1521cd4068907433b09d59fc1ed56f03e61063c5ad7376bb9823a8e7" [metadata.files] aiohttp = [ @@ -1193,57 +1193,57 @@ colored = [ {file = "colored-1.4.4.tar.gz", hash = "sha256:04ff4d4dd514274fe3b99a21bb52fb96f2688c01e93fba7bef37221e7cb56ce0"}, ] coverage = [ - {file = "coverage-7.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2a7f23bbaeb2a87f90f607730b45564076d870f1fb07b9318d0c21f36871932b"}, - {file = "coverage-7.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c18d47f314b950dbf24a41787ced1474e01ca816011925976d90a88b27c22b89"}, - {file = "coverage-7.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef14d75d86f104f03dea66c13188487151760ef25dd6b2dbd541885185f05f40"}, - {file = "coverage-7.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66e50680e888840c0995f2ad766e726ce71ca682e3c5f4eee82272c7671d38a2"}, - {file = "coverage-7.0.5-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9fed35ca8c6e946e877893bbac022e8563b94404a605af1d1e6accc7eb73289"}, - {file = "coverage-7.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d8d04e755934195bdc1db45ba9e040b8d20d046d04d6d77e71b3b34a8cc002d0"}, - {file = "coverage-7.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e109f1c9a3ece676597831874126555997c48f62bddbcace6ed17be3e372de8"}, - {file = "coverage-7.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0a1890fca2962c4f1ad16551d660b46ea77291fba2cc21c024cd527b9d9c8809"}, - {file = "coverage-7.0.5-cp310-cp310-win32.whl", hash = "sha256:be9fcf32c010da0ba40bf4ee01889d6c737658f4ddff160bd7eb9cac8f094b21"}, - {file = "coverage-7.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:cbfcba14a3225b055a28b3199c3d81cd0ab37d2353ffd7f6fd64844cebab31ad"}, - {file = "coverage-7.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:30b5fec1d34cc932c1bc04017b538ce16bf84e239378b8f75220478645d11fca"}, - {file = "coverage-7.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1caed2367b32cc80a2b7f58a9f46658218a19c6cfe5bc234021966dc3daa01f0"}, - {file = "coverage-7.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d254666d29540a72d17cc0175746cfb03d5123db33e67d1020e42dae611dc196"}, - {file = "coverage-7.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19245c249aa711d954623d94f23cc94c0fd65865661f20b7781210cb97c471c0"}, - {file = "coverage-7.0.5-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b05ed4b35bf6ee790832f68932baf1f00caa32283d66cc4d455c9e9d115aafc"}, - {file = "coverage-7.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:29de916ba1099ba2aab76aca101580006adfac5646de9b7c010a0f13867cba45"}, - {file = "coverage-7.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e057e74e53db78122a3979f908973e171909a58ac20df05c33998d52e6d35757"}, - {file = "coverage-7.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:411d4ff9d041be08fdfc02adf62e89c735b9468f6d8f6427f8a14b6bb0a85095"}, - {file = "coverage-7.0.5-cp311-cp311-win32.whl", hash = "sha256:52ab14b9e09ce052237dfe12d6892dd39b0401690856bcfe75d5baba4bfe2831"}, - {file = "coverage-7.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:1f66862d3a41674ebd8d1a7b6f5387fe5ce353f8719040a986551a545d7d83ea"}, - {file = "coverage-7.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b69522b168a6b64edf0c33ba53eac491c0a8f5cc94fa4337f9c6f4c8f2f5296c"}, - {file = "coverage-7.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436e103950d05b7d7f55e39beeb4d5be298ca3e119e0589c0227e6d0b01ee8c7"}, - {file = "coverage-7.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c56bec53d6e3154eaff6ea941226e7bd7cc0d99f9b3756c2520fc7a94e6d96"}, - {file = "coverage-7.0.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a38362528a9115a4e276e65eeabf67dcfaf57698e17ae388599568a78dcb029"}, - {file = "coverage-7.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f67472c09a0c7486e27f3275f617c964d25e35727af952869dd496b9b5b7f6a3"}, - {file = "coverage-7.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:220e3fa77d14c8a507b2d951e463b57a1f7810a6443a26f9b7591ef39047b1b2"}, - {file = "coverage-7.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ecb0f73954892f98611e183f50acdc9e21a4653f294dfbe079da73c6378a6f47"}, - {file = "coverage-7.0.5-cp37-cp37m-win32.whl", hash = "sha256:d8f3e2e0a1d6777e58e834fd5a04657f66affa615dae61dd67c35d1568c38882"}, - {file = "coverage-7.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:9e662e6fc4f513b79da5d10a23edd2b87685815b337b1a30cd11307a6679148d"}, - {file = "coverage-7.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:790e4433962c9f454e213b21b0fd4b42310ade9c077e8edcb5113db0818450cb"}, - {file = "coverage-7.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:49640bda9bda35b057b0e65b7c43ba706fa2335c9a9896652aebe0fa399e80e6"}, - {file = "coverage-7.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d66187792bfe56f8c18ba986a0e4ae44856b1c645336bd2c776e3386da91e1dd"}, - {file = "coverage-7.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:276f4cd0001cd83b00817c8db76730938b1ee40f4993b6a905f40a7278103b3a"}, - {file = "coverage-7.0.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95304068686545aa368b35dfda1cdfbbdbe2f6fe43de4a2e9baa8ebd71be46e2"}, - {file = "coverage-7.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:17e01dd8666c445025c29684d4aabf5a90dc6ef1ab25328aa52bedaa95b65ad7"}, - {file = "coverage-7.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea76dbcad0b7b0deb265d8c36e0801abcddf6cc1395940a24e3595288b405ca0"}, - {file = "coverage-7.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:50a6adc2be8edd7ee67d1abc3cd20678987c7b9d79cd265de55941e3d0d56499"}, - {file = "coverage-7.0.5-cp38-cp38-win32.whl", hash = "sha256:e4ce984133b888cc3a46867c8b4372c7dee9cee300335e2925e197bcd45b9e16"}, - {file = "coverage-7.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:4a950f83fd3f9bca23b77442f3a2b2ea4ac900944d8af9993743774c4fdc57af"}, - {file = "coverage-7.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c2155943896ac78b9b0fd910fb381186d0c345911f5333ee46ac44c8f0e43ab"}, - {file = "coverage-7.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:54f7e9705e14b2c9f6abdeb127c390f679f6dbe64ba732788d3015f7f76ef637"}, - {file = "coverage-7.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ee30375b409d9a7ea0f30c50645d436b6f5dfee254edffd27e45a980ad2c7f4"}, - {file = "coverage-7.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b78729038abea6a5df0d2708dce21e82073463b2d79d10884d7d591e0f385ded"}, - {file = "coverage-7.0.5-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13250b1f0bd023e0c9f11838bdeb60214dd5b6aaf8e8d2f110c7e232a1bff83b"}, - {file = "coverage-7.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2c407b1950b2d2ffa091f4e225ca19a66a9bd81222f27c56bd12658fc5ca1209"}, - {file = "coverage-7.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c76a3075e96b9c9ff00df8b5f7f560f5634dffd1658bafb79eb2682867e94f78"}, - {file = "coverage-7.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f26648e1b3b03b6022b48a9b910d0ae209e2d51f50441db5dce5b530fad6d9b1"}, - {file = "coverage-7.0.5-cp39-cp39-win32.whl", hash = "sha256:ba3027deb7abf02859aca49c865ece538aee56dcb4871b4cced23ba4d5088904"}, - {file = "coverage-7.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:949844af60ee96a376aac1ded2a27e134b8c8d35cc006a52903fc06c24a3296f"}, - {file = "coverage-7.0.5-pp37.pp38.pp39-none-any.whl", hash = "sha256:b9727ac4f5cf2cbf87880a63870b5b9730a8ae3a4a360241a0fdaa2f71240ff0"}, - {file = "coverage-7.0.5.tar.gz", hash = "sha256:051afcbd6d2ac39298d62d340f94dbb6a1f31de06dfaf6fcef7b759dd3860c45"}, + {file = "coverage-7.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3b946bbcd5a8231383450b195cfb58cb01cbe7f8949f5758566b881df4b33baf"}, + {file = "coverage-7.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ec8e767f13be637d056f7e07e61d089e555f719b387a7070154ad80a0ff31801"}, + {file = "coverage-7.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4a5a5879a939cb84959d86869132b00176197ca561c664fc21478c1eee60d75"}, + {file = "coverage-7.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b643cb30821e7570c0aaf54feaf0bfb630b79059f85741843e9dc23f33aaca2c"}, + {file = "coverage-7.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32df215215f3af2c1617a55dbdfb403b772d463d54d219985ac7cd3bf124cada"}, + {file = "coverage-7.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:33d1ae9d4079e05ac4cc1ef9e20c648f5afabf1a92adfaf2ccf509c50b85717f"}, + {file = "coverage-7.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:29571503c37f2ef2138a306d23e7270687c0efb9cab4bd8038d609b5c2393a3a"}, + {file = "coverage-7.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:63ffd21aa133ff48c4dff7adcc46b7ec8b565491bfc371212122dd999812ea1c"}, + {file = "coverage-7.1.0-cp310-cp310-win32.whl", hash = "sha256:4b14d5e09c656de5038a3f9bfe5228f53439282abcab87317c9f7f1acb280352"}, + {file = "coverage-7.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:8361be1c2c073919500b6601220a6f2f98ea0b6d2fec5014c1d9cfa23dd07038"}, + {file = "coverage-7.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:da9b41d4539eefd408c46725fb76ecba3a50a3367cafb7dea5f250d0653c1040"}, + {file = "coverage-7.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5b15ed7644ae4bee0ecf74fee95808dcc34ba6ace87e8dfbf5cb0dc20eab45a"}, + {file = "coverage-7.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d12d076582507ea460ea2a89a8c85cb558f83406c8a41dd641d7be9a32e1274f"}, + {file = "coverage-7.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2617759031dae1bf183c16cef8fcfb3de7617f394c813fa5e8e46e9b82d4222"}, + {file = "coverage-7.1.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4e4881fa9e9667afcc742f0c244d9364d197490fbc91d12ac3b5de0bf2df146"}, + {file = "coverage-7.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9d58885215094ab4a86a6aef044e42994a2bd76a446dc59b352622655ba6621b"}, + {file = "coverage-7.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ffeeb38ee4a80a30a6877c5c4c359e5498eec095878f1581453202bfacc8fbc2"}, + {file = "coverage-7.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3baf5f126f30781b5e93dbefcc8271cb2491647f8283f20ac54d12161dff080e"}, + {file = "coverage-7.1.0-cp311-cp311-win32.whl", hash = "sha256:ded59300d6330be27bc6cf0b74b89ada58069ced87c48eaf9344e5e84b0072f7"}, + {file = "coverage-7.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:6a43c7823cd7427b4ed763aa7fb63901ca8288591323b58c9cd6ec31ad910f3c"}, + {file = "coverage-7.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7a726d742816cb3a8973c8c9a97539c734b3a309345236cd533c4883dda05b8d"}, + {file = "coverage-7.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc7c85a150501286f8b56bd8ed3aa4093f4b88fb68c0843d21ff9656f0009d6a"}, + {file = "coverage-7.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5b4198d85a3755d27e64c52f8c95d6333119e49fd001ae5798dac872c95e0f8"}, + {file = "coverage-7.1.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddb726cb861c3117a553f940372a495fe1078249ff5f8a5478c0576c7be12050"}, + {file = "coverage-7.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:51b236e764840a6df0661b67e50697aaa0e7d4124ca95e5058fa3d7cbc240b7c"}, + {file = "coverage-7.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7ee5c9bb51695f80878faaa5598040dd6c9e172ddcf490382e8aedb8ec3fec8d"}, + {file = "coverage-7.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c31b75ae466c053a98bf26843563b3b3517b8f37da4d47b1c582fdc703112bc3"}, + {file = "coverage-7.1.0-cp37-cp37m-win32.whl", hash = "sha256:3b155caf3760408d1cb903b21e6a97ad4e2bdad43cbc265e3ce0afb8e0057e73"}, + {file = "coverage-7.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2a60d6513781e87047c3e630b33b4d1e89f39836dac6e069ffee28c4786715f5"}, + {file = "coverage-7.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f2cba5c6db29ce991029b5e4ac51eb36774458f0a3b8d3137241b32d1bb91f06"}, + {file = "coverage-7.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:beeb129cacea34490ffd4d6153af70509aa3cda20fdda2ea1a2be870dfec8d52"}, + {file = "coverage-7.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c45948f613d5d18c9ec5eaa203ce06a653334cf1bd47c783a12d0dd4fd9c851"}, + {file = "coverage-7.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef382417db92ba23dfb5864a3fc9be27ea4894e86620d342a116b243ade5d35d"}, + {file = "coverage-7.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c7c0d0827e853315c9bbd43c1162c006dd808dbbe297db7ae66cd17b07830f0"}, + {file = "coverage-7.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e5cdbb5cafcedea04924568d990e20ce7f1945a1dd54b560f879ee2d57226912"}, + {file = "coverage-7.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9817733f0d3ea91bea80de0f79ef971ae94f81ca52f9b66500c6a2fea8e4b4f8"}, + {file = "coverage-7.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:218fe982371ac7387304153ecd51205f14e9d731b34fb0568181abaf7b443ba0"}, + {file = "coverage-7.1.0-cp38-cp38-win32.whl", hash = "sha256:04481245ef966fbd24ae9b9e537ce899ae584d521dfbe78f89cad003c38ca2ab"}, + {file = "coverage-7.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:8ae125d1134bf236acba8b83e74c603d1b30e207266121e76484562bc816344c"}, + {file = "coverage-7.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2bf1d5f2084c3932b56b962a683074a3692bce7cabd3aa023c987a2a8e7612f6"}, + {file = "coverage-7.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:98b85dd86514d889a2e3dd22ab3c18c9d0019e696478391d86708b805f4ea0fa"}, + {file = "coverage-7.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38da2db80cc505a611938d8624801158e409928b136c8916cd2e203970dde4dc"}, + {file = "coverage-7.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3164d31078fa9efe406e198aecd2a02d32a62fecbdef74f76dad6a46c7e48311"}, + {file = "coverage-7.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db61a79c07331e88b9a9974815c075fbd812bc9dbc4dc44b366b5368a2936063"}, + {file = "coverage-7.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ccb092c9ede70b2517a57382a601619d20981f56f440eae7e4d7eaafd1d1d09"}, + {file = "coverage-7.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:33ff26d0f6cc3ca8de13d14fde1ff8efe1456b53e3f0273e63cc8b3c84a063d8"}, + {file = "coverage-7.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d47dd659a4ee952e90dc56c97d78132573dc5c7b09d61b416a9deef4ebe01a0c"}, + {file = "coverage-7.1.0-cp39-cp39-win32.whl", hash = "sha256:d248cd4a92065a4d4543b8331660121b31c4148dd00a691bfb7a5cdc7483cfa4"}, + {file = "coverage-7.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:7ed681b0f8e8bcbbffa58ba26fcf5dbc8f79e7997595bf071ed5430d8c08d6f3"}, + {file = "coverage-7.1.0-pp37.pp38.pp39-none-any.whl", hash = "sha256:755e89e32376c850f826c425ece2c35a4fc266c081490eb0a841e7c1cb0d3bda"}, + {file = "coverage-7.1.0.tar.gz", hash = "sha256:10188fe543560ec4874f974b5305cd1a8bdcfa885ee00ea3a03733464c4ca265"}, ] distlib = [ {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, @@ -1362,8 +1362,8 @@ httpx = [ {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"}, ] identify = [ - {file = "identify-2.5.13-py2.py3-none-any.whl", hash = "sha256:8aa48ce56e38c28b6faa9f261075dea0a942dfbb42b341b4e711896cbb40f3f7"}, - {file = "identify-2.5.13.tar.gz", hash = "sha256:abb546bca6f470228785338a01b539de8a85bbf46491250ae03363956d8ebb10"}, + {file = "identify-2.5.15-py2.py3-none-any.whl", hash = "sha256:1f4b36c5f50f3f950864b2a047308743f064eaa6f6645da5e5c780d1c7125487"}, + {file = "identify-2.5.15.tar.gz", hash = "sha256:c22aa206f47cc40486ecf585d27ad5f40adbfc494a3fa41dc3ed0499a23b123f"}, ] idna = [ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, @@ -1470,8 +1470,8 @@ mkdocs-rss-plugin = [ {file = "mkdocs_rss_plugin-1.5.0-py2.py3-none-any.whl", hash = "sha256:2ab14c20bf6b7983acbe50181e7e4a0778731d9c2d5c38107ca7047a7abd2165"}, ] mkdocstrings = [ - {file = "mkdocstrings-0.19.1-py3-none-any.whl", hash = "sha256:32a38d88f67f65b264184ea71290f9332db750d189dea4200cbbe408d304c261"}, - {file = "mkdocstrings-0.19.1.tar.gz", hash = "sha256:d1037cacb4b522c1e8c164ed5d00d724a82e49dcee0af80db8fb67b384faeef9"}, + {file = "mkdocstrings-0.20.0-py3-none-any.whl", hash = "sha256:f17fc2c4f760ec302b069075ef9e31045aa6372ca91d2f35ded3adba8e25a472"}, + {file = "mkdocstrings-0.20.0.tar.gz", hash = "sha256:c757f4f646d4f939491d6bc9256bfe33e36c5f8026392f49eaa351d241c838e5"}, ] mkdocstrings-python = [ {file = "mkdocstrings-python-0.8.3.tar.gz", hash = "sha256:9ae473f6dc599339b09eee17e4d2b05d6ac0ec29860f3fc9b7512d940fc61adf"}, @@ -1656,8 +1656,8 @@ packaging = [ {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, ] pathspec = [ - {file = "pathspec-0.10.3-py3-none-any.whl", hash = "sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6"}, - {file = "pathspec-0.10.3.tar.gz", hash = "sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6"}, + {file = "pathspec-0.11.0-py3-none-any.whl", hash = "sha256:3a66eb970cbac598f9e5ccb5b2cf58930cd8e3ed86d393d541eaf2d8b1705229"}, + {file = "pathspec-0.11.0.tar.gz", hash = "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc"}, ] platformdirs = [ {file = "platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"}, diff --git a/pyproject.toml b/pyproject.toml index 99ee64632..e16e5253e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ black = "^22.3.0,<22.10.0" # macos wheel issue on 22.10 mypy = "^0.990" pytest-cov = "^2.12.1" mkdocs = "^1.3.0" -mkdocstrings = {extras = ["python"], version = "^0.19.0"} +mkdocstrings = {extras = ["python"], version = "^0.20.0"} mkdocs-material = "^8.2.15" pre-commit = "^2.13.0" pytest-aiohttp = "^1.0.4" From 15f116d1602d246412905491eebe760559409038 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 26 Jan 2023 12:25:43 +0100 Subject: [PATCH 31/34] moved link --- docs/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index 6a065b413..1e84d9413 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ # Introduction -Welcome to the [Textual](https://github.com/Textualize/textual) framework documentation. Built with ❤️ by [Textualize.io](https://www.textualize.io) +Welcome to the [Textual](https://github.com/Textualize/textual) framework documentation. !!! tip @@ -13,7 +13,7 @@ Welcome to the [Textual](https://github.com/Textualize/textual) framework docume ## What is Textual? -Textual is a *Rapid Application Development* framework for Python. +Textual is a *Rapid Application Development* framework for Python, built by [Textualize.io](https://www.textualize.io). Build sophisticated user interfaces with a simple Python API. Run your apps in the terminal and (*coming soon*) a web browser. From 56ef37637bfb68c8c0ba6e08f642b8c3b086823e Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 26 Jan 2023 12:31:45 +0100 Subject: [PATCH 32/34] superfluous words --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 1e84d9413..90c197a1f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -33,7 +33,7 @@ Build sophisticated user interfaces with a simple Python API. Run your apps in t --- - Low system requirements. Run Textual on a single board computer if you want to. + Run Textual on a single board computer if you want to. From d01c8a7e07dda2c59043e2a0512ed6a708f8e59e Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 26 Jan 2023 12:34:10 +0100 Subject: [PATCH 33/34] more superfluous words --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 90c197a1f..4d1c05832 100644 --- a/docs/index.md +++ b/docs/index.md @@ -60,7 +60,7 @@ Build sophisticated user interfaces with a simple Python API. Run your apps in t -- :material-scale-balance:{ .lg .middle } __Open Source, MIT__ +- :material-scale-balance:{ .lg .middle } __Open Source__ --- From 211280e0be857d828f91bad35c53799e90b008a3 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 26 Jan 2023 12:37:57 +0100 Subject: [PATCH 34/34] Fixed wording of tree node docs --- docs/widgets/tree.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/widgets/tree.md b/docs/widgets/tree.md index 1667e11e1..7f0772566 100644 --- a/docs/widgets/tree.md +++ b/docs/widgets/tree.md @@ -21,7 +21,7 @@ The example below creates a simple tree. --8<-- "docs/examples/widgets/tree.py" ``` -A each tree widget has a "root" attribute which is an instance of a [TreeNode][textual.widgets.tree.TreeNode]. Call [add()][textual.widgets.tree.TreeNode.add] or [add_leaf()][textual.widgets.tree,TreeNode.add_leaf] to add new nodes underneath the root. Both these methods return a TreeNode for the child, so you can add more levels. +Tree widgets have a "root" attribute which is an instance of a [TreeNode][textual.widgets.tree.TreeNode]. Call [add()][textual.widgets.tree.TreeNode.add] or [add_leaf()][textual.widgets.tree,TreeNode.add_leaf] to add new nodes underneath the root. Both these methods return a TreeNode for the child which you can use to add additional levels. ## Reactive Attributes