Compositor ignores non-mounted widgets.

This, in turn, ensures widgets are not rendered before they are mounted.
This commit is contained in:
Rodrigo Girão Serrão
2023-11-21 14:23:09 +00:00
parent 67dd75ae64
commit a000994b2f
6 changed files with 42 additions and 1 deletions

View File

@@ -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

View File

@@ -573,6 +573,9 @@ class Compositor:
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:

View File

@@ -2202,6 +2202,7 @@ class App(Generic[ReturnType], DOMNode):
self.check_idle()
finally:
self._mounted_event.set()
self._is_mounted = True
Reactive._initialize_object(self)

View File

@@ -122,6 +122,13 @@ class MessagePump(metaclass=_MessagePumpMeta):
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 @@ class MessagePump(metaclass=_MessagePumpMeta):
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 @@ class MessagePump(metaclass=_MessagePumpMeta):
raise
except Exception as error:
self._mounted_event.set()
self._is_mounted = True
self.app._handle_exception(error)
break
finally:

View File

@@ -396,7 +396,7 @@ class Widget(DOMNode):
@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]:

27
tests/test_mount.py Normal file
View File

@@ -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()