From b526aea99f1f375d9e55357c37fb9d579e38fdd6 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 13 Feb 2023 11:14:52 +0000 Subject: [PATCH 01/18] implements screens view --- src/textual/app.py | 20 +++++++++++------ src/textual/dom.py | 33 +++++++++++++++++++++++++++-- src/textual/drivers/linux_driver.py | 3 +-- src/textual/walk.py | 8 +++---- 4 files changed, 49 insertions(+), 15 deletions(-) diff --git a/src/textual/app.py b/src/textual/app.py index 5261b5b6e0..224f7e06e7 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -25,13 +25,14 @@ Generic, Iterable, List, + Sequence, Type, TypeVar, Union, cast, overload, ) -from weakref import WeakSet, WeakValueDictionary +from weakref import WeakSet import nanoid import rich @@ -44,6 +45,7 @@ from . import Logger, LogGroup, LogVerbosity, actions, events, log, messages from ._animator import DEFAULT_EASING, Animatable, Animator, EasingFunction from ._ansi_sequences import SYNC_END, SYNC_START +from ._app_node_list import AppNodeList from ._asyncio import create_task from ._callback import invoke from ._context import active_app @@ -283,6 +285,7 @@ def __init__( _init_uvloop() super().__init__() + self._node_list_view = AppNodeList(self) self.features: frozenset[FeatureFlag] = parse_features(os.getenv("TEXTUAL", "")) self._filter: LineFilter | None = None @@ -385,9 +388,7 @@ def __init__( self.scroll_sensitivity_y: float = 2.0 """Number of lines to scroll in the Y direction with wheel or trackpad.""" - self._installed_screens: WeakValueDictionary[ - str, Screen | Callable[[], Screen] - ] = WeakValueDictionary() + self._installed_screens: dict[str, Screen | Callable[[], Screen]] = {} self._installed_screens.update(**self.SCREENS) self.devtools: DevtoolsClient | None = None @@ -420,6 +421,11 @@ def return_value(self) -> ReturnType | None: """ReturnType | None: The return type of the app.""" return self._return_value + @property + def children_view(self) -> Sequence["Widget"]: + """A view on to the children which contains just the screen.""" + return self._node_list_view + def animate( self, attribute: str, @@ -1265,7 +1271,7 @@ def switch_screen(self, screen: Screen | str) -> AwaitMount: return await_mount return AwaitMount(self.screen, []) - def install_screen(self, screen: Screen, name: str | None = None) -> AwaitMount: + def install_screen(self, screen: Screen, name: str | None = None) -> None: """Install a screen. Installing a screen prevents Textual from destroying it when it is no longer on the screen stack. @@ -1290,9 +1296,8 @@ def install_screen(self, screen: Screen, name: str | None = None) -> AwaitMount: "Can't install screen; {screen!r} has already been installed" ) self._installed_screens[name] = screen - _screen, await_mount = self._get_screen(name) # Ensures screen is running + # _screen, await_mount = self._get_screen(name) # Ensures screen is running self.log.system(f"{screen} INSTALLED name={name!r}") - return await_mount def uninstall_screen(self, screen: Screen | str) -> str | None: """Uninstall a screen. If the screen was not previously installed then this @@ -1894,6 +1899,7 @@ async def on_event(self, event: events.Event) -> None: # Handle input events that haven't been forwarded # If the event has been forwarded it may have bubbled up back to the App if isinstance(event, events.Compose): + self.log(event) screen = Screen(id="_default") self._register(self, screen) self._screen_stack.append(screen) diff --git a/src/textual/dom.py b/src/textual/dom.py index 1ed204aa9e..5317d01801 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -7,6 +7,7 @@ ClassVar, Iterable, Iterator, + Sequence, Type, TypeVar, cast, @@ -150,6 +151,11 @@ def __init__( super().__init__() + @property + def children_view(self) -> Sequence["Widget"]: + """A view on to the children.""" + return self.children + @property def auto_refresh(self) -> float | None: return self._auto_refresh @@ -499,6 +505,29 @@ def visible(self, new_value: bool) -> None: def tree(self) -> Tree: """Get a Rich tree object which will recursively render the structure of the node tree. + Returns: + A Rich object which may be printed. + """ + + def render_info(node: DOMNode) -> Pretty: + return Pretty(node) + + tree = Tree(render_info(self)) + + def add_children(tree, node): + for child in node.children_view: + info = render_info(child) + branch = tree.add(info) + if tree.children: + add_children(branch, child) + + add_children(tree, self) + return tree + + @property + def css_tree(self) -> Tree: + """Get a Rich tree object which will recursively render the structure of the node tree. + Returns: A Rich object which may be printed. """ @@ -527,7 +556,7 @@ def render_info(node: DOMNode) -> Columns: tree = Tree(render_info(self)) def add_children(tree, node): - for child in node.children: + for child in node.children_view: info = render_info(child) css = child.styles.css if css: @@ -541,7 +570,7 @@ def add_children(tree, node): ), ) branch = tree.add(info) - if tree.children: + if tree.children_view: add_children(branch, child) add_children(tree, self) diff --git a/src/textual/drivers/linux_driver.py b/src/textual/drivers/linux_driver.py index d2c00f61b4..4686cba05d 100644 --- a/src/textual/drivers/linux_driver.py +++ b/src/textual/drivers/linux_driver.py @@ -236,5 +236,4 @@ def more_data() -> bool: except Exception as error: log(error) finally: - with timer("selector.close"): - selector.close() + selector.close() diff --git a/src/textual/walk.py b/src/textual/walk.py index 0dc6724716..726af5e811 100644 --- a/src/textual/walk.py +++ b/src/textual/walk.py @@ -67,8 +67,8 @@ def walk_depth_first( else: if isinstance(node, check_type): yield node - if node.children: - push(iter(node.children)) + if node.children_view: + push(iter(node.children_view)) @overload @@ -122,9 +122,9 @@ def walk_breadth_first( if with_root and isinstance(root, check_type): yield root - extend(root.children) + extend(root.children_view) while queue: node = popleft() if isinstance(node, check_type): yield node - extend(node.children) + extend(node.children_view) From 2c0efd14934ec7922bd55af9b062139aef15dacc Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 13 Feb 2023 11:30:40 +0000 Subject: [PATCH 02/18] simplify install --- CHANGELOG.md | 1 + src/textual/app.py | 11 ++++------- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 974e5ee860..5fe3427ec3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - 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 - `Tree` now shows a (subdued) cursor for a highlighted node when focus has moved elsewhere https://github.com/Textualize/textual/issues/1471 - Breaking change: renamed `Checkbox` to `Switch` https://github.com/Textualize/textual/issues/1746 +- `App.install_screen` name is no longer optional ### Fixed diff --git a/src/textual/app.py b/src/textual/app.py index 224f7e06e7..950e4c1a98 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -1271,24 +1271,22 @@ def switch_screen(self, screen: Screen | str) -> AwaitMount: return await_mount return AwaitMount(self.screen, []) - def install_screen(self, screen: Screen, name: str | None = None) -> None: + def install_screen(self, screen: Screen, name: str) -> None: """Install a screen. Installing a screen prevents Textual from destroying it when it is no longer on the screen stack. + Note that you don't need to install a screen to use it. See [push_screen][textual.app.App.push_screen] + or [switch_screen][textual.app.App.switch_screen] to make a new screen current. Args: screen: Screen to install. - name: Unique name of screen or None to auto-generate. - Defaults to None. - + name: Unique name to identify the screen. Raises: ScreenError: If the screen can't be installed. Returns: An awaitable that awaits the mounting of the screen and its children. """ - if name is None: - name = nanoid.generate() if name in self._installed_screens: raise ScreenError(f"Can't install screen; {name!r} is already installed") if screen in self._installed_screens.values(): @@ -1296,7 +1294,6 @@ def install_screen(self, screen: Screen, name: str | None = None) -> None: "Can't install screen; {screen!r} has already been installed" ) self._installed_screens[name] = screen - # _screen, await_mount = self._get_screen(name) # Ensures screen is running self.log.system(f"{screen} INSTALLED name={name!r}") def uninstall_screen(self, screen: Screen | str) -> str | None: From cd9fdc1f350127563da8b24ec690c4480d332866 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 13 Feb 2023 11:35:28 +0000 Subject: [PATCH 03/18] renamed app node list to app children view --- src/textual/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/textual/app.py b/src/textual/app.py index 950e4c1a98..1dceaa6040 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -45,7 +45,7 @@ from . import Logger, LogGroup, LogVerbosity, actions, events, log, messages from ._animator import DEFAULT_EASING, Animatable, Animator, EasingFunction from ._ansi_sequences import SYNC_END, SYNC_START -from ._app_node_list import AppNodeList +from ._app_children_view import AppChildrenView from ._asyncio import create_task from ._callback import invoke from ._context import active_app @@ -285,7 +285,7 @@ def __init__( _init_uvloop() super().__init__() - self._node_list_view = AppNodeList(self) + self._node_list_view = AppChildrenView(self) self.features: frozenset[FeatureFlag] = parse_features(os.getenv("TEXTUAL", "")) self._filter: LineFilter | None = None From bf7f26b16a29e087171a00617ceb1c3e849a8739 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 13 Feb 2023 11:46:18 +0000 Subject: [PATCH 04/18] children view --- src/textual/_app_children_view.py | 86 +++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 src/textual/_app_children_view.py diff --git a/src/textual/_app_children_view.py b/src/textual/_app_children_view.py new file mode 100644 index 0000000000..78e3ebece2 --- /dev/null +++ b/src/textual/_app_children_view.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +import sys +from typing import TYPE_CHECKING, Any, Iterator, Sequence, overload +from weakref import ref + +import rich.repr + +if TYPE_CHECKING: + from .app import App + from .widget import Widget + + +class AppChildrenView(Sequence["Widget"]): + """Presents a view of the App's children which contains only the current screen.""" + + def __init__(self, app: App) -> None: + self._app = ref(app) + + @property + def app(self) -> App | None: + return self._app() + + @property + def _updates(self) -> int: + app = self.app + assert app is not None + return app.children._updates + + @property + def _nodes(self) -> Sequence[Widget]: + app = self.app + if app is None: + return [] + from .app import ScreenError + + try: + return [app.screen] + except ScreenError: + return [] + + def __bool__(self) -> bool: + return bool(self._nodes) + + def __length_hint__(self) -> int: + return 1 + + def __rich_repr__(self) -> rich.repr.Result: + yield self._nodes + + def __len__(self) -> int: + return len(self._nodes) + + def __contains__(self, widget: object) -> bool: + return widget in self._nodes + + def index(self, widget: Any, start: int = 0, stop: int = sys.maxsize) -> int: + """Return the index of the given widget. + + Args: + widget: The widget to find in the node list. + + Returns: + The index of the widget in the node list. + + Raises: + ValueError: If the widget is not in the node list. + """ + return self._nodes.index(widget, start, stop) + + def __iter__(self) -> Iterator[Widget]: + return iter(self._nodes) + + def __reversed__(self) -> Iterator[Widget]: + return reversed(self._nodes) + + @overload + def __getitem__(self, index: int) -> Widget: + ... + + @overload + def __getitem__(self, index: slice) -> list[Widget]: + ... + + def __getitem__(self, index: int | slice) -> Widget | list[Widget]: + return list(self._nodes)[index] From 5e6690a5e8cf1c4184f4c9635290dcaee4f50d7c Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 13 Feb 2023 12:11:54 +0000 Subject: [PATCH 05/18] changelog --- CHANGELOG.md | 8 ++++++-- src/textual/app.py | 7 +++++-- src/textual/dom.py | 3 ++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fe3427ec3..c93c854d5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,14 +19,18 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added Shift+scroll wheel and ctrl+scroll wheel to scroll horizontally - Added `Tree.action_toggle_node` to toggle a node without selecting, and bound it to Space https://github.com/Textualize/textual/issues/1433 - Added `Tree.reset` to fully reset a `Tree` https://github.com/Textualize/textual/issues/1437 -- Added DOMNode.watch and DOMNode.is_attached methods https://github.com/Textualize/textual/pull/1750 +- Added `DOMNode.watch` and `DOMNode.is_attached` methods https://github.com/Textualize/textual/pull/1750 +- Added `DOMNode.css_tree` which is a renderable that shows the DOM and CSS https://github.com/Textualize/textual/pull/1778 +- Added `DOMNode.children_view` which is a view on to a nodes children list, use for querying https://github.com/Textualize/textual/pull/1778 ### 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 - `Tree` now shows a (subdued) cursor for a highlighted node when focus has moved elsewhere https://github.com/Textualize/textual/issues/1471 - Breaking change: renamed `Checkbox` to `Switch` https://github.com/Textualize/textual/issues/1746 -- `App.install_screen` name is no longer optional +- `App.install_screen` name is no longer optional https://github.com/Textualize/textual/pull/1778 +- `App.query` now only includes the current screen https://github.com/Textualize/textual/pull/1778 +- `DOMNode.tree` now displays simple DOM structure only https://github.com/Textualize/textual/pull/1778 ### Fixed diff --git a/src/textual/app.py b/src/textual/app.py index 1dceaa6040..b431cc2f1a 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -1297,8 +1297,11 @@ def install_screen(self, screen: Screen, name: str) -> None: self.log.system(f"{screen} INSTALLED name={name!r}") def uninstall_screen(self, screen: Screen | str) -> str | None: - """Uninstall a screen. If the screen was not previously installed then this - method is a null-op. + """Uninstall a screen. + + If the screen was not previously installed then this method is a null-op. + Uninstalling a screen allows Textual to delete it when it is popped or switched. + Note that you Args: screen: The screen to uninstall or the name of a installed screen. diff --git a/src/textual/dom.py b/src/textual/dom.py index 5317d01801..217b0df2d5 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -526,7 +526,8 @@ def add_children(tree, node): @property def css_tree(self) -> Tree: - """Get a Rich tree object which will recursively render the structure of the node tree. + """Get a Rich tree object which will recursively render the structure of the node tree, + which also displays CSS and size information. Returns: A Rich object which may be printed. From f50c34389716c6b86a04e81c9eb0930cb73da5b9 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 13 Feb 2023 12:13:31 +0000 Subject: [PATCH 06/18] typing --- src/textual/dom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/dom.py b/src/textual/dom.py index 217b0df2d5..42478bf826 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -490,7 +490,7 @@ def visible(self) -> bool: return self.styles.visibility != "hidden" @visible.setter - def visible(self, new_value: bool) -> None: + def visible(self, new_value: bool | str) -> None: if isinstance(new_value, bool): self.styles.visibility = "visible" if new_value else "hidden" elif new_value in VALID_VISIBILITY: From fbfb5887e8e9364785e7c6afc33275697f5f29d0 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 13 Feb 2023 12:18:33 +0000 Subject: [PATCH 07/18] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c93c854d5b..3712f71e71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - `App.install_screen` name is no longer optional https://github.com/Textualize/textual/pull/1778 - `App.query` now only includes the current screen https://github.com/Textualize/textual/pull/1778 - `DOMNode.tree` now displays simple DOM structure only https://github.com/Textualize/textual/pull/1778 +- `App.install_screen` now returns None rather than AwaitMount https://github.com/Textualize/textual/pull/1778 ### Fixed From 204c048e2b07f4c9858688527b03d3141b6a8458 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 13 Feb 2023 13:39:09 +0000 Subject: [PATCH 08/18] children_view test --- src/textual/app.py | 5 ++++- tests/test_screens.py | 13 ++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/textual/app.py b/src/textual/app.py index b431cc2f1a..f723e1dba0 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -424,7 +424,10 @@ def return_value(self) -> ReturnType | None: @property def children_view(self) -> Sequence["Widget"]: """A view on to the children which contains just the screen.""" - return self._node_list_view + try: + return (self.screen,) + except ScreenError: + return () def animate( self, diff --git a/tests/test_screens.py b/tests/test_screens.py index e877c1ade1..a92b0493f9 100644 --- a/tests/test_screens.py +++ b/tests/test_screens.py @@ -42,9 +42,11 @@ class ScreensApp(App): pilot.app.pop_screen() -@skip_py310 async def test_screens(): app = App() + # There should be nothing in the children since the app hasn't run yet + assert not app.children + assert not app.children_view app._set_active() with pytest.raises(ScreenStackError): @@ -60,6 +62,10 @@ async def test_screens(): app.install_screen(screen1, "screen1") app.install_screen(screen2, "screen2") + # Installing a screen does not add it to the DOM + assert not app.children + assert not app.children_view + # Check they are installed assert app.is_screen_installed("screen1") assert app.is_screen_installed("screen2") @@ -84,17 +90,22 @@ async def test_screens(): assert app.screen_stack == [screen1] # Check it is current assert app.screen is screen1 + # There should be one item in the children view + assert app.children_view == (screen1,) # Switch to another screen app.switch_screen("screen2") # Check it has changed the stack and that it is current assert app.screen_stack == [screen2] assert app.screen is screen2 + assert app.children_view == (screen2,) # Push another screen app.push_screen("screen3") assert app.screen_stack == [screen2, screen3] assert app.screen is screen3 + # Only the current screen is in children_view + assert app.children_view == (screen3,) # Pop a screen assert app.pop_screen() is screen3 From a845016264c2e6f41ee60b6812a35742e655c5b5 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 13 Feb 2023 14:41:17 +0000 Subject: [PATCH 09/18] no longer required --- src/textual/_app_children_view.py | 86 ------------------------------- 1 file changed, 86 deletions(-) delete mode 100644 src/textual/_app_children_view.py diff --git a/src/textual/_app_children_view.py b/src/textual/_app_children_view.py deleted file mode 100644 index 78e3ebece2..0000000000 --- a/src/textual/_app_children_view.py +++ /dev/null @@ -1,86 +0,0 @@ -from __future__ import annotations - -import sys -from typing import TYPE_CHECKING, Any, Iterator, Sequence, overload -from weakref import ref - -import rich.repr - -if TYPE_CHECKING: - from .app import App - from .widget import Widget - - -class AppChildrenView(Sequence["Widget"]): - """Presents a view of the App's children which contains only the current screen.""" - - def __init__(self, app: App) -> None: - self._app = ref(app) - - @property - def app(self) -> App | None: - return self._app() - - @property - def _updates(self) -> int: - app = self.app - assert app is not None - return app.children._updates - - @property - def _nodes(self) -> Sequence[Widget]: - app = self.app - if app is None: - return [] - from .app import ScreenError - - try: - return [app.screen] - except ScreenError: - return [] - - def __bool__(self) -> bool: - return bool(self._nodes) - - def __length_hint__(self) -> int: - return 1 - - def __rich_repr__(self) -> rich.repr.Result: - yield self._nodes - - def __len__(self) -> int: - return len(self._nodes) - - def __contains__(self, widget: object) -> bool: - return widget in self._nodes - - def index(self, widget: Any, start: int = 0, stop: int = sys.maxsize) -> int: - """Return the index of the given widget. - - Args: - widget: The widget to find in the node list. - - Returns: - The index of the widget in the node list. - - Raises: - ValueError: If the widget is not in the node list. - """ - return self._nodes.index(widget, start, stop) - - def __iter__(self) -> Iterator[Widget]: - return iter(self._nodes) - - def __reversed__(self) -> Iterator[Widget]: - return reversed(self._nodes) - - @overload - def __getitem__(self, index: int) -> Widget: - ... - - @overload - def __getitem__(self, index: slice) -> list[Widget]: - ... - - def __getitem__(self, index: int | slice) -> Widget | list[Widget]: - return list(self._nodes)[index] From c6ce34d0094cfbb773be19564bee290835cebf55 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 13 Feb 2023 14:49:39 +0000 Subject: [PATCH 10/18] removed deprecate view object --- src/textual/app.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/textual/app.py b/src/textual/app.py index f723e1dba0..56a9228741 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -45,7 +45,6 @@ from . import Logger, LogGroup, LogVerbosity, actions, events, log, messages from ._animator import DEFAULT_EASING, Animatable, Animator, EasingFunction from ._ansi_sequences import SYNC_END, SYNC_START -from ._app_children_view import AppChildrenView from ._asyncio import create_task from ._callback import invoke from ._context import active_app @@ -285,7 +284,6 @@ def __init__( _init_uvloop() super().__init__() - self._node_list_view = AppChildrenView(self) self.features: frozenset[FeatureFlag] = parse_features(os.getenv("TEXTUAL", "")) self._filter: LineFilter | None = None From e75024ef5852f7d61e206db5a661c57284e5a0f8 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 13 Feb 2023 15:06:26 +0000 Subject: [PATCH 11/18] nanoid --- pyproject.toml | 1 - src/textual/app.py | 1 - 2 files changed, 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7b627f0af0..f49d9c615c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,6 @@ typing-extensions = "^4.0.0" aiohttp = { version = ">=3.8.1", optional = true } click = {version = ">=8.1.2", optional = true} msgpack = { version = ">=1.0.3", optional = true } -nanoid = ">=2.0.0" mkdocs-exclude = "^1.0.2" [tool.poetry.extras] diff --git a/src/textual/app.py b/src/textual/app.py index 56a9228741..58f66afcad 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -34,7 +34,6 @@ ) from weakref import WeakSet -import nanoid import rich import rich.repr from rich.console import Console, RenderableType From 6b5015b266b0ff963eb1fd2d039bb2d62938ffe8 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 13 Feb 2023 15:09:40 +0000 Subject: [PATCH 12/18] mass renaming --- src/textual/_layout.py | 4 ++-- src/textual/app.py | 20 ++++++++++---------- src/textual/dom.py | 12 ++++++------ src/textual/walk.py | 10 +++++----- src/textual/widget.py | 28 +++++++++++++--------------- src/textual/widgets/_list_view.py | 18 +++++++++--------- tests/test_screens.py | 10 +++++----- tests/test_unmount.py | 2 +- tests/test_widget_child_moving.py | 20 ++++++++++---------- tests/test_widget_mount_point.py | 2 +- tests/test_widget_mounting.py | 24 ++++++++++++------------ tests/test_widget_removing.py | 22 +++++++++++----------- 12 files changed, 85 insertions(+), 87 deletions(-) diff --git a/src/textual/_layout.py b/src/textual/_layout.py index 5556a705e6..5123d832e1 100644 --- a/src/textual/_layout.py +++ b/src/textual/_layout.py @@ -57,7 +57,7 @@ def get_content_width(self, widget: Widget, container: Size, viewport: Size) -> Returns: Width of the content. """ - if not widget.children: + if not widget._nodes: width = 0 else: # Use a size of 0, 0 to ignore relative sizes, since those are flexible anyway @@ -85,7 +85,7 @@ def get_content_height( Returns: Content height (in lines). """ - if not widget.children: + if not widget._nodes: height = 0 else: # Use a height of zero to ignore relative heights diff --git a/src/textual/app.py b/src/textual/app.py index 58f66afcad..88030283c6 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -419,7 +419,7 @@ def return_value(self) -> ReturnType | None: return self._return_value @property - def children_view(self) -> Sequence["Widget"]: + def children(self) -> Sequence["Widget"]: """A view on to the children which contains just the screen.""" try: return (self.screen,) @@ -1646,19 +1646,19 @@ def _register_child( # Now to figure out where to place it. If we've got a `before`... if before is not None: # ...it's safe to NodeList._insert before that location. - parent.children._insert(before, child) + parent._nodes._insert(before, child) elif after is not None and after != -1: # In this case we've got an after. -1 holds the special # position (for now) of meaning "okay really what I mean is # do an append, like if I'd asked to add with no before or # after". So... we insert before the next item in the node # list, iff after isn't -1. - parent.children._insert(after + 1, child) + parent._nodes._insert(after + 1, child) else: # At this point we appear to not be adding before or after, # or we've got a before/after value that really means # "please append". So... - parent.children._append(child) + parent._nodes._append(child) # Now that the widget is in the NodeList of its parent, sort out # the rest of the admin. @@ -1703,8 +1703,8 @@ def _register( raise AppError(f"Can't register {widget!r}; expected a Widget instance") if widget not in self._registry: self._register_child(parent, widget, before, after) - if widget.children: - self._register(widget, *widget.children) + if widget._nodes: + self._register(widget, *widget._nodes) apply_stylesheet(widget) if not self._running: @@ -1721,7 +1721,7 @@ def _unregister(self, widget: Widget) -> None: """ widget.reset_focus() if isinstance(widget._parent, Widget): - widget._parent.children._remove(widget) + widget._parent._nodes._remove(widget) widget._detach() self._registry.discard(widget) @@ -2104,7 +2104,7 @@ def _detach_from_dom(self, widgets: list[Widget]) -> list[Widget]: # snipping each affected branch from the DOM. for widget in pruned_remove: if widget.parent is not None: - widget.parent.children._remove(widget) + widget.parent._nodes._remove(widget) # Return the list of widgets that should end up being sent off in a # prune event. @@ -2123,10 +2123,10 @@ def _walk_children(self, root: Widget) -> Iterable[list[Widget]]: while stack: widget = pop() - children = [*widget.children, *widget._get_virtual_dom()] + children = [*widget._nodes, *widget._get_virtual_dom()] if children: yield children - for child in widget.children: + for child in widget._nodes: push(child) def _remove_nodes(self, widgets: list[Widget]) -> AwaitRemove: diff --git a/src/textual/dom.py b/src/textual/dom.py index 42478bf826..f83888d302 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -133,7 +133,7 @@ def __init__( check_identifiers("class name", *_classes) self._classes.update(_classes) - self.children: NodeList = NodeList() + self._nodes: NodeList = NodeList() self._css_styles: Styles = Styles(self) self._inline_styles: Styles = Styles(self) self.styles: RenderStyles = RenderStyles( @@ -152,9 +152,9 @@ def __init__( super().__init__() @property - def children_view(self) -> Sequence["Widget"]: + def children(self) -> Sequence["Widget"]: """A view on to the children.""" - return self.children + return self._nodes @property def auto_refresh(self) -> float | None: @@ -678,7 +678,7 @@ def displayed_children(self) -> list[Widget]: Children of this widget which will be displayed. """ - return [child for child in self.children if child.display] + return [child for child in self._nodes if child.display] def watch( self, @@ -721,7 +721,7 @@ def _add_child(self, node: Widget) -> None: Args: node: A DOM node. """ - self.children._append(node) + self._nodes._append(node) node._attach(self) def _add_children(self, *nodes: Widget) -> None: @@ -730,7 +730,7 @@ def _add_children(self, *nodes: Widget) -> None: Args: *nodes: Positional args should be new DOM nodes. """ - _append = self.children._append + _append = self._nodes._append for node in nodes: node._attach(self) _append(node) diff --git a/src/textual/walk.py b/src/textual/walk.py index 726af5e811..d43d40b064 100644 --- a/src/textual/walk.py +++ b/src/textual/walk.py @@ -53,7 +53,7 @@ def walk_depth_first( """ from textual.dom import DOMNode - stack: list[Iterator[DOMNode]] = [iter(root.children)] + stack: list[Iterator[DOMNode]] = [iter(root._nodes)] pop = stack.pop push = stack.append check_type = filter_type or DOMNode @@ -67,8 +67,8 @@ def walk_depth_first( else: if isinstance(node, check_type): yield node - if node.children_view: - push(iter(node.children_view)) + if node.children: + push(iter(node.children)) @overload @@ -122,9 +122,9 @@ def walk_breadth_first( if with_root and isinstance(root, check_type): yield root - extend(root.children_view) + extend(root.children) while queue: node = popleft() if isinstance(node, check_type): yield node - extend(node.children_view) + extend(node.children) diff --git a/src/textual/widget.py b/src/textual/widget.py index 197eb6147b..0842aff932 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -299,7 +299,7 @@ def siblings(self) -> list[Widget]: """ parent = self.parent if parent is not None: - siblings = list(parent.children) + siblings = list(parent._nodes) siblings.remove(self) return siblings else: @@ -390,7 +390,7 @@ def get_child_by_id( NoMatches: if no children could be found for this ID WrongType: if the wrong type was found. """ - child = self.children._get_by_id(id) + child = self._nodes._get_by_id(id) if child is None: raise NoMatches(f"No child found with id={id!r}") if expect_type is None: @@ -472,7 +472,7 @@ def _arrange(self, size: Size) -> DockArrangeResult: """ assert self.is_container - cache_key = (size, self.children._updates) + cache_key = (size, self._nodes._updates) if ( self._arrangement_cache_key == cache_key and self._cached_arrangement is not None @@ -481,7 +481,7 @@ def _arrange(self, size: Size) -> DockArrangeResult: self._arrangement_cache_key = cache_key arrangement = self._cached_arrangement = arrange( - self, self.children, size, self.screen.size + self, self._nodes, size, self.screen.size ) return arrangement @@ -549,7 +549,7 @@ def _find_mount_point(self, spot: int | str | "Widget") -> tuple["Widget", int]: # children. We should be able to go looking for the widget's # location amongst its parent's children. try: - return cast("Widget", spot.parent), spot.parent.children.index(spot) + return cast("Widget", spot.parent), spot.parent._nodes.index(spot) except ValueError: raise MountError(f"{spot!r} is not a child of {self!r}") from None @@ -687,7 +687,7 @@ def _to_widget(child: int | Widget, called: str) -> Widget: """Ensure a given child reference is a Widget.""" if isinstance(child, int): try: - child = self.children[child] + child = self._nodes[child] except IndexError: raise WidgetError( f"An index of {child} for the child to {called} is out of bounds" @@ -696,7 +696,7 @@ def _to_widget(child: int | Widget, called: str) -> Widget: # We got an actual widget, so let's be sure it really is one of # our children. try: - _ = self.children.index(child) + _ = self._nodes.index(child) except ValueError: raise WidgetError(f"{child!r} is not a child of {self!r}") from None return child @@ -709,11 +709,11 @@ def _to_widget(child: int | Widget, called: str) -> Widget: # child; where we're moving it to, which should be within the child # list; and how we're supposed to move it. All that's left is doing # the right thing. - self.children._remove(child) + self._nodes._remove(child) if before is not None: - self.children._insert(self.children.index(target), child) + self._nodes._insert(self._nodes.index(target), child) else: - self.children._insert(self.children.index(target) + 1, child) + self._nodes._insert(self._nodes.index(target) + 1, child) # Request a refresh. self.refresh(layout=True) @@ -1181,9 +1181,7 @@ def focusable_children(self) -> list[Widget]: List of widgets that can receive focus. """ - focusable = [ - child for child in self.children if child.display and child.visible - ] + focusable = [child for child in self._nodes if child.display and child.visible] return sorted(focusable, key=attrgetter("_focus_sort_key")) @property @@ -1277,7 +1275,7 @@ def is_container(self) -> bool: Returns: True if this widget is a container. """ - return self.styles.layout is not None or bool(self.children) + return self.styles.layout is not None or bool(self._nodes) @property def is_scrollable(self) -> bool: @@ -1286,7 +1284,7 @@ def is_scrollable(self) -> bool: Returns: True if this widget may be scrolled. """ - return self.styles.layout is not None or bool(self.children) + return self.styles.layout is not None or bool(self._nodes) @property def layer(self) -> str: diff --git a/src/textual/widgets/_list_view.py b/src/textual/widgets/_list_view.py index 3d9db58922..76c0fdf059 100644 --- a/src/textual/widgets/_list_view.py +++ b/src/textual/widgets/_list_view.py @@ -94,33 +94,33 @@ def highlighted_child(self) -> ListItem | None: """ if self.index is None: return None - elif 0 <= self.index < len(self.children): - return self.children[self.index] + elif 0 <= self.index < len(self._nodes): + return self._nodes[self.index] def validate_index(self, index: int | None) -> int | None: """Clamp the index to the valid range, or set to None if there's nothing to highlight.""" - if not self.children or index is None: + if not self._nodes or index is None: return None return self._clamp_index(index) def _clamp_index(self, index: int) -> int: """Clamp the index to a valid value given the current list of children""" - last_index = max(len(self.children) - 1, 0) + last_index = max(len(self._nodes) - 1, 0) return clamp(index, 0, last_index) def _is_valid_index(self, index: int | None) -> bool: """Return True if the current index is valid given the current list of children""" if index is None: return False - return 0 <= index < len(self.children) + return 0 <= index < len(self._nodes) def watch_index(self, old_index: int, new_index: int) -> None: """Updates the highlighting when the index changes.""" if self._is_valid_index(old_index): - old_child = self.children[old_index] + old_child = self._nodes[old_index] old_child.highlighted = False if self._is_valid_index(new_index): - new_child = self.children[new_index] + new_child = self._nodes[new_index] new_child.highlighted = True else: new_child = None @@ -166,7 +166,7 @@ def action_cursor_up(self) -> None: def on_list_item__child_clicked(self, event: ListItem._ChildClicked) -> None: self.focus() - self.index = self.children.index(event.sender) + self.index = self._nodes.index(event.sender) self.post_message_no_wait(self.Selected(self, event.sender)) def _scroll_highlighted_region(self) -> None: @@ -175,4 +175,4 @@ def _scroll_highlighted_region(self) -> None: self.scroll_to_widget(self.highlighted_child, animate=False) def __len__(self): - return len(self.children) + return len(self._nodes) diff --git a/tests/test_screens.py b/tests/test_screens.py index a92b0493f9..f88367e8c6 100644 --- a/tests/test_screens.py +++ b/tests/test_screens.py @@ -45,8 +45,8 @@ class ScreensApp(App): async def test_screens(): app = App() # There should be nothing in the children since the app hasn't run yet + assert not app._nodes assert not app.children - assert not app.children_view app._set_active() with pytest.raises(ScreenStackError): @@ -63,8 +63,8 @@ async def test_screens(): app.install_screen(screen2, "screen2") # Installing a screen does not add it to the DOM + assert not app._nodes assert not app.children - assert not app.children_view # Check they are installed assert app.is_screen_installed("screen1") @@ -91,21 +91,21 @@ async def test_screens(): # Check it is current assert app.screen is screen1 # There should be one item in the children view - assert app.children_view == (screen1,) + assert app.children == (screen1,) # Switch to another screen app.switch_screen("screen2") # Check it has changed the stack and that it is current assert app.screen_stack == [screen2] assert app.screen is screen2 - assert app.children_view == (screen2,) + assert app.children == (screen2,) # Push another screen app.push_screen("screen3") assert app.screen_stack == [screen2, screen3] assert app.screen is screen3 # Only the current screen is in children_view - assert app.children_view == (screen3,) + assert app.children == (screen3,) # Pop a screen assert app.pop_screen() is screen3 diff --git a/tests/test_unmount.py b/tests/test_unmount.py index 6ef23f247d..2301e4010a 100644 --- a/tests/test_unmount.py +++ b/tests/test_unmount.py @@ -13,7 +13,7 @@ async def test_unmount(): class UnmountWidget(Container): def on_unmount(self, event: events.Unmount): unmount_ids.append( - f"{self.__class__.__name__}#{self.id}-{self.parent is not None}-{len(self.children)}" + f"{self.__class__.__name__}#{self.id}-{self.parent is not None}-{len(self._nodes)}" ) class MyScreen(Screen): diff --git a/tests/test_widget_child_moving.py b/tests/test_widget_child_moving.py index d9e15de66a..9b99b3ae80 100644 --- a/tests/test_widget_child_moving.py +++ b/tests/test_widget_child_moving.py @@ -59,9 +59,9 @@ async def test_widget_move_child() -> None: container = Widget(*widgets) await pilot.app.mount(container) container.move_child(child, before=target) - assert container.children[0].id == "widget-1" - assert container.children[1].id == "widget-0" - assert container.children[2].id == "widget-2" + assert container._nodes[0].id == "widget-1" + assert container._nodes[1].id == "widget-0" + assert container._nodes[2].id == "widget-2" # Test the different permutations of moving one widget after another. perms = ((0, 1), (widgets[0], 1), (0, widgets[1]), (widgets[0], widgets[1])) @@ -70,22 +70,22 @@ async def test_widget_move_child() -> None: container = Widget(*widgets) await pilot.app.mount(container) container.move_child(child, after=target) - assert container.children[0].id == "widget-1" - assert container.children[1].id == "widget-0" - assert container.children[2].id == "widget-2" + assert container._nodes[0].id == "widget-1" + assert container._nodes[1].id == "widget-0" + assert container._nodes[2].id == "widget-2" # Test moving after a child after the last child. async with App().run_test() as pilot: container = Widget(*widgets) await pilot.app.mount(container) container.move_child(widgets[0], after=widgets[-1]) - assert container.children[0].id == "widget-1" - assert container.children[-1].id == "widget-0" + assert container._nodes[0].id == "widget-1" + assert container._nodes[-1].id == "widget-0" # Test moving after a child after the last child's numeric position. async with App().run_test() as pilot: container = Widget(*widgets) await pilot.app.mount(container) container.move_child(widgets[0], after=widgets[9]) - assert container.children[0].id == "widget-1" - assert container.children[-1].id == "widget-0" + assert container._nodes[0].id == "widget-1" + assert container._nodes[-1].id == "widget-0" diff --git a/tests/test_widget_mount_point.py b/tests/test_widget_mount_point.py index eebfdec5a7..57e070df62 100644 --- a/tests/test_widget_mount_point.py +++ b/tests/test_widget_mount_point.py @@ -23,7 +23,7 @@ def test_find_dom_spot(): # Just as a quick double-check, make sure the main components are in # their intended place. - assert list(screen.children) == [header, body, footer] + assert list(screen._nodes) == [header, body, footer] # Now check that we find what we're looking for in the places we expect # to find them. diff --git a/tests/test_widget_mounting.py b/tests/test_widget_mounting.py index 666dca5373..305fd7c827 100644 --- a/tests/test_widget_mounting.py +++ b/tests/test_widget_mounting.py @@ -26,72 +26,72 @@ async def test_mount_via_app() -> None: async with App().run_test() as pilot: # Mount the first one and make sure it's there. await pilot.app.mount(widgets[0]) - assert len(pilot.app.screen.children) == 1 - assert pilot.app.screen.children[0] == widgets[0] + assert len(pilot.app.screen._nodes) == 1 + assert pilot.app.screen._nodes[0] == widgets[0] # Mount the next 2 widgets via mount. await pilot.app.mount(*widgets[1:3]) - assert list(pilot.app.screen.children) == widgets[0:3] + assert list(pilot.app.screen._nodes) == widgets[0:3] # Finally mount the rest of the widgets via mount_all. await pilot.app.mount_all(widgets[3:]) - assert list(pilot.app.screen.children) == widgets + assert list(pilot.app.screen._nodes) == widgets async with App().run_test() as pilot: # Mount a widget before -1, which is "before the end". penultimate = Static(id="penultimate") await pilot.app.mount_all(widgets) await pilot.app.mount(penultimate, before=-1) - assert pilot.app.screen.children[-2] == penultimate + assert pilot.app.screen._nodes[-2] == penultimate async with App().run_test() as pilot: # Mount a widget after -1, which is "at the end". ultimate = Static(id="ultimate") await pilot.app.mount_all(widgets) await pilot.app.mount(ultimate, after=-1) - assert pilot.app.screen.children[-1] == ultimate + assert pilot.app.screen._nodes[-1] == ultimate async with App().run_test() as pilot: # Mount a widget before -2, which is "before the penultimate". penpenultimate = Static(id="penpenultimate") await pilot.app.mount_all(widgets) await pilot.app.mount(penpenultimate, before=-2) - assert pilot.app.screen.children[-3] == penpenultimate + assert pilot.app.screen._nodes[-3] == penpenultimate async with App().run_test() as pilot: # Mount a widget after -2, which is "before the end". penultimate = Static(id="penultimate") await pilot.app.mount_all(widgets) await pilot.app.mount(penultimate, after=-2) - assert pilot.app.screen.children[-2] == penultimate + assert pilot.app.screen._nodes[-2] == penultimate async with App().run_test() as pilot: # Mount a widget before 0, which is "at the start". start = Static(id="start") await pilot.app.mount_all(widgets) await pilot.app.mount(start, before=0) - assert pilot.app.screen.children[0] == start + assert pilot.app.screen._nodes[0] == start async with App().run_test() as pilot: # Mount a widget after 0. You get the idea... second = Static(id="second") await pilot.app.mount_all(widgets) await pilot.app.mount(second, after=0) - assert pilot.app.screen.children[1] == second + assert pilot.app.screen._nodes[1] == second async with App().run_test() as pilot: # Mount a widget relative to another via query. queue_jumper = Static(id="queue-jumper") await pilot.app.mount_all(widgets) await pilot.app.mount(queue_jumper, after="#starter-5") - assert pilot.app.screen.children[6] == queue_jumper + assert pilot.app.screen._nodes[6] == queue_jumper async with App().run_test() as pilot: # Mount a widget relative to another via query. queue_jumper = Static(id="queue-jumper") await pilot.app.mount_all(widgets) await pilot.app.mount(queue_jumper, after=widgets[5]) - assert pilot.app.screen.children[6] == queue_jumper + assert pilot.app.screen._nodes[6] == queue_jumper async with App().run_test() as pilot: # Make sure we get told off for trying to before and after. diff --git a/tests/test_widget_removing.py b/tests/test_widget_removing.py index 49b92e1913..6c648d04c7 100644 --- a/tests/test_widget_removing.py +++ b/tests/test_widget_removing.py @@ -13,28 +13,28 @@ async def test_remove_single_widget(): assert not widget.is_attached await pilot.app.mount(widget) assert widget.is_attached - assert len(pilot.app.screen.children) == 1 + assert len(pilot.app.screen._nodes) == 1 await pilot.app.query_one(Static).remove() assert not widget.is_attached - assert len(pilot.app.screen.children) == 0 + assert len(pilot.app.screen._nodes) == 0 async def test_many_remove_all_widgets(): """It should be possible to remove all widgets on a multi-widget screen.""" async with App().run_test() as pilot: await pilot.app.mount(*[Static() for _ in range(10)]) - assert len(pilot.app.screen.children) == 10 + assert len(pilot.app.screen._nodes) == 10 await pilot.app.query(Static).remove() - assert len(pilot.app.screen.children) == 0 + assert len(pilot.app.screen._nodes) == 0 async def test_many_remove_some_widgets(): """It should be possible to remove some widgets on a multi-widget screen.""" async with App().run_test() as pilot: await pilot.app.mount(*[Static(classes=f"is-{n%2}") for n in range(10)]) - assert len(pilot.app.screen.children) == 10 + assert len(pilot.app.screen._nodes) == 10 await pilot.app.query(".is-0").remove() - assert len(pilot.app.screen.children) == 5 + assert len(pilot.app.screen._nodes) == 5 async def test_remove_branch(): @@ -46,7 +46,7 @@ async def test_remove_branch(): Container(Container(Container(Container(Container(Static()))))), ) assert len(pilot.app.screen.walk_children(with_self=False)) == 13 - await pilot.app.screen.children[0].remove() + await pilot.app.screen._nodes[0].remove() assert len(pilot.app.screen.walk_children(with_self=False)) == 7 @@ -68,14 +68,14 @@ async def test_remove_move_focus(): async with App().run_test() as pilot: buttons = [Button(str(n)) for n in range(10)] await pilot.app.mount(Container(*buttons[:5]), Container(*buttons[5:])) - assert len(pilot.app.screen.children) == 2 + assert len(pilot.app.screen._nodes) == 2 assert len(pilot.app.screen.walk_children(with_self=False)) == 12 assert pilot.app.focused is None await pilot.press("tab") assert pilot.app.focused is not None assert pilot.app.focused == buttons[0] - await pilot.app.screen.children[0].remove() - assert len(pilot.app.screen.children) == 1 + await pilot.app.screen._nodes[0].remove() + assert len(pilot.app.screen._nodes) == 1 assert len(pilot.app.screen.walk_children(with_self=False)) == 6 assert pilot.app.focused is not None assert pilot.app.focused == buttons[9] @@ -95,7 +95,7 @@ def on_unmount(self, _): Removable(Removable(Removable(id="grandchild"), id="child"), id="parent") ) assert len(pilot.app.screen.walk_children(with_self=False)) == 3 - await pilot.app.screen.children[0].remove() + await pilot.app.screen._nodes[0].remove() assert len(pilot.app.screen.walk_children(with_self=False)) == 0 assert removals == ["grandchild", "child", "parent"] From c7e5ac14fb501c54fe9ae49f0fe986279b09e74a Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 13 Feb 2023 15:10:32 +0000 Subject: [PATCH 13/18] rename --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3712f71e71..6df3da39e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - `App.query` now only includes the current screen https://github.com/Textualize/textual/pull/1778 - `DOMNode.tree` now displays simple DOM structure only https://github.com/Textualize/textual/pull/1778 - `App.install_screen` now returns None rather than AwaitMount https://github.com/Textualize/textual/pull/1778 +- `DOMNode.children` is now a simple sequence, the NodesList is exposed as `DOMNode._nodes` https://github.com/Textualize/textual/pull/1778 ### Fixed From 4cdc1398539ce110b7565c86d9028953c0d1d37e Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 13 Feb 2023 15:12:02 +0000 Subject: [PATCH 14/18] name fix --- src/textual/dom.py | 6 +++--- tests/test_screens.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/textual/dom.py b/src/textual/dom.py index f83888d302..0d7ef4e784 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -515,7 +515,7 @@ def render_info(node: DOMNode) -> Pretty: tree = Tree(render_info(self)) def add_children(tree, node): - for child in node.children_view: + for child in node.children: info = render_info(child) branch = tree.add(info) if tree.children: @@ -557,7 +557,7 @@ def render_info(node: DOMNode) -> Columns: tree = Tree(render_info(self)) def add_children(tree, node): - for child in node.children_view: + for child in node.children: info = render_info(child) css = child.styles.css if css: @@ -571,7 +571,7 @@ def add_children(tree, node): ), ) branch = tree.add(info) - if tree.children_view: + if tree.children: add_children(branch, child) add_children(tree, self) diff --git a/tests/test_screens.py b/tests/test_screens.py index f88367e8c6..ca56df9c5b 100644 --- a/tests/test_screens.py +++ b/tests/test_screens.py @@ -104,7 +104,7 @@ async def test_screens(): app.push_screen("screen3") assert app.screen_stack == [screen2, screen3] assert app.screen is screen3 - # Only the current screen is in children_view + # Only the current screen is in children assert app.children == (screen3,) # Pop a screen From 2a8b099f330fe3ebd4a14d9e8b10f76fc2617ba4 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 13 Feb 2023 15:47:38 +0000 Subject: [PATCH 15/18] fix --- src/textual/widgets/_tree.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index 3786b080da..5117d10dc1 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -473,7 +473,7 @@ def __init__( text_label = self.process_label(label) self._updates = 0 - self._nodes: dict[NodeID, TreeNode[TreeDataType]] = {} + self._tree_nodes: dict[NodeID, TreeNode[TreeDataType]] = {} self._current_id = 0 self.root = self._add_node(None, text_label, data) @@ -515,7 +515,7 @@ def _add_node( expand: bool = False, ) -> TreeNode[TreeDataType]: node = TreeNode(self, parent, self._new_id(), label, data, expanded=expand) - self._nodes[node._id] = node + self._tree_nodes[node._id] = node self._updates += 1 return node @@ -630,7 +630,7 @@ def get_node_by_id(self, node_id: NodeID) -> TreeNode[TreeDataType]: Tree.UnknownID: Raised if the `TreeNode` ID is unknown. """ try: - return self._nodes[node_id] + return self._tree_nodes[node_id] except KeyError: raise self.UnknownNodeID(f"Unknown NodeID ({node_id}) in tree") from None From 497503fa3ab425cbbc56a53d2a6c41728e89596a Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 13 Feb 2023 17:14:52 +0000 Subject: [PATCH 16/18] docstrings --- src/textual/app.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/textual/app.py b/src/textual/app.py index 88030283c6..46a11d9bc0 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -1281,6 +1281,7 @@ def install_screen(self, screen: Screen, name: str) -> None: Args: screen: Screen to install. name: Unique name to identify the screen. + Raises: ScreenError: If the screen can't be installed. @@ -1301,7 +1302,10 @@ def uninstall_screen(self, screen: Screen | str) -> str | None: If the screen was not previously installed then this method is a null-op. Uninstalling a screen allows Textual to delete it when it is popped or switched. - Note that you + Note that uninstalling a screen is only required if you have previously installed it + with [install_screen][textual.app.App.install_screen]. + Textual will also uninstall screens automatically on exit. + Args: screen: The screen to uninstall or the name of a installed screen. From 6f1abe849f7694f68e8726493e41fa9ed11d94da Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 13 Feb 2023 17:30:53 +0000 Subject: [PATCH 17/18] fix reference --- 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 5ae8b14efd..9014f3d979 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" ``` -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. +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 From 2db2ae1b521a58b3f277aac2d81a7799c59641bc Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 14 Feb 2023 09:34:04 +0000 Subject: [PATCH 18/18] property docstring --- src/textual/dom.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/textual/dom.py b/src/textual/dom.py index 0d7ef4e784..74f87d073b 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -503,11 +503,7 @@ def visible(self, new_value: bool | str) -> None: @property def tree(self) -> Tree: - """Get a Rich tree object which will recursively render the structure of the node tree. - - Returns: - A Rich object which may be printed. - """ + """Get a Rich tree object which will recursively render the structure of the node tree.""" def render_info(node: DOMNode) -> Pretty: return Pretty(node) @@ -528,9 +524,6 @@ def add_children(tree, node): def css_tree(self) -> Tree: """Get a Rich tree object which will recursively render the structure of the node tree, which also displays CSS and size information. - - Returns: - A Rich object which may be printed. """ from rich.columns import Columns from rich.console import Group