simplify compose

This commit is contained in:
Will McGugan
2022-10-15 13:07:15 +01:00
parent babf5beefc
commit 8982d88f9e
8 changed files with 120 additions and 35 deletions

View File

@@ -3,8 +3,8 @@ from textual.widgets import Welcome
class WelcomeApp(App):
def on_key(self) -> None:
self.mount(Welcome())
async def on_key(self) -> None:
await self.mount(Welcome())
def on_button_pressed(self) -> None:
self.exit()

View File

@@ -183,7 +183,7 @@ Let's look at an example which looks up word definitions from an [api](https://d
=== "Output"
```{.textual path="docs/examples/events/dictionary.py" press="tab,t,e,x,t,_,_,_,_,_,_,_,_,_,_,_"}
```{.textual path="docs/examples/events/dictionary.py" press="t,e,x,t,_,_,_,_,_,_,_,_,_,_,_"}
```
Note the highlighted line in the above code which calls `asyncio.create_task` to run coroutine in the background. Without this you would find typing in to the text box to be unresponsive.

View File

@@ -70,7 +70,7 @@ Textual is a framework for building applications that run within your terminal.
```
```{.textual path="docs/examples/events/dictionary.py" columns="100" lines="30" press="tab,_,t,e,x,t,_,_,_,_,_,_,_,_,_,_,_,_,_"}
```{.textual path="docs/examples/events/dictionary.py" columns="100" lines="30" press="_,t,e,x,t,_,_,_,_,_,_,_,_,_,_,_,_,_"}
```

View File

@@ -23,6 +23,11 @@ class DictionaryApp(App):
yield Input(placeholder="Search for a word")
yield Content(Static(id="results"), id="results-container")
def on_mount(self) -> None:
"""Called when app starts."""
# Give the input focus, so we can start typing straight away
self.query_one(Input).focus()
async def on_input_changed(self, message: Input.Changed) -> None:
"""A coroutine to handle a text changed message."""
if message.value:

35
sandbox/will/mount.py Normal file
View File

@@ -0,0 +1,35 @@
from textual.app import App, ComposeResult
from textual.containers import Container
from textual.widget import Widget
from textual.widgets import Static
class MountWidget(Widget):
def on_mount(self) -> None:
print("Widget mounted")
class MountContainer(Container):
def compose(self) -> ComposeResult:
yield Container(MountWidget(id="bar"))
def on_mount(self) -> None:
bar = self.query_one("#bar")
print("MountContainer got", bar)
class MountApp(App):
def compose(self) -> ComposeResult:
yield MountContainer(id="foo")
def on_mount(self) -> None:
foo = self.query_one("#foo")
print("foo is", foo)
static = self.query_one("#bar")
print("App got", static)
if __name__ == "__main__":
app = MountApp()
app.run()

View File

@@ -47,7 +47,7 @@ from .messages import CallbackType
from .reactive import Reactive
from .renderables.blank import Blank
from .screen import Screen
from .widget import Widget, _wait_for_mount
from .widget import AwaitMount, Widget
PLATFORM = platform.system()
WINDOWS = PLATFORM == "Windows"
@@ -696,15 +696,15 @@ class App(Generic[ReturnType], DOMNode):
self._require_stylesheet_update.add(self.screen if node is None else node)
self.check_idle()
def mount(self, *anon_widgets: Widget, **widgets: Widget) -> list[Widget]:
def mount(self, *anon_widgets: Widget, **widgets: Widget) -> AwaitMount:
"""Mount widgets. Widgets specified as positional args, or keywords args. If supplied
as keyword args they will be assigned an id of the key.
"""
mounted_widgets = self._register(self.screen, *anon_widgets, **widgets)
return mounted_widgets
return AwaitMount(mounted_widgets)
def mount_all(self, widgets: Iterable[Widget]) -> list[Widget]:
def mount_all(self, widgets: Iterable[Widget]) -> AwaitMount:
"""Mount widgets from an iterable.
Args:
@@ -713,7 +713,7 @@ class App(Generic[ReturnType], DOMNode):
mounted_widgets = list(widgets)
for widget in mounted_widgets:
self._register(self.screen, widget)
return mounted_widgets
return AwaitMount(mounted_widgets)
def is_screen_installed(self, screen: Screen | str) -> bool:
"""Check if a given screen has been installed.
@@ -1011,14 +1011,13 @@ class App(Generic[ReturnType], DOMNode):
self.set_interval(0.25, self.css_monitor, name="css monitor")
self.log.system("[b green]STARTED[/]", self.css_monitor)
process_messages = super()._process_messages
async def run_process_messages():
compose_event = events.Compose(sender=self)
await self._dispatch_message(compose_event)
mount_event = events.Mount(sender=self)
await self._dispatch_message(mount_event)
try:
await self._dispatch_message(events.Compose(sender=self))
await self._dispatch_message(events.Mount(sender=self))
finally:
self._mounted_event.set()
Reactive._initialize_object(self)
@@ -1030,7 +1029,18 @@ class App(Generic[ReturnType], DOMNode):
await self._ready()
if ready_callback is not None:
await ready_callback()
await process_messages()
self._running = True
try:
await self._process_messages_loop()
except asyncio.CancelledError:
pass
finally:
self._running = False
for timer in list(self._timers):
await timer.stop()
await self.animator.stop()
await self._close_all()
@@ -1065,6 +1075,9 @@ class App(Generic[ReturnType], DOMNode):
if self.devtools.is_connected:
await self._disconnect_devtools()
async def _pre_process(self) -> None:
pass
async def _ready(self) -> None:
"""Called immediately prior to processing messages.
@@ -1091,8 +1104,7 @@ class App(Generic[ReturnType], DOMNode):
async def _on_compose(self) -> None:
widgets = list(self.compose())
self.mount_all(widgets)
await _wait_for_mount(widgets)
await self.mount_all(widgets)
def _on_idle(self) -> None:
"""Perform actions when there are no messages in the queue."""

View File

@@ -286,10 +286,12 @@ class MessagePump(metaclass=MessagePumpMeta):
def _start_messages(self) -> None:
"""Start messages task."""
self._task = asyncio.create_task(self._process_messages())
self.post_message_no_wait(events.Compose(sender=self))
async def _process_messages(self) -> None:
self._running = True
await self._pre_process()
try:
await self._process_messages_loop()
except CancelledError:
@@ -299,6 +301,18 @@ class MessagePump(metaclass=MessagePumpMeta):
for timer in list(self._timers):
await timer.stop()
async def _pre_process(self) -> None:
"""Procedure to run before processing messages."""
# Dispatch compose and mount messages without going through loop
# These events must occur in this order, and at the start.
try:
await self._dispatch_message(events.Compose(sender=self))
await self._dispatch_message(events.Mount(sender=self))
finally:
# This is critical, mount may be waiting
self._mounted_event.set()
Reactive._initialize_object(self)
async def _process_messages_loop(self) -> None:
"""Process messages until the queue is closed."""
_rich_traceback_guard = True
@@ -326,9 +340,6 @@ class MessagePump(metaclass=MessagePumpMeta):
except MessagePumpClosed:
break
is_mount = isinstance(message, events.Mount)
if is_mount:
Reactive._initialize_object(self)
try:
await self._dispatch_message(message)
except CancelledError:
@@ -338,8 +349,6 @@ class MessagePump(metaclass=MessagePumpMeta):
self.app._handle_exception(error)
break
finally:
if is_mount:
self._mounted_event.set()
self._message_queue.task_done()
current_time = time()

View File

@@ -4,7 +4,17 @@ from asyncio import Lock, wait, create_task
from fractions import Fraction
from itertools import islice
from operator import attrgetter
from typing import TYPE_CHECKING, ClassVar, Collection, Iterable, NamedTuple, cast
from typing import (
Awaitable,
Generator,
TYPE_CHECKING,
ClassVar,
Collection,
Iterable,
NamedTuple,
Sequence,
cast,
)
import rich.repr
from rich.console import (
@@ -59,11 +69,26 @@ _JUSTIFY_MAP: dict[str, JustifyMethod] = {
}
async def _wait_for_mount(widgets: list[Widget]) -> None:
"""Wait for widget to be mounted."""
aws = [create_task(widget._mounted_event.wait()) for widget in widgets]
if aws:
await wait(aws)
class AwaitMount:
"""An awaitable returned by mount().
Example:
await self.mount(Static("foo"))
"""
def __init__(self, widgets: Sequence[Widget]) -> None:
self._widgets = widgets
def __await__(self) -> Generator[None, None, None]:
async def await_mount() -> None:
aws = [
create_task(widget._mounted_event.wait()) for widget in self._widgets
]
if aws:
await wait(aws)
return await_mount().__await__()
class _Styled:
@@ -323,7 +348,7 @@ class Widget(DOMNode):
"""Clear arrangement cache, forcing a new arrange operation."""
self._arrangement = None
def mount(self, *anon_widgets: Widget, **widgets: Widget) -> None:
def mount(self, *anon_widgets: Widget, **widgets: Widget) -> AwaitMount:
"""Mount child widgets (making this widget a container).
Widgets may be passed as positional arguments or keyword arguments. If keyword arguments,
@@ -335,7 +360,8 @@ class Widget(DOMNode):
```
"""
self.app._register(self, *anon_widgets, **widgets)
mounted_widgets = self.app._register(self, *anon_widgets, **widgets)
return AwaitMount(mounted_widgets)
# self.app.screen.refresh(layout=True)
# self.refresh(layout=True)
@@ -1878,9 +1904,7 @@ class Widget(DOMNode):
async def _on_compose(self, event: events.Compose) -> None:
widgets = list(self.compose())
self.mount(*widgets)
await _wait_for_mount(widgets)
await self.post_message(events.Mount(self))
await self.mount(*widgets)
def _on_mount(self, event: events.Mount) -> None:
if self.styles.overflow_y == "scroll":