wait for screen (#2584)

* wait for screen

* comments and changelog

* wait for screen after keys

* extra wait for animation

* comment

* comment

* docstring
This commit is contained in:
Will McGugan
2023-05-16 21:06:09 +01:00
committed by GitHub
parent 83e4be77db
commit abb7705ed0
3 changed files with 56 additions and 5 deletions

View File

@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- App `title` and `sub_title` attributes can be set to any type https://github.com/Textualize/textual/issues/2521 - App `title` and `sub_title` attributes can be set to any type https://github.com/Textualize/textual/issues/2521
- Using `Widget.move_child` where the target and the child being moved are the same is now a no-op https://github.com/Textualize/textual/issues/1743 - Using `Widget.move_child` where the target and the child being moved are the same is now a no-op https://github.com/Textualize/textual/issues/1743
- Calling `dismiss` on a screen that is not at the top of the stack now raises an exception https://github.com/Textualize/textual/issues/2575 - Calling `dismiss` on a screen that is not at the top of the stack now raises an exception https://github.com/Textualize/textual/issues/2575
- `MessagePump.call_after_refresh` and `MessagePump.call_later` will not return `False` if the callback could not be scheduled. https://github.com/Textualize/textual/pull/2584
### Fixed ### Fixed

View File

@@ -349,20 +349,25 @@ class MessagePump(metaclass=_MessagePumpMeta):
self._timers.add(timer) self._timers.add(timer)
return timer return timer
def call_after_refresh(self, callback: Callable, *args: Any, **kwargs: Any) -> None: def call_after_refresh(self, callback: Callable, *args: Any, **kwargs: Any) -> bool:
"""Schedule a callback to run after all messages are processed and the screen """Schedule a callback to run after all messages are processed and the screen
has been refreshed. Positional and keyword arguments are passed to the callable. has been refreshed. Positional and keyword arguments are passed to the callable.
Args: Args:
callback: A callable. callback: A callable.
Returns:
`True` if the callback was scheduled, or `False` if the callback could not be
scheduled (may occur if the message pump was closed or closing).
""" """
# We send the InvokeLater message to ourselves first, to ensure we've cleared # We send the InvokeLater message to ourselves first, to ensure we've cleared
# out anything already pending in our own queue. # out anything already pending in our own queue.
message = messages.InvokeLater(partial(callback, *args, **kwargs)) message = messages.InvokeLater(partial(callback, *args, **kwargs))
self.post_message(message) return self.post_message(message)
def call_later(self, callback: Callable, *args: Any, **kwargs: Any) -> None: def call_later(self, callback: Callable, *args: Any, **kwargs: Any) -> bool:
"""Schedule a callback to run after all messages are processed in this object. """Schedule a callback to run after all messages are processed in this object.
Positional and keywords arguments are passed to the callable. Positional and keywords arguments are passed to the callable.
@@ -370,9 +375,14 @@ class MessagePump(metaclass=_MessagePumpMeta):
callback: Callable to call next. callback: Callable to call next.
*args: Positional arguments to pass to the callable. *args: Positional arguments to pass to the callable.
**kwargs: Keyword arguments to pass to the callable. **kwargs: Keyword arguments to pass to the callable.
Returns:
`True` if the callback was scheduled, or `False` if the callback could not be
scheduled (may occur if the message pump was closed or closing).
""" """
message = events.Callback(callback=partial(callback, *args, **kwargs)) message = events.Callback(callback=partial(callback, *args, **kwargs))
self.post_message(message) return self.post_message(message)
def call_next(self, callback: Callable, *args: Any, **kwargs: Any) -> None: def call_next(self, callback: Callable, *args: Any, **kwargs: Any) -> None:
"""Schedule a callback to run immediately after processing the current message. """Schedule a callback to run immediately after processing the current message.

View File

@@ -65,6 +65,7 @@ class Pilot(Generic[ReturnType]):
""" """
if keys: if keys:
await self._app._press_keys(keys) await self._app._press_keys(keys)
await self._wait_for_screen()
async def click( async def click(
self, self,
@@ -132,13 +133,49 @@ class Pilot(Generic[ReturnType]):
app.post_message(MouseMove(**message_arguments)) app.post_message(MouseMove(**message_arguments))
await self.pause() await self.pause()
async def _wait_for_screen(self, timeout: float = 30.0) -> bool:
"""Wait for the current screen to have processed all pending events.
Args:
timeout: A timeout in seconds to wait.
Returns:
`True` if all events were processed, or `False` if the wait timed out.
"""
children = [self.app, *self.app.screen.walk_children(with_self=True)]
count = 0
count_zero_event = asyncio.Event()
def decrement_counter() -> None:
"""Decrement internal counter, and set an event if it reaches zero."""
nonlocal count
count -= 1
if count == 0:
# When count is zero, all messages queued at the start of the method have been processed
count_zero_event.set()
# Increase the count for every successful call_later
for child in children:
if child.call_later(decrement_counter):
count += 1
if count:
# Wait for the count to return to zero, or a timeout
try:
await asyncio.wait_for(count_zero_event.wait(), timeout=timeout)
except asyncio.TimeoutError:
return False
return True
async def pause(self, delay: float | None = None) -> None: async def pause(self, delay: float | None = None) -> None:
"""Insert a pause. """Insert a pause.
Args: Args:
delay: Seconds to pause, or None to wait for cpu idle. delay: Seconds to pause, or None to wait for cpu idle.
""" """
# These sleep zeros, are to force asyncio to give up a time-slice, # These sleep zeros, are to force asyncio to give up a time-slice.
await self._wait_for_screen()
if delay is None: if delay is None:
await wait_for_idle(0) await wait_for_idle(0)
else: else:
@@ -152,7 +189,9 @@ class Pilot(Generic[ReturnType]):
async def wait_for_scheduled_animations(self) -> None: async def wait_for_scheduled_animations(self) -> None:
"""Wait for any current and scheduled animations to complete.""" """Wait for any current and scheduled animations to complete."""
await self._wait_for_screen()
await self._app.animator.wait_until_complete() await self._app.animator.wait_until_complete()
await self._wait_for_screen()
await wait_for_idle() await wait_for_idle()
self.app.screen._on_timer_update() self.app.screen._on_timer_update()
@@ -162,5 +201,6 @@ class Pilot(Generic[ReturnType]):
Args: Args:
result: The app result returned by `run` or `run_async`. result: The app result returned by `run` or `run_async`.
""" """
await self._wait_for_screen()
await wait_for_idle() await wait_for_idle()
self.app.exit(result) self.app.exit(result)