From a000994b2f09f41b35184ad0d1dd370be00c563c 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: Tue, 21 Nov 2023 14:23:09 +0000 Subject: [PATCH] Compositor ignores non-mounted widgets. This, in turn, ensures widgets are not rendered before they are mounted. --- CHANGELOG.md | 1 + src/textual/_compositor.py | 3 +++ src/textual/app.py | 1 + src/textual/message_pump.py | 9 +++++++++ src/textual/widget.py | 2 +- tests/test_mount.py | 27 +++++++++++++++++++++++++++ 6 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 tests/test_mount.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 980abbc52e..3e1da5c856 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Off-by-one in CSS error reporting https://github.com/Textualize/textual/issues/3625 - Loading indicators and app notifications overlapped in the wrong order https://github.com/Textualize/textual/issues/3677 - Widgets being loaded are disabled and have their scrolling explicitly disabled too https://github.com/Textualize/textual/issues/3677 +- Method render on a widget could be called before mounting said widget https://github.com/Textualize/textual/issues/2914 ### Added diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index ddfba87806..65bf98bd36 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -573,6 +573,9 @@ def add_widget( visible: Whether the widget should be visible by default. This may be overridden by the CSS rule `visibility`. """ + if not widget._is_mounted: + return + styles = widget.styles visibility = styles.get_rule("visibility") if visibility is not None: diff --git a/src/textual/app.py b/src/textual/app.py index 2ef5ad2cb0..630e7753ed 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -2202,6 +2202,7 @@ async def invoke_ready_callback() -> None: self.check_idle() finally: self._mounted_event.set() + self._is_mounted = True Reactive._initialize_object(self) diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 33c80fa807..018b009e6c 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -122,6 +122,13 @@ def __init__(self, parent: MessagePump | None = None) -> None: self._last_idle: float = time() self._max_idle: float | None = None self._mounted_event = asyncio.Event() + self._is_mounted = False + """Having this explicit Boolean is an optimization. + + The same information could be retrieved from `self._mounted_event.is_set()`, but + we need to access this frequently in the compositor and the attribute with the + explicit Boolean value is faster than the two lookups and the function call. + """ self._next_callbacks: list[events.Callback] = [] self._thread_id: int = threading.get_ident() @@ -508,6 +515,7 @@ async def _pre_process(self) -> bool: finally: # This is critical, mount may be waiting self._mounted_event.set() + self._is_mounted = True return True def _post_mount(self): @@ -547,6 +555,7 @@ async def _process_messages_loop(self) -> None: raise except Exception as error: self._mounted_event.set() + self._is_mounted = True self.app._handle_exception(error) break finally: diff --git a/src/textual/widget.py b/src/textual/widget.py index b351a60773..f786cc5970 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -396,7 +396,7 @@ def __init__( @property def is_mounted(self) -> bool: """Check if this widget is mounted.""" - return self._mounted_event.is_set() + return self._is_mounted @property def siblings(self) -> list[Widget]: diff --git a/tests/test_mount.py b/tests/test_mount.py new file mode 100644 index 0000000000..23567d2d06 --- /dev/null +++ b/tests/test_mount.py @@ -0,0 +1,27 @@ +"""Regression test for https://github.com/Textualize/textual/issues/2914 + +Make sure that calls to render only happen after a widget being mounted. +""" + +import asyncio + +from textual.app import App +from textual.widget import Widget + + +class W(Widget): + def render(self): + return self.renderable + + async def on_mount(self): + await asyncio.sleep(0.1) + self.renderable = "1234" + + +async def test_render_only_after_mount(): + """Regression test for https://github.com/Textualize/textual/issues/2914""" + app = App() + async with app.run_test() as pilot: + app.mount(W()) + app.mount(W()) + await pilot.pause()