From ebce1a8da744be262aa0fa4b347c062083bc59ad Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 16 May 2023 19:53:24 +0100 Subject: [PATCH 1/7] wait for screen --- CHANGELOG.md | 1 + src/textual/message_pump.py | 18 ++++++++++++++---- src/textual/pilot.py | 36 +++++++++++++++++++++++++++++++++++- 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bce29efdac..08488a2bcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 - 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 +- `MessagePump.call_after_refresh` and `MessagePump.call_later` will not return `False` if the callback could not be scheduled. ### Fixed diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 7439919ce3..171e3e710a 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -349,20 +349,25 @@ def set_interval( self._timers.add(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 has been refreshed. Positional and keyword arguments are passed to the callable. Args: callback: A callable. + + Returns: + `True` if the callback was scheduled, or `False` if the callback could not be + schedule (may occur if the message pump was closed). + """ # We send the InvokeLater message to ourselves first, to ensure we've cleared # out anything already pending in our own queue. 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. Positional and keywords arguments are passed to the callable. @@ -370,9 +375,14 @@ def call_later(self, callback: Callable, *args: Any, **kwargs: Any) -> None: callback: Callable to call next. *args: Positional 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 + schedule (may occur if the message pump was closed). + """ 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: """Schedule a callback to run immediately after processing the current message. diff --git a/src/textual/pilot.py b/src/textual/pilot.py index eaab423340..9d28fec118 100644 --- a/src/textual/pilot.py +++ b/src/textual/pilot.py @@ -132,13 +132,45 @@ async def hover( app.post_message(MouseMove(**message_arguments)) await self.pause() + async def _wait_for_screen(self, timeout: float = 10.0) -> bool: + """Wait for the current screen to have processed all current 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: + count_zero_event.set() + + for child in children: + if child.call_later(decrement_counter): + count += 1 + + 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: """Insert a pause. Args: 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: await wait_for_idle(0) else: @@ -153,6 +185,7 @@ async def wait_for_animation(self) -> None: async def wait_for_scheduled_animations(self) -> None: """Wait for any current and scheduled animations to complete.""" await self._app.animator.wait_until_complete() + await self._wait_for_screen() await wait_for_idle() self.app.screen._on_timer_update() @@ -162,5 +195,6 @@ async def exit(self, result: ReturnType) -> None: Args: result: The app result returned by `run` or `run_async`. """ + await self._wait_for_screen() await wait_for_idle() self.app.exit(result) From 2f71bd17322826a34ed028c39243b9d2e992bfeb Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 16 May 2023 20:01:48 +0100 Subject: [PATCH 2/7] comments and changelog --- CHANGELOG.md | 2 +- src/textual/message_pump.py | 4 ++-- src/textual/pilot.py | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08488a2bcc..145e578a2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +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 - 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 -- `MessagePump.call_after_refresh` and `MessagePump.call_later` will not return `False` if the callback could not be scheduled. +- `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 diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 171e3e710a..281ff0baf7 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -358,7 +358,7 @@ def call_after_refresh(self, callback: Callable, *args: Any, **kwargs: Any) -> b Returns: `True` if the callback was scheduled, or `False` if the callback could not be - schedule (may occur if the message pump was closed). + schedule (may occur if the message pump was closed or closing). """ # We send the InvokeLater message to ourselves first, to ensure we've cleared @@ -378,7 +378,7 @@ def call_later(self, callback: Callable, *args: Any, **kwargs: Any) -> bool: Returns: `True` if the callback was scheduled, or `False` if the callback could not be - schedule (may occur if the message pump was closed). + schedule (may occur if the message pump was closed or closing). """ message = events.Callback(callback=partial(callback, *args, **kwargs)) diff --git a/src/textual/pilot.py b/src/textual/pilot.py index 9d28fec118..d4b1bed370 100644 --- a/src/textual/pilot.py +++ b/src/textual/pilot.py @@ -152,10 +152,12 @@ def decrement_counter() -> None: if count == 0: count_zero_event.set() + # Increase the count for every successful call later for child in children: if child.call_later(decrement_counter): count += 1 + # 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: From d15b8861ffdc7c72ddb87df20335844d8db194e4 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 16 May 2023 20:06:29 +0100 Subject: [PATCH 3/7] wait for screen after keys --- src/textual/pilot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/textual/pilot.py b/src/textual/pilot.py index d4b1bed370..9b5e9b36cd 100644 --- a/src/textual/pilot.py +++ b/src/textual/pilot.py @@ -65,6 +65,7 @@ async def press(self, *keys: str) -> None: """ if keys: await self._app._press_keys(keys) + await self._wait_for_screen() async def click( self, From f36d5cb283b50a574320c5a0041a3d163d8e9a6a Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 16 May 2023 20:14:49 +0100 Subject: [PATCH 4/7] extra wait for animation --- src/textual/message_pump.py | 4 ++-- src/textual/pilot.py | 14 ++++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 281ff0baf7..a4dfc82560 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -358,7 +358,7 @@ def call_after_refresh(self, callback: Callable, *args: Any, **kwargs: Any) -> b Returns: `True` if the callback was scheduled, or `False` if the callback could not be - schedule (may occur if the message pump was closed or closing). + scheduled (may occur if the message pump was closed or closing). """ # We send the InvokeLater message to ourselves first, to ensure we've cleared @@ -378,7 +378,7 @@ def call_later(self, callback: Callable, *args: Any, **kwargs: Any) -> bool: Returns: `True` if the callback was scheduled, or `False` if the callback could not be - schedule (may occur if the message pump was closed or closing). + scheduled (may occur if the message pump was closed or closing). """ message = events.Callback(callback=partial(callback, *args, **kwargs)) diff --git a/src/textual/pilot.py b/src/textual/pilot.py index 9b5e9b36cd..623ce410e2 100644 --- a/src/textual/pilot.py +++ b/src/textual/pilot.py @@ -133,7 +133,7 @@ async def hover( app.post_message(MouseMove(**message_arguments)) await self.pause() - async def _wait_for_screen(self, timeout: float = 10.0) -> bool: + async def _wait_for_screen(self, timeout: float = 30.0) -> bool: """Wait for the current screen to have processed all current events. Args: @@ -158,11 +158,12 @@ def decrement_counter() -> None: if child.call_later(decrement_counter): count += 1 - # 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 + 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 @@ -187,6 +188,7 @@ async def wait_for_animation(self) -> None: async def wait_for_scheduled_animations(self) -> None: """Wait for any current and scheduled animations to complete.""" + await self._wait_for_screen() await self._app.animator.wait_until_complete() await self._wait_for_screen() await wait_for_idle() From 9146888b23492d2593a459bbe92448f778be05a5 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 16 May 2023 20:31:59 +0100 Subject: [PATCH 5/7] comment --- src/textual/pilot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/pilot.py b/src/textual/pilot.py index 623ce410e2..02731375f9 100644 --- a/src/textual/pilot.py +++ b/src/textual/pilot.py @@ -153,7 +153,7 @@ def decrement_counter() -> None: if count == 0: count_zero_event.set() - # Increase the count for every successful call later + # Increase the count for every successful call_later for child in children: if child.call_later(decrement_counter): count += 1 From fca6223188ce55e9d590c2c000f03ecab49e9b37 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 16 May 2023 20:39:53 +0100 Subject: [PATCH 6/7] comment --- src/textual/pilot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/textual/pilot.py b/src/textual/pilot.py index 02731375f9..bf64c20759 100644 --- a/src/textual/pilot.py +++ b/src/textual/pilot.py @@ -151,6 +151,7 @@ def decrement_counter() -> None: 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 From a41d5b2f5d37b404594a61b000e5d935f2a11334 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 16 May 2023 20:45:59 +0100 Subject: [PATCH 7/7] docstring --- src/textual/pilot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/pilot.py b/src/textual/pilot.py index bf64c20759..041e00e13e 100644 --- a/src/textual/pilot.py +++ b/src/textual/pilot.py @@ -134,7 +134,7 @@ async def hover( await self.pause() async def _wait_for_screen(self, timeout: float = 30.0) -> bool: - """Wait for the current screen to have processed all current events. + """Wait for the current screen to have processed all pending events. Args: timeout: A timeout in seconds to wait.