Skip to content

Commit 9639449

Browse files
authored
Preserve original sender when using call_after_refresh (#2806)
* Defer sender in call_after_refresh * docstring * test * typing
1 parent 038cdb2 commit 9639449

File tree

5 files changed

+29
-9
lines changed

5 files changed

+29
-9
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
2222

2323
- Tooltips are now inherited, so will work with compound widgets
2424

25+
### Fixed
26+
27+
- call_after_refresh will preserve the sender within the callback https://github.com/Textualize/textual/pull/2806
28+
2529
## [0.28.0] - 2023-06-19
2630

2731
### Added

src/textual/message.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def __init__(self) -> None:
4949

5050
def __post_init__(self) -> None:
5151
"""Allow dataclasses to initialize the object."""
52-
self._sender: MessageTarget | None = active_message_pump.get(None)
52+
self._sender: MessagePump | None = active_message_pump.get(None)
5353
self.time: float = _time.get_time()
5454
self._forwarded = False
5555
self._no_default_action = False
@@ -91,7 +91,7 @@ def _set_forwarded(self) -> None:
9191
"""Mark this event as being forwarded."""
9292
self._forwarded = True
9393

94-
def _set_sender(self, sender: MessageTarget) -> None:
94+
def _set_sender(self, sender: MessagePump) -> None:
9595
"""Set the sender."""
9696
self._sender = sender
9797

src/textual/message_pump.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,9 @@ def call_next(self, callback: Callback, *args: Any, **kwargs: Any) -> None:
422422

423423
def _on_invoke_later(self, message: messages.InvokeLater) -> None:
424424
# Forward InvokeLater message to the Screen
425-
self.app.screen._invoke_later(message.callback)
425+
self.app.screen._invoke_later(
426+
message.callback, message._sender or active_message_pump.get()
427+
)
426428

427429
def _close_messages_no_wait(self) -> None:
428430
"""Request the message queue to immediately exit."""

src/textual/screen.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from . import errors, events, messages
2828
from ._callback import invoke
2929
from ._compositor import Compositor, MapGeometry
30-
from ._context import visible_screen_stack
30+
from ._context import active_message_pump, visible_screen_stack
3131
from ._path import CSSPathType, _css_path_type_as_list, _make_path_object_relative
3232
from ._types import CallbackType
3333
from .binding import Binding
@@ -48,6 +48,7 @@
4848

4949
# Unused & ignored imports are needed for the docs to link to these objects:
5050
from .errors import NoWidget # type: ignore # noqa: F401
51+
from .message_pump import MessagePump
5152

5253
# Screen updates will be batched so that they don't happen more often than 60 times per second:
5354
UPDATE_PERIOD: Final[float] = 1 / 60
@@ -154,7 +155,7 @@ def __init__(
154155
self._compositor = Compositor()
155156
self._dirty_widgets: set[Widget] = set()
156157
self.__update_timer: Timer | None = None
157-
self._callbacks: list[CallbackType] = []
158+
self._callbacks: list[tuple[CallbackType, MessagePump]] = []
158159
self._result_callbacks: list[ResultCallback[ScreenResultType]] = []
159160

160161
self._tooltip_widget: Widget | None = None
@@ -587,17 +588,22 @@ async def _invoke_and_clear_callbacks(self) -> None:
587588
if self._callbacks:
588589
callbacks = self._callbacks[:]
589590
self._callbacks.clear()
590-
for callback in callbacks:
591-
await invoke(callback)
591+
for callback, message_pump in callbacks:
592+
reset_token = active_message_pump.set(message_pump)
593+
try:
594+
await invoke(callback)
595+
finally:
596+
active_message_pump.reset(reset_token)
592597

593-
def _invoke_later(self, callback: CallbackType) -> None:
598+
def _invoke_later(self, callback: CallbackType, sender: MessagePump) -> None:
594599
"""Enqueue a callback to be invoked after the screen is repainted.
595600
596601
Args:
597602
callback: A callback.
603+
sender: The sender (active message pump) of the callback.
598604
"""
599605

600-
self._callbacks.append(callback)
606+
self._callbacks.append((callback, sender))
601607
self.check_idle()
602608

603609
def _push_result_callback(

tests/test_call_later.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import asyncio
22

3+
from textual._context import active_message_pump
34
from textual.app import App
5+
from textual.message_pump import MessagePump
46

57

68
class CallLaterApp(App[None]):
@@ -30,13 +32,19 @@ async def test_call_after_refresh() -> None:
3032

3133
called_event = asyncio.Event()
3234

35+
callback_message_pump: MessagePump | None = None
36+
3337
def callback() -> None:
3438
nonlocal display_count
39+
nonlocal callback_message_pump
3540
called_event.set()
3641
display_count = app.display_count
42+
callback_message_pump = active_message_pump.get()
3743

3844
async with app.run_test():
3945
app.call_after_refresh(callback)
4046
await asyncio.wait_for(called_event.wait(), 1)
4147
app_display_count = app.display_count
4248
assert app_display_count == display_count
49+
# Should be app because the callback should be in the same context as app
50+
assert callback_message_pump is app

0 commit comments

Comments
 (0)