From fe2043eca04fa3c2d23e0385edd1b65b3cb0c859 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 23 May 2024 16:51:44 +0100 Subject: [PATCH 1/3] enforce mounting --- src/textual/app.py | 5 +++++ src/textual/message_pump.py | 15 +++++++++++++++ src/textual/widget.py | 8 +++++--- tests/test_widget_mounting.py | 7 +++++++ 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/textual/app.py b/src/textual/app.py index 58fd5b2ee2..58cdaad074 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -769,6 +769,11 @@ async def stop_animation(self, attribute: str, complete: bool = True) -> None: """ await self._animator.stop_animation(self, attribute, complete) + @property + def is_dom_root(self) -> bool: + """Is this a root node (i.e. the App)?""" + return True + @property def debug(self) -> bool: """Is debug mode enabled?""" diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index a3a0293fd1..807fd2d35f 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -215,6 +215,11 @@ def message_queue_size(self) -> int: """The current size of the message queue.""" return self._message_queue.qsize() + @property + def is_dom_root(self): + """Is this a root node (i.e. the App)?""" + return False + @property def app(self) -> "App[object]": """ @@ -239,6 +244,16 @@ def app(self) -> "App[object]": active_app.set(node) return node + @property + def _is_linked_to_app(self) -> bool: + """Is this node linked to the app through the DOM?""" + node: MessagePump | None = self + + while (node := node._parent) is not None: + if node.is_dom_root: + return True + return False + @property def is_parent_active(self) -> bool: """Is the parent active?""" diff --git a/src/textual/widget.py b/src/textual/widget.py index bc06c39258..51a892edba 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -934,6 +934,8 @@ def mount( Only one of ``before`` or ``after`` can be provided. If both are provided a ``MountError`` will be raised. """ + if not self._is_linked_to_app: + raise MountError(f"Can't mount widget(s) before {self!r} is mounted") # Check for duplicate IDs in the incoming widgets ids_to_mount = [widget.id for widget in widgets if widget.id is not None] unique_ids = set(ids_to_mount) @@ -1121,9 +1123,9 @@ async def recompose(self) -> None: Recomposing will remove children and call `self.compose` again to remount. """ - if self._parent is not None: - async with self.batch(): - await self.query("*").exclude(".-textual-system").remove() + async with self.batch(): + await self.query("*").exclude(".-textual-system").remove() + if self._is_linked_to_app: await self.mount_all(compose(self)) def _post_register(self, app: App) -> None: diff --git a/tests/test_widget_mounting.py b/tests/test_widget_mounting.py index 305fd7c827..086e448684 100644 --- a/tests/test_widget_mounting.py +++ b/tests/test_widget_mounting.py @@ -114,3 +114,10 @@ async def test_mount_via_app() -> None: await pilot.app.mount_all(widgets) with pytest.raises(TooManyMatches): await pilot.app.mount(Static(), before="Static") + + +def test_mount_error() -> None: + """Mounting a widget on an un-mounted widget should raise an error.""" + with pytest.raises(MountError): + widget = Widget() + widget.mount(Static()) From 4e83677e769bf0f5f2fca49617031bc0c163ba5f Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 23 May 2024 16:59:35 +0100 Subject: [PATCH 2/3] changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f918509620..be8288bb1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fixed `Footer` grid size https://github.com/Textualize/textual/pull/4545 +### Changed + +- Attempting to mount on a non-mounted widget now raises a MountError https://github.com/Textualize/textual/pull/4547 + ## [0.63.2] - 2024-05-23 ### Fixed From c7c9a6ec67fabaca2e73256703e1d9be25e8975a Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 23 May 2024 17:02:11 +0100 Subject: [PATCH 3/3] fix check --- 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 51a892edba..3c474f3ce2 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -1123,10 +1123,11 @@ async def recompose(self) -> None: Recomposing will remove children and call `self.compose` again to remount. """ - async with self.batch(): - await self.query("*").exclude(".-textual-system").remove() - if self._is_linked_to_app: - await self.mount_all(compose(self)) + if self._parent is not None: + async with self.batch(): + await self.query("*").exclude(".-textual-system").remove() + if self._is_linked_to_app: + await self.mount_all(compose(self)) def _post_register(self, app: App) -> None: """Called when the instance is registered.