From 5db945166458d6c1d4ebebfc0689b7520db92191 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 5 Mar 2023 09:35:53 +0000 Subject: [PATCH 01/18] remove sender --- docs/examples/events/custom01.py | 6 ++-- src/textual/_animator.py | 1 - src/textual/_xterm_parser.py | 34 +++++++-------------- src/textual/app.py | 28 ++++++++--------- src/textual/events.py | 41 +++++++++---------------- src/textual/message.py | 25 ++++++--------- src/textual/message_pump.py | 19 ++++++------ src/textual/messages.py | 13 ++++---- src/textual/reactive.py | 6 ++-- src/textual/screen.py | 4 +-- src/textual/scrollbar.py | 3 +- src/textual/timer.py | 4 --- src/textual/widget.py | 8 +++-- src/textual/widgets/_button.py | 6 ++-- src/textual/widgets/_data_table.py | 42 ++++++++------------------ src/textual/widgets/_directory_tree.py | 3 +- src/textual/widgets/_input.py | 12 ++++---- src/textual/widgets/_list_item.py | 5 ++- src/textual/widgets/_list_view.py | 14 +++++---- src/textual/widgets/_markdown.py | 22 ++++++-------- src/textual/widgets/_radio_set.py | 9 +++--- src/textual/widgets/_switch.py | 6 ++-- src/textual/widgets/_toggle_button.py | 8 ++--- src/textual/widgets/_tree.py | 24 +++++---------- tests/test_data_table.py | 7 ++--- 25 files changed, 144 insertions(+), 206 deletions(-) diff --git a/docs/examples/events/custom01.py b/docs/examples/events/custom01.py index c96f2e0a9d..b850494db9 100644 --- a/docs/examples/events/custom01.py +++ b/docs/examples/events/custom01.py @@ -10,9 +10,9 @@ class ColorButton(Static): class Selected(Message): """Color selected message.""" - def __init__(self, sender: MessageTarget, color: Color) -> None: + def __init__(self, color: Color) -> None: self.color = color - super().__init__(sender) + super().__init__() def __init__(self, color: Color) -> None: self.color = color @@ -26,7 +26,7 @@ def on_mount(self) -> None: async def on_click(self) -> None: # The post_message method sends an event to be handled in the DOM - await self.post_message(self.Selected(self, self.color)) + await self.post_message(self.Selected(self.color)) def render(self) -> str: return str(self.color) diff --git a/src/textual/_animator.py b/src/textual/_animator.py index 9340e1fd9e..0814cc4bc0 100644 --- a/src/textual/_animator.py +++ b/src/textual/_animator.py @@ -182,7 +182,6 @@ def __init__(self, app: App, frames_per_second: int = 60) -> None: self._timer = Timer( app, 1 / frames_per_second, - app, name="Animator", callback=self, pause=True, diff --git a/src/textual/_xterm_parser.py b/src/textual/_xterm_parser.py index a68da302c7..e1b2169c15 100644 --- a/src/textual/_xterm_parser.py +++ b/src/textual/_xterm_parser.py @@ -26,10 +26,7 @@ class XTermParser(Parser[events.Event]): _re_sgr_mouse = re.compile(r"\x1b\[<(\d+);(\d+);(\d+)([Mm])") - def __init__( - self, sender: MessageTarget, more_data: Callable[[], bool], debug: bool = False - ) -> None: - self.sender = sender + def __init__(self, more_data: Callable[[], bool], debug: bool = False) -> None: self.more_data = more_data self.last_x = 0 self.last_y = 0 @@ -47,7 +44,7 @@ def feed(self, data: str) -> Iterable[events.Event]: self.debug_log(f"FEED {data!r}") return super().feed(data) - def parse_mouse_code(self, code: str, sender: MessageTarget) -> events.Event | None: + def parse_mouse_code(self, code: str) -> events.Event | None: sgr_match = self._re_sgr_mouse.match(code) if sgr_match: _buttons, _x, _y, state = sgr_match.groups() @@ -74,7 +71,6 @@ def parse_mouse_code(self, code: str, sender: MessageTarget) -> events.Event | N button = (buttons + 1) & 3 event = event_class( - sender, x, y, delta_x, @@ -103,7 +99,7 @@ def reissue_sequence_as_keys(reissue_sequence: str) -> None: key_events = sequence_to_key_events(character) for event in key_events: if event.key == "escape": - event = events.Key(event.sender, "circumflex_accent", "^") + event = events.Key("circumflex_accent", "^") on_token(event) while not self.is_eof: @@ -116,9 +112,7 @@ def reissue_sequence_as_keys(reissue_sequence: str) -> None: # the full escape code was. pasted_text = "".join(paste_buffer[:-1]) # Note the removal of NUL characters: https://github.com/Textualize/textual/issues/1661 - on_token( - events.Paste(self.sender, text=pasted_text.replace("\x00", "")) - ) + on_token(events.Paste(text=pasted_text.replace("\x00", ""))) paste_buffer.clear() character = ESC if use_prior_escape else (yield read1()) @@ -145,12 +139,12 @@ def reissue_sequence_as_keys(reissue_sequence: str) -> None: peek_buffer = yield self.peek_buffer() if not peek_buffer: # An escape arrived without any following characters - on_token(events.Key(self.sender, "escape", "\x1b")) + on_token(events.Key("escape", "\x1b")) continue if peek_buffer and peek_buffer[0] == ESC: # There is an escape in the buffer, so ESC ESC has arrived yield read1() - on_token(events.Key(self.sender, "escape", "\x1b")) + on_token(events.Key("escape", "\x1b")) # If there is no further data, it is not part of a sequence, # So we don't need to go in to the loop if len(peek_buffer) == 1 and not more_data(): @@ -208,7 +202,7 @@ def reissue_sequence_as_keys(reissue_sequence: str) -> None: mouse_match = _re_mouse_event.match(sequence) if mouse_match is not None: mouse_code = mouse_match.group(0) - event = self.parse_mouse_code(mouse_code, self.sender) + event = self.parse_mouse_code(mouse_code) if event: on_token(event) break @@ -221,11 +215,7 @@ def reissue_sequence_as_keys(reissue_sequence: str) -> None: mode_report_match["mode_id"] == "2026" and int(mode_report_match["setting_parameter"]) > 0 ): - on_token( - messages.TerminalSupportsSynchronizedOutput( - self.sender - ) - ) + on_token(messages.TerminalSupportsSynchronizedOutput()) break else: if not bracketed_paste: @@ -247,9 +237,7 @@ def _sequence_to_key_events( keys = ANSI_SEQUENCES_KEYS.get(sequence) if keys is not None: for key in keys: - yield events.Key( - self.sender, key.value, sequence if len(sequence) == 1 else None - ) + yield events.Key(key.value, sequence if len(sequence) == 1 else None) elif len(sequence) == 1: try: if not sequence.isalnum(): @@ -262,6 +250,6 @@ def _sequence_to_key_events( else: name = sequence name = KEY_NAME_REPLACEMENTS.get(name, name) - yield events.Key(self.sender, name, sequence) + yield events.Key(name, sequence) except: - yield events.Key(self.sender, sequence, sequence) + yield events.Key(sequence, sequence) diff --git a/src/textual/app.py b/src/textual/app.py index 3ca169e857..68a7c4f986 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -525,7 +525,7 @@ def exit( """ self._exit = True self._return_value = result - self.post_message_no_wait(messages.ExitApp(sender=self)) + self.post_message_no_wait(messages.ExitApp()) if message: self._exit_renderables.append(message) @@ -1272,7 +1272,7 @@ def _replace_screen(self, screen: Screen) -> Screen: The screen that was replaced. """ - screen.post_message_no_wait(events.ScreenSuspend(self)) + screen.post_message_no_wait(events.ScreenSuspend()) self.log.system(f"{screen} SUSPENDED") if not self.is_screen_installed(screen) and screen not in self._screen_stack: screen.remove() @@ -1288,7 +1288,7 @@ def push_screen(self, screen: Screen | str) -> AwaitMount: """ next_screen, await_mount = self._get_screen(screen) self._screen_stack.append(next_screen) - self.screen.post_message_no_wait(events.ScreenResume(self)) + self.screen.post_message_no_wait(events.ScreenResume()) self.log.system(f"{self.screen} is current (PUSHED)") return await_mount @@ -1303,7 +1303,7 @@ def switch_screen(self, screen: Screen | str) -> AwaitMount: self._replace_screen(self._screen_stack.pop()) next_screen, await_mount = self._get_screen(screen) self._screen_stack.append(next_screen) - self.screen.post_message_no_wait(events.ScreenResume(self)) + self.screen.post_message_no_wait(events.ScreenResume()) self.log.system(f"{self.screen} is current (SWITCHED)") return await_mount return AwaitMount(self.screen, []) @@ -1382,7 +1382,7 @@ def pop_screen(self) -> Screen: ) previous_screen = self._replace_screen(screen_stack.pop()) self.screen._screen_resized(self.size) - self.screen.post_message_no_wait(events.ScreenResume(self)) + self.screen.post_message_no_wait(events.ScreenResume()) self.log.system(f"{self.screen} is active") return previous_screen @@ -1404,16 +1404,16 @@ async def _set_mouse_over(self, widget: Widget | None) -> None: if widget is None: if self.mouse_over is not None: try: - await self.mouse_over.post_message(events.Leave(self)) + await self.mouse_over.post_message(events.Leave()) finally: self.mouse_over = None else: if self.mouse_over is not widget: try: if self.mouse_over is not None: - await self.mouse_over._forward_event(events.Leave(self)) + await self.mouse_over._forward_event(events.Leave()) if widget is not None: - await widget._forward_event(events.Enter(self)) + await widget._forward_event(events.Enter()) finally: self.mouse_over = widget @@ -1427,11 +1427,11 @@ def capture_mouse(self, widget: Widget | None) -> None: return if self.mouse_captured is not None: self.mouse_captured.post_message_no_wait( - events.MouseRelease(self, self.mouse_position) + events.MouseRelease(self.mouse_position) ) self.mouse_captured = widget if widget is not None: - widget.post_message_no_wait(events.MouseCapture(self, self.mouse_position)) + widget.post_message_no_wait(events.MouseCapture(self.mouse_position)) def panic(self, *renderables: RenderableType) -> None: """Exits the app then displays a message. @@ -1544,8 +1544,8 @@ async def invoke_ready_callback() -> None: with self.batch_update(): try: try: - await self._dispatch_message(events.Compose(sender=self)) - await self._dispatch_message(events.Mount(sender=self)) + await self._dispatch_message(events.Compose()) + await self._dispatch_message(events.Mount()) finally: self._mounted_event.set() @@ -1579,7 +1579,7 @@ async def invoke_ready_callback() -> None: self._running = True try: - load_event = events.Load(sender=self) + load_event = events.Load() await self._dispatch_message(load_event) driver: Driver @@ -1825,7 +1825,7 @@ async def _shutdown(self) -> None: await self._close_all() await self._close_messages() - await self._dispatch_message(events.Unmount(sender=self)) + await self._dispatch_message(events.Unmount()) self._print_error_renderables() if self.devtools is not None and self.devtools.is_connected: diff --git a/src/textual/events.py b/src/textual/events.py index 28887bec9e..dcbb43ed9c 100644 --- a/src/textual/events.py +++ b/src/textual/events.py @@ -28,9 +28,9 @@ def __rich_repr__(self) -> rich.repr.Result: @rich.repr.auto class Callback(Event, bubble=False, verbose=True): - def __init__(self, sender: MessageTarget, callback: CallbackType) -> None: + def __init__(self, callback: CallbackType) -> None: self.callback = callback - super().__init__(sender) + super().__init__() def __rich_repr__(self) -> rich.repr.Result: yield "callback", self.callback @@ -71,8 +71,8 @@ class Idle(Event, bubble=False): class Action(Event): __slots__ = ["action"] - def __init__(self, sender: MessageTarget, action: str) -> None: - super().__init__(sender) + def __init__(self, action: str) -> None: + super().__init__() self.action = action def __rich_repr__(self) -> rich.repr.Result: @@ -82,7 +82,6 @@ def __rich_repr__(self) -> rich.repr.Result: class Resize(Event, bubble=False): """Sent when the app or widget has been resized. Args: - sender: The sender of the event (the Screen). size: The new size of the Widget. virtual_size: The virtual size (scrollable size) of the Widget. container_size: The size of the Widget's container widget. Defaults to None. @@ -93,7 +92,6 @@ class Resize(Event, bubble=False): def __init__( self, - sender: MessageTarget, size: Size, virtual_size: Size, container_size: Size | None = None, @@ -101,7 +99,7 @@ def __init__( self.size = size self.virtual_size = virtual_size self.container_size = size if container_size is None else container_size - super().__init__(sender) + super().__init__() def can_replace(self, message: "Message") -> bool: return isinstance(message, Resize) @@ -149,13 +147,12 @@ class MouseCapture(Event, bubble=False): Args: - sender: The sender of the event, (in this case the app). mouse_position: The position of the mouse when captured. """ - def __init__(self, sender: MessageTarget, mouse_position: Offset) -> None: - super().__init__(sender) + def __init__(self, mouse_position: Offset) -> None: + super().__init__() self.mouse_position = mouse_position def __rich_repr__(self) -> rich.repr.Result: @@ -167,12 +164,11 @@ class MouseRelease(Event, bubble=False): """Mouse has been released. Args: - sender: The sender of the event, (in this case the app). mouse_position: The position of the mouse when released. """ - def __init__(self, sender: MessageTarget, mouse_position: Offset) -> None: - super().__init__(sender) + def __init__(self, mouse_position: Offset) -> None: + super().__init__() self.mouse_position = mouse_position def __rich_repr__(self) -> rich.repr.Result: @@ -188,7 +184,6 @@ class Key(InputEvent): """Sent when the user hits a key on the keyboard. Args: - sender: The sender of the event (always the App). key: The key that was pressed. character: A printable character or ``None`` if it is not printable. @@ -198,8 +193,8 @@ class Key(InputEvent): __slots__ = ["key", "character", "aliases"] - def __init__(self, sender: MessageTarget, key: str, character: str | None) -> None: - super().__init__(sender) + def __init__(self, key: str, character: str | None) -> None: + super().__init__() self.key = key self.character = ( (key if len(key) == 1 else None) if character is None else character @@ -245,7 +240,6 @@ class MouseEvent(InputEvent, bubble=True): """Sent in response to a mouse event. Args: - sender: The sender of the event. x: The relative x coordinate. y: The relative y coordinate. delta_x: Change in x since the last message. @@ -276,7 +270,6 @@ class MouseEvent(InputEvent, bubble=True): def __init__( self, - sender: MessageTarget, x: int, y: int, delta_x: int, @@ -289,7 +282,7 @@ def __init__( screen_y: int | None = None, style: Style | None = None, ) -> None: - super().__init__(sender) + super().__init__() self.x = x self.y = y self.delta_x = delta_x @@ -305,7 +298,6 @@ def __init__( @classmethod def from_event(cls: Type[MouseEventT], event: MouseEvent) -> MouseEventT: new_event = cls( - event.sender, event.x, event.y, event.delta_x, @@ -387,7 +379,6 @@ def get_content_offset(self, widget: Widget) -> Offset | None: def _apply_offset(self, x: int, y: int) -> MouseEvent: return self.__class__( - self.sender, x=self.x + x, y=self.y + y, delta_x=self.delta_x, @@ -437,13 +428,12 @@ class Timer(Event, bubble=False, verbose=True): def __init__( self, - sender: MessageTarget, timer: "TimerClass", time: float, count: int = 0, callback: TimerCallback | None = None, ) -> None: - super().__init__(sender) + super().__init__() self.timer = timer self.time = time self.count = count @@ -486,12 +476,11 @@ class Paste(Event, bubble=True): and disable it when the app shuts down. Args: - sender: The sender of the event, (in this case the app). text: The text that has been pasted. """ - def __init__(self, sender: MessageTarget, text: str) -> None: - super().__init__(sender) + def __init__(self, text: str) -> None: + super().__init__() self.text = text def __rich_repr__(self) -> rich.repr.Result: diff --git a/src/textual/message.py b/src/textual/message.py index 8f6816619f..fb38837264 100644 --- a/src/textual/message.py +++ b/src/textual/message.py @@ -14,18 +14,9 @@ @rich.repr.auto class Message: - """Base class for a message. - - Args: - sender: The sender of the message / event. - - Attributes: - sender: The sender of the message. - time: The time when the message was sent. - """ + """Base class for a message.""" __slots__ = [ - "sender", "time", "_forwarded", "_no_default_action", @@ -34,15 +25,13 @@ class Message: "_prevent", ] - sender: MessageTarget bubble: ClassVar[bool] = True # Message will bubble to parent verbose: ClassVar[bool] = False # Message is verbose no_dispatch: ClassVar[bool] = False # Message may not be handled by client code namespace: ClassVar[str] = "" # Namespace to disambiguate messages - def __init__(self, sender: MessageTarget) -> None: - self.sender: MessageTarget = sender - + def __init__(self) -> None: + self._sender: MessageTarget | None = None self.time: float = _clock.get_time_no_wait() self._forwarded = False self._no_default_action = False @@ -55,7 +44,7 @@ def __init__(self, sender: MessageTarget) -> None: super().__init__() def __rich_repr__(self) -> rich.repr.Result: - yield self.sender + yield from () def __init_subclass__( cls, @@ -73,6 +62,12 @@ def __init_subclass__( if namespace is not None: cls.namespace = namespace + @property + def sender(self) -> MessageTarget: + """The sender of the message.""" + assert self._sender is not None + return self._sender + @property def is_forwarded(self) -> bool: return self._forwarded diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 4697da2c67..54e117035f 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -321,7 +321,6 @@ def set_interval( timer = Timer( self, interval, - self, name=name or f"set_interval#{Timer._timer_count}", callback=callback, repeat=repeat or None, @@ -341,7 +340,7 @@ def call_after_refresh(self, callback: Callable, *args, **kwargs) -> None: # We send the InvokeLater message to ourselves first, to ensure we've cleared # out anything already pending in our own queue. - message = messages.InvokeLater(self, partial(callback, *args, **kwargs)) + message = messages.InvokeLater(partial(callback, *args, **kwargs)) self.post_message_no_wait(message) def call_later(self, callback: Callable, *args, **kwargs) -> None: @@ -353,7 +352,7 @@ def call_later(self, callback: Callable, *args, **kwargs) -> None: *args: Positional arguments to pass to the callable. **kwargs: Keyword arguments to pass to the callable. """ - message = events.Callback(self, callback=partial(callback, *args, **kwargs)) + message = events.Callback(callback=partial(callback, *args, **kwargs)) self.post_message_no_wait(message) def call_next(self, callback: Callable, *args, **kwargs) -> None: @@ -372,7 +371,7 @@ def _on_invoke_later(self, message: messages.InvokeLater) -> None: def _close_messages_no_wait(self) -> None: """Request the message queue to immediately exit.""" - self._message_queue.put_nowait(messages.CloseMessages(sender=self)) + self._message_queue.put_nowait(messages.CloseMessages()) async def _on_close_messages(self, message: messages.CloseMessages) -> None: await self._close_messages() @@ -386,7 +385,7 @@ async def _close_messages(self, wait: bool = True) -> None: for timer in stop_timers: await timer.stop() self._timers.clear() - await self._message_queue.put(events.Unmount(sender=self)) + await self._message_queue.put(events.Unmount()) Reactive._reset_object(self) await self._message_queue.put(None) if wait and self._task is not None and asyncio.current_task() != self._task: @@ -428,8 +427,8 @@ async def _pre_process(self) -> None: # 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)) + await self._dispatch_message(events.Compose()) + await self._dispatch_message(events.Mount()) self._post_mount() except Exception as error: self.app._handle_exception(error) @@ -489,7 +488,7 @@ async def _process_messages_loop(self) -> None: ): self._last_idle = current_time if not self._closed: - event = events.Idle(self) + event = events.Idle() for _cls, method in self._get_dispatch_methods( "on_idle", event ): @@ -590,7 +589,7 @@ async def _on_message(self, message: Message) -> None: def check_idle(self) -> None: """Prompt the message pump to call idle if the queue is empty.""" if self._message_queue.empty(): - self.post_message_no_wait(messages.Prompt(sender=self)) + self.post_message_no_wait(messages.Prompt()) async def post_message(self, message: Message) -> bool: """Post a message or an event to this message pump. @@ -607,6 +606,8 @@ async def post_message(self, message: Message) -> bool: return False if not self.check_message_enabled(message): return True + if message._sender is None: + message._sender = self # Add a copy of the prevented message types to the message # This is so that prevented messages are honoured by the event's handler message._prevent.update(self._get_prevented_messages()) diff --git a/src/textual/messages.py b/src/textual/messages.py index 778751d267..d849cb69d8 100644 --- a/src/textual/messages.py +++ b/src/textual/messages.py @@ -25,12 +25,11 @@ class ExitApp(Message, verbose=True): @rich.repr.auto class Update(Message, verbose=True): - def __init__(self, sender: MessagePump, widget: Widget): - super().__init__(sender) + def __init__(self, widget: Widget): + super().__init__() self.widget = widget def __rich_repr__(self) -> rich.repr.Result: - yield self.sender yield self.widget def __eq__(self, other: object) -> bool: @@ -63,9 +62,9 @@ def can_replace(self, message: Message) -> bool: class InvokeLater(Message, verbose=True, bubble=False): """Sent by Textual to invoke a callback.""" - def __init__(self, sender: MessagePump, callback: CallbackType) -> None: + def __init__(self, callback: CallbackType) -> None: self.callback = callback - super().__init__(sender) + super().__init__() def __rich_repr__(self) -> rich.repr.Result: yield "callback", self.callback @@ -75,9 +74,9 @@ def __rich_repr__(self) -> rich.repr.Result: class ScrollToRegion(Message, bubble=False): """Ask the parent to scroll a given region in to view.""" - def __init__(self, sender: MessagePump, region: Region) -> None: + def __init__(self, region: Region) -> None: self.region = region - super().__init__(sender) + super().__init__() class Prompt(Message, no_dispatch=True): diff --git a/src/textual/reactive.py b/src/textual/reactive.py index 8a4ac4d822..f415325951 100644 --- a/src/textual/reactive.py +++ b/src/textual/reactive.py @@ -211,7 +211,7 @@ async def await_watcher(awaitable: Awaitable) -> None: await awaitable # Watcher may have changed the state, so run compute again obj.post_message_no_wait( - events.Callback(sender=obj, callback=partial(Reactive._compute, obj)) + events.Callback(callback=partial(Reactive._compute, obj)) ) def invoke_watcher( @@ -236,9 +236,7 @@ def invoke_watcher( if isawaitable(watch_result): # Result is awaitable, so we need to await it within an async context obj.post_message_no_wait( - events.Callback( - sender=obj, callback=partial(await_watcher, watch_result) - ) + events.Callback(callback=partial(await_watcher, watch_result)) ) watch_function = getattr(obj, f"watch_{name}", None) diff --git a/src/textual/screen.py b/src/textual/screen.py index dbe5b3a80e..c1bcd7fa9c 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -330,14 +330,14 @@ def set_focus(self, widget: Widget | None, scroll_visible: bool = True) -> None: if widget is None: # No focus, so blur currently focused widget if it exists if self.focused is not None: - self.focused.post_message_no_wait(events.Blur(self)) + self.focused.post_message_no_wait(events.Blur()) self.focused = None self.log.debug("focus was removed") elif widget.focusable: if self.focused != widget: if self.focused is not None: # Blur currently focused widget - self.focused.post_message_no_wait(events.Blur(self)) + self.focused.post_message_no_wait(events.Blur()) # Change focus self.focused = widget # Send focus event diff --git a/src/textual/scrollbar.py b/src/textual/scrollbar.py index 46a65e27be..06b2db8277 100644 --- a/src/textual/scrollbar.py +++ b/src/textual/scrollbar.py @@ -47,7 +47,6 @@ class ScrollTo(ScrollMessage, verbose=True): def __init__( self, - sender: MessageTarget, x: float | None = None, y: float | None = None, animate: bool = True, @@ -55,7 +54,7 @@ def __init__( self.x = x self.y = y self.animate = animate - super().__init__(sender) + super().__init__() def __rich_repr__(self) -> rich.repr.Result: yield "x", self.x, None diff --git a/src/textual/timer.py b/src/textual/timer.py index e54c7cef14..b4ecbd34aa 100644 --- a/src/textual/timer.py +++ b/src/textual/timer.py @@ -34,7 +34,6 @@ class Timer: Args: event_target: The object which will receive the timer events. interval: The time between timer events, in seconds. - sender: The sender of the event. name: A name to assign the event (for debugging). Defaults to None. callback: A optional callback to invoke when the event is handled. Defaults to None. repeat: The number of times to repeat the timer, or None to repeat forever. Defaults to None. @@ -48,7 +47,6 @@ def __init__( self, event_target: MessageTarget, interval: float, - sender: MessageTarget, *, name: str | None = None, callback: TimerCallback | None = None, @@ -59,7 +57,6 @@ def __init__( self._target_repr = repr(event_target) self._target = weakref.ref(event_target) self._interval = interval - self.sender = sender self.name = f"Timer#{self._timer_count}" if name is None else name self._timer_count += 1 self._callback = callback @@ -170,7 +167,6 @@ async def _tick(self, *, next_timer: float, count: int) -> None: app._handle_exception(error) else: event = events.Timer( - self.sender, timer=self, time=next_timer, count=count, diff --git a/src/textual/widget.py b/src/textual/widget.py index ea1a70c3cd..bbdd06ac39 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -2588,6 +2588,8 @@ async def post_message(self, message: Message) -> bool: Returns: True if the message was posted, False if this widget was closed / closing. """ + if message._sender is None: + message._sender = self if not self.check_message_enabled(message): return True if not self.is_running: @@ -2608,13 +2610,13 @@ async def _on_idle(self, event: events.Idle) -> None: else: if self._scroll_required: self._scroll_required = False - screen.post_message_no_wait(messages.UpdateScroll(self)) + screen.post_message_no_wait(messages.UpdateScroll()) if self._repaint_required: self._repaint_required = False - screen.post_message_no_wait(messages.Update(self, self)) + screen.post_message_no_wait(messages.Update(self)) if self._layout_required: self._layout_required = False - screen.post_message_no_wait(messages.Layout(self)) + screen.post_message_no_wait(messages.Layout()) def focus(self, scroll_visible: bool = True) -> None: """Give focus to this widget. diff --git a/src/textual/widgets/_button.py b/src/textual/widgets/_button.py index 6b02ab2718..c9e4be6bf0 100644 --- a/src/textual/widgets/_button.py +++ b/src/textual/widgets/_button.py @@ -165,9 +165,9 @@ class Pressed(Message, bubble=True): button: The button that was pressed. """ - @property - def button(self) -> Button: - return cast(Button, self.sender) + def __init__(self, button: Button) -> None: + self.button = button + super().__init__() def __init__( self, diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py index 3ae85ca767..7784c7b80b 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -319,7 +319,6 @@ class CellHighlighted(Message, bubble=True): def __init__( self, - sender: DataTable, value: CellType, coordinate: Coordinate, cell_key: CellKey, @@ -330,10 +329,9 @@ def __init__( """The coordinate of the highlighted cell.""" self.cell_key: CellKey = cell_key """The key for the highlighted cell.""" - super().__init__(sender) + super().__init__() def __rich_repr__(self) -> rich.repr.Result: - yield "sender", self.sender yield "value", self.value yield "coordinate", self.coordinate yield "cell_key", self.cell_key @@ -348,7 +346,6 @@ class CellSelected(Message, bubble=True): def __init__( self, - sender: DataTable, value: CellType, coordinate: Coordinate, cell_key: CellKey, @@ -359,10 +356,9 @@ def __init__( """The coordinate of the cell that was selected.""" self.cell_key: CellKey = cell_key """The key for the selected cell.""" - super().__init__(sender) + super().__init__() def __rich_repr__(self) -> rich.repr.Result: - yield "sender", self.sender yield "value", self.value yield "coordinate", self.coordinate yield "cell_key", self.cell_key @@ -376,15 +372,14 @@ class RowHighlighted(Message, bubble=True): widget in the DOM. """ - def __init__(self, sender: DataTable, cursor_row: int, row_key: RowKey) -> None: + def __init__(self, cursor_row: int, row_key: RowKey) -> None: self.cursor_row: int = cursor_row """The y-coordinate of the cursor that highlighted the row.""" self.row_key: RowKey = row_key """The key of the row that was highlighted.""" - super().__init__(sender) + super().__init__() def __rich_repr__(self) -> rich.repr.Result: - yield "sender", self.sender yield "cursor_row", self.cursor_row yield "row_key", self.row_key @@ -397,15 +392,14 @@ class RowSelected(Message, bubble=True): widget in the DOM. """ - def __init__(self, sender: DataTable, cursor_row: int, row_key: RowKey) -> None: + def __init__(self, cursor_row: int, row_key: RowKey) -> None: self.cursor_row: int = cursor_row """The y-coordinate of the cursor that made the selection.""" self.row_key: RowKey = row_key """The key of the row that was selected.""" - super().__init__(sender) + super().__init__() def __rich_repr__(self) -> rich.repr.Result: - yield "sender", self.sender yield "cursor_row", self.cursor_row yield "row_key", self.row_key @@ -418,17 +412,14 @@ class ColumnHighlighted(Message, bubble=True): widget in the DOM. """ - def __init__( - self, sender: DataTable, cursor_column: int, column_key: ColumnKey - ) -> None: + def __init__(self, cursor_column: int, column_key: ColumnKey) -> None: self.cursor_column: int = cursor_column """The x-coordinate of the column that was highlighted.""" self.column_key = column_key """The key of the column that was highlighted.""" - super().__init__(sender) + super().__init__() def __rich_repr__(self) -> rich.repr.Result: - yield "sender", self.sender yield "cursor_column", self.cursor_column yield "column_key", self.column_key @@ -441,17 +432,14 @@ class ColumnSelected(Message, bubble=True): widget in the DOM. """ - def __init__( - self, sender: DataTable, cursor_column: int, column_key: ColumnKey - ) -> None: + def __init__(self, cursor_column: int, column_key: ColumnKey) -> None: self.cursor_column: int = cursor_column """The x-coordinate of the column that was selected.""" self.column_key = column_key """The key of the column that was selected.""" - super().__init__(sender) + super().__init__() def __rich_repr__(self) -> rich.repr.Result: - yield "sender", self.sender yield "cursor_column", self.cursor_column yield "column_key", self.column_key @@ -460,7 +448,6 @@ class HeaderSelected(Message, bubble=True): def __init__( self, - sender: DataTable, column_key: ColumnKey, column_index: int, label: Text, @@ -471,10 +458,9 @@ def __init__( """The index for the column.""" self.label = label """The text of the label.""" - super().__init__(sender) + super().__init__() def __rich_repr__(self) -> rich.repr.Result: - yield "sender", self.sender yield "column_key", self.column_key yield "column_index", self.column_index yield "label", self.label.plain @@ -484,7 +470,6 @@ class RowLabelSelected(Message, bubble=True): def __init__( self, - sender: DataTable, row_key: RowKey, row_index: int, label: Text, @@ -495,10 +480,9 @@ def __init__( """The index for the column.""" self.label = label """The text of the label.""" - super().__init__(sender) + super().__init__() def __rich_repr__(self) -> rich.repr.Result: - yield "sender", self.sender yield "row_key", self.row_key yield "row_index", self.row_index yield "label", self.label.plain @@ -898,7 +882,7 @@ def _highlight_coordinate(self, coordinate: Coordinate) -> None: cell_key = self.coordinate_to_cell_key(coordinate) self.post_message_no_wait( DataTable.CellHighlighted( - self, cell_value, coordinate=coordinate, cell_key=cell_key + cell_value, coordinate=coordinate, cell_key=cell_key ) ) diff --git a/src/textual/widgets/_directory_tree.py b/src/textual/widgets/_directory_tree.py index 8b481a5522..cc95dfb0bb 100644 --- a/src/textual/widgets/_directory_tree.py +++ b/src/textual/widgets/_directory_tree.py @@ -77,9 +77,8 @@ class FileSelected(Message, bubble=True): path: The path of the file that was selected. """ - def __init__(self, sender: MessageTarget, path: str) -> None: + def __init__(self, path: str) -> None: self.path: str = path - super().__init__(sender) def __init__( self, diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index 4a6816ac77..9372e61c8d 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -146,10 +146,10 @@ class Changed(Message, bubble=True): input: The `Input` widget that was changed. """ - def __init__(self, sender: Input, value: str) -> None: - super().__init__(sender) + def __init__(self, input: Input, value: str) -> None: + super().__init__() + self.input: Input = input self.value: str = value - self.input: Input = sender class Submitted(Message, bubble=True): """Posted when the enter key is pressed within an `Input`. @@ -162,10 +162,10 @@ class Submitted(Message, bubble=True): input: The `Input` widget that is being submitted. """ - def __init__(self, sender: Input, value: str) -> None: - super().__init__(sender) + def __init__(self, input: Input, value: str) -> None: + super().__init__() + self.input: Input = input self.value: str = value - self.input: Input = sender def __init__( self, diff --git a/src/textual/widgets/_list_item.py b/src/textual/widgets/_list_item.py index b8bf513672..a6567fec05 100644 --- a/src/textual/widgets/_list_item.py +++ b/src/textual/widgets/_list_item.py @@ -1,5 +1,7 @@ """Provides a list item widget for use with `ListView`.""" +from __future__ import annotations + from textual import events from textual.message import Message from textual.reactive import reactive @@ -41,7 +43,8 @@ class ListItem(Widget, can_focus=False): class _ChildClicked(Message): """For informing with the parent ListView that we were clicked""" - sender: "ListItem" + def __init__(self, item: ListItem) -> None: + self.item = item def on_click(self, event: events.Click) -> None: self.post_message_no_wait(self._ChildClicked(self)) diff --git a/src/textual/widgets/_list_view.py b/src/textual/widgets/_list_view.py index e24ecf31b5..53b1250415 100644 --- a/src/textual/widgets/_list_view.py +++ b/src/textual/widgets/_list_view.py @@ -48,8 +48,9 @@ class Highlighted(Message, bubble=True): item: The highlighted item, if there is one highlighted. """ - def __init__(self, sender: ListView, item: ListItem | None) -> None: - super().__init__(sender) + def __init__(self, list_view: ListView, item: ListItem | None) -> None: + super().__init__() + self.list_view = list_view self.item: ListItem | None = item class Selected(Message, bubble=True): @@ -62,8 +63,9 @@ class Selected(Message, bubble=True): item: The selected item. """ - def __init__(self, sender: ListView, item: ListItem) -> None: - super().__init__(sender) + def __init__(self, list_view: ListView, item: ListItem) -> None: + super().__init__() + self.list_view = list_view self.item: ListItem = item def __init__( @@ -194,8 +196,8 @@ def action_cursor_up(self) -> None: def on_list_item__child_clicked(self, event: ListItem._ChildClicked) -> None: self.focus() - self.index = self._nodes.index(event.sender) - self.post_message_no_wait(self.Selected(self, event.sender)) + self.index = self._nodes.index(event.item) + self.post_message_no_wait(self.Selected(self, event.item)) def _scroll_highlighted_region(self) -> None: """Used to keep the highlighted index within vision""" diff --git a/src/textual/widgets/_markdown.py b/src/textual/widgets/_markdown.py index d7aabe628b..c8203a5a23 100644 --- a/src/textual/widgets/_markdown.py +++ b/src/textual/widgets/_markdown.py @@ -100,7 +100,7 @@ def set_content(self, text: Text) -> None: async def action_link(self, href: str) -> None: """Called on link click.""" - await self.post_message(Markdown.LinkClicked(href, sender=self)) + await self.post_message(Markdown.LinkClicked(href)) class MarkdownHeader(MarkdownBlock): @@ -524,26 +524,24 @@ def __init__( class TableOfContentsUpdated(Message, bubble=True): """The table of contents was updated.""" - def __init__( - self, table_of_contents: TableOfContentsType, *, sender: Widget - ) -> None: - super().__init__(sender=sender) + def __init__(self, table_of_contents: TableOfContentsType) -> None: + super().__init__() self.table_of_contents: TableOfContentsType = table_of_contents """Table of contents.""" class TableOfContentsSelected(Message, bubble=True): """An item in the TOC was selected.""" - def __init__(self, block_id: str, *, sender: Widget) -> None: - super().__init__(sender=sender) + def __init__(self, block_id: str) -> None: + super().__init__() self.block_id = block_id """ID of the block that was selected.""" class LinkClicked(Message, bubble=True): """A link in the document was clicked.""" - def __init__(self, href: str, *, sender: Widget) -> None: - super().__init__(sender=sender) + def __init__(self, href: str) -> None: + super().__init__() self.href: str = href """The link that was selected.""" @@ -702,9 +700,7 @@ async def update(self, markdown: str) -> None: ) ) - await self.post_message( - Markdown.TableOfContentsUpdated(table_of_contents, sender=self) - ) + await self.post_message(Markdown.TableOfContentsUpdated(table_of_contents)) with self.app.batch_update(): await self.query("MarkdownBlock").remove() await self.mount_all(output) @@ -761,7 +757,7 @@ async def on_tree_node_selected(self, message: Tree.NodeSelected) -> None: node_data = message.node.data if node_data is not None: await self.post_message( - Markdown.TableOfContentsSelected(node_data["block_id"], sender=self) + Markdown.TableOfContentsSelected(node_data["block_id"]) ) diff --git a/src/textual/widgets/_radio_set.py b/src/textual/widgets/_radio_set.py index fd40615116..819382fea9 100644 --- a/src/textual/widgets/_radio_set.py +++ b/src/textual/widgets/_radio_set.py @@ -37,15 +37,14 @@ class Changed(Message, bubble=True): This message can be handled using an `on_radio_set_changed` method. """ - def __init__(self, sender: RadioSet, pressed: RadioButton) -> None: + def __init__(self, radio_set: RadioSet, pressed: RadioButton) -> None: """Initialise the message. Args: - sender: The radio set sending the message. pressed: The radio button that was pressed. """ - super().__init__(sender) - self.input = sender + super().__init__() + self.input = radio_set # TODO: Why is this called "input ?" """A reference to the `RadioSet` that was changed.""" self.pressed = pressed """The `RadioButton` that was pressed to make the change.""" @@ -54,7 +53,7 @@ def __init__(self, sender: RadioSet, pressed: RadioButton) -> None: # this point, and so we can't go looking for the index of the # pressed button via the normal route. So here we go under the # hood. - self.index = sender._nodes.index(pressed) + self.index = radio_set._nodes.index(pressed) """The index of the `RadioButton` that was pressed to make the change.""" def __init__( diff --git a/src/textual/widgets/_switch.py b/src/textual/widgets/_switch.py index 97e3e22233..e5dc09a752 100644 --- a/src/textual/widgets/_switch.py +++ b/src/textual/widgets/_switch.py @@ -87,10 +87,10 @@ class Changed(Message, bubble=True): input: The `Switch` widget that was changed. """ - def __init__(self, sender: Switch, value: bool) -> None: - super().__init__(sender) + def __init__(self, switch: Switch, value: bool) -> None: + super().__init__() self.value: bool = value - self.input: Switch = sender + self.switch: Switch = switch def __init__( self, diff --git a/src/textual/widgets/_toggle_button.py b/src/textual/widgets/_toggle_button.py index 40be4e6186..ef8b2df11e 100644 --- a/src/textual/widgets/_toggle_button.py +++ b/src/textual/widgets/_toggle_button.py @@ -218,15 +218,15 @@ def on_click(self) -> None: class Changed(Message, bubble=True): """Posted when the value of the toggle button changes.""" - def __init__(self, sender: ToggleButton, value: bool) -> None: + def __init__(self, toggle_button: ToggleButton, value: bool) -> None: """Initialise the message. Args: - sender: The toggle button sending the message. + toggle_button: The toggle button sending the message. value: The value of the toggle button. """ - super().__init__(sender) - self.input = sender + super().__init__() + self.toggle_button = toggle_button """A reference to the toggle button that was changed.""" self.value = value """The value of the toggle button after the change.""" diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index 3c5e28ceba..dd5ad8c58d 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -436,11 +436,9 @@ class NodeCollapsed(Generic[EventTreeDataType], Message, bubble=True): node: The node that was collapsed. """ - def __init__( - self, sender: MessageTarget, node: TreeNode[EventTreeDataType] - ) -> None: + def __init__(self, node: TreeNode[EventTreeDataType]) -> None: self.node: TreeNode[EventTreeDataType] = node - super().__init__(sender) + super().__init__() class NodeExpanded(Generic[EventTreeDataType], Message, bubble=True): """Event sent when a node is expanded. @@ -452,11 +450,9 @@ class NodeExpanded(Generic[EventTreeDataType], Message, bubble=True): node: The node that was expanded. """ - def __init__( - self, sender: MessageTarget, node: TreeNode[EventTreeDataType] - ) -> None: + def __init__(self, node: TreeNode[EventTreeDataType]) -> None: self.node: TreeNode[EventTreeDataType] = node - super().__init__(sender) + super().__init__() class NodeHighlighted(Generic[EventTreeDataType], Message, bubble=True): """Event sent when a node is highlighted. @@ -468,11 +464,9 @@ class NodeHighlighted(Generic[EventTreeDataType], Message, bubble=True): node: The node that was highlighted. """ - def __init__( - self, sender: MessageTarget, node: TreeNode[EventTreeDataType] - ) -> None: + def __init__(self, node: TreeNode[EventTreeDataType]) -> None: self.node: TreeNode[EventTreeDataType] = node - super().__init__(sender) + super().__init__() class NodeSelected(Generic[EventTreeDataType], Message, bubble=True): """Event sent when a node is selected. @@ -484,11 +478,9 @@ class NodeSelected(Generic[EventTreeDataType], Message, bubble=True): node: The node that was selected. """ - def __init__( - self, sender: MessageTarget, node: TreeNode[EventTreeDataType] - ) -> None: + def __init__(self, node: TreeNode[EventTreeDataType]) -> None: self.node: TreeNode[EventTreeDataType] = node - super().__init__(sender) + super().__init__() def __init__( self, diff --git a/tests/test_data_table.py b/tests/test_data_table.py index 5121410b4b..d6129d1107 100644 --- a/tests/test_data_table.py +++ b/tests/test_data_table.py @@ -556,9 +556,8 @@ async def test_coordinate_to_cell_key_invalid_coordinate(): table.coordinate_to_cell_key(Coordinate(9999, 9999)) -def make_click_event(sender: MessagePump): +def make_click_event(): return Click( - sender=sender, x=1, y=2, delta_x=0, @@ -577,7 +576,7 @@ async def test_datatable_on_click_cell_cursor(): app = DataTableApp() async with app.run_test() as pilot: table = app.query_one(DataTable) - click = make_click_event(app) + click = make_click_event() column_key = table.add_column("ABC") table.add_row("123") row_key = table.add_row("456") @@ -591,13 +590,11 @@ async def test_datatable_on_click_cell_cursor(): "CellSelected", ] cell_highlighted_event: DataTable.CellHighlighted = app.messages[1] - assert cell_highlighted_event.sender is table assert cell_highlighted_event.value == "456" assert cell_highlighted_event.cell_key == CellKey(row_key, column_key) assert cell_highlighted_event.coordinate == Coordinate(1, 0) cell_selected_event: DataTable.CellSelected = app.messages[2] - assert cell_selected_event.sender is table assert cell_selected_event.value == "456" assert cell_selected_event.cell_key == CellKey(row_key, column_key) assert cell_selected_event.coordinate == Coordinate(1, 0) From 2702b7f0823dcc0aa91d5bb0f8af7cf909e6586a Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 5 Mar 2023 09:50:01 +0000 Subject: [PATCH 02/18] removed priority post --- src/textual/_types.py | 3 --- src/textual/drivers/headless_driver.py | 2 +- src/textual/drivers/linux_driver.py | 2 +- src/textual/events.py | 4 ++-- src/textual/message_pump.py | 25 ++----------------------- src/textual/screen.py | 15 +++++++-------- 6 files changed, 13 insertions(+), 38 deletions(-) diff --git a/src/textual/_types.py b/src/textual/_types.py index ca1b52f1bf..6d543d0ffc 100644 --- a/src/textual/_types.py +++ b/src/textual/_types.py @@ -11,9 +11,6 @@ class MessageTarget(Protocol): async def post_message(self, message: "Message") -> bool: ... - async def _post_priority_message(self, message: "Message") -> bool: - ... - def post_message_no_wait(self, message: "Message") -> bool: ... diff --git a/src/textual/drivers/headless_driver.py b/src/textual/drivers/headless_driver.py index 75172b9b6c..4c4b1b0d05 100644 --- a/src/textual/drivers/headless_driver.py +++ b/src/textual/drivers/headless_driver.py @@ -39,7 +39,7 @@ def send_size_event(): terminal_size = self._get_terminal_size() width, height = terminal_size textual_size = Size(width, height) - event = events.Resize(self._target, textual_size, textual_size) + event = events.Resize(textual_size, textual_size) asyncio.run_coroutine_threadsafe( self._target.post_message(event), loop=loop, diff --git a/src/textual/drivers/linux_driver.py b/src/textual/drivers/linux_driver.py index 4686cba05d..6b70cfbcd9 100644 --- a/src/textual/drivers/linux_driver.py +++ b/src/textual/drivers/linux_driver.py @@ -97,7 +97,7 @@ def send_size_event(): terminal_size = self._get_terminal_size() width, height = terminal_size textual_size = Size(width, height) - event = events.Resize(self._target, textual_size, textual_size) + event = events.Resize(textual_size, textual_size) asyncio.run_coroutine_threadsafe( self._target.post_message(event), loop=loop, diff --git a/src/textual/events.py b/src/textual/events.py index dcbb43ed9c..695912f675 100644 --- a/src/textual/events.py +++ b/src/textual/events.py @@ -1,11 +1,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Awaitable, Callable, Type, TypeVar +from typing import TYPE_CHECKING, Type, TypeVar import rich.repr from rich.style import Style -from ._types import CallbackType, MessageTarget +from ._types import CallbackType from .geometry import Offset, Size from .keys import _get_key_aliases from .message import Message diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 54e117035f..500e23bd6a 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -287,7 +287,6 @@ def set_timer( timer = Timer( self, delay, - self, name=name or f"set_timer#{Timer._timer_count}", callback=callback, repeat=0, @@ -614,28 +613,6 @@ async def post_message(self, message: Message) -> bool: await self._message_queue.put(message) return True - # TODO: This may not be needed, or may only be needed by the timer - # Consider removing or making private - async def _post_priority_message(self, message: Message) -> bool: - """Post a "priority" messages which will be processes prior to regular messages. - - Note that you should rarely need this in a regular app. It exists primarily to allow - timer messages to skip the queue, so that they can be more regular. - - Args: - message: A message. - - Returns: - True if the messages was processed, False if it wasn't. - """ - # TODO: Allow priority messages to jump the queue - if self._closing or self._closed: - return False - if not self.check_message_enabled(message): - return False - await self._message_queue.put(message) - return True - def post_message_no_wait(self, message: Message) -> bool: """Posts a message on the queue. @@ -649,6 +626,8 @@ def post_message_no_wait(self, message: Message) -> bool: return False if not self.check_message_enabled(message): return False + if message._sender is None: + message._sender = self # Add a copy of the prevented message types to the message # This is so that prevented messages are honoured by the event's handler message._prevent.update(self._get_prevented_messages()) diff --git a/src/textual/screen.py b/src/textual/screen.py index c1bcd7fa9c..7ffce3e1ee 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -343,7 +343,7 @@ def set_focus(self, widget: Widget | None, scroll_visible: bool = True) -> None: # Send focus event if scroll_visible: self.screen.scroll_to_widget(widget) - widget.post_message_no_wait(events.Focus(self)) + widget.post_message_no_wait(events.Focus()) self.log.debug(widget, "was focused") async def _on_idle(self, event: events.Idle) -> None: @@ -381,7 +381,7 @@ def _on_timer_update(self) -> None: self.app._display(self, self._compositor.render()) self._dirty_widgets.clear() if self._callbacks: - self.post_message_no_wait(events.InvokeCallbacks(self)) + self.post_message_no_wait(events.InvokeCallbacks()) self.update_timer.pause() @@ -441,7 +441,7 @@ def _refresh_layout( ): widget.post_message_no_wait( ResizeEvent( - self, region.size, virtual_size, container_size + region.size, virtual_size, container_size ) ) @@ -451,7 +451,7 @@ def _refresh_layout( Show = events.Show for widget in hidden: - widget.post_message_no_wait(Hide(self)) + widget.post_message_no_wait(Hide()) # We want to send a resize event to widgets that were just added or change since last layout send_resize = shown | resized @@ -468,11 +468,11 @@ def _refresh_layout( widget._size_updated(region.size, virtual_size, container_size) if widget in send_resize: widget.post_message_no_wait( - ResizeEvent(self, region.size, virtual_size, container_size) + ResizeEvent(region.size, virtual_size, container_size) ) for widget in shown: - widget.post_message_no_wait(Show(self)) + widget.post_message_no_wait(Show()) except Exception as error: self.app._handle_exception(error) @@ -480,7 +480,7 @@ def _refresh_layout( display_update = self._compositor.render(full=full) self.app._display(self, display_update) if not self.app._dom_ready: - self.app.post_message_no_wait(events.Ready(self)) + self.app.post_message_no_wait(events.Ready()) self.app._dom_ready = True async def _on_update(self, message: messages.Update) -> None: @@ -528,7 +528,6 @@ async def _handle_mouse_move(self, event: events.MouseMove) -> None: else: await self.app._set_mouse_over(widget) mouse_event = events.MouseMove( - self, event.x - region.x, event.y - region.y, event.delta_x, From 4819c6d473553c15db09deef1484bcaa43b34584 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 5 Mar 2023 09:50:28 +0000 Subject: [PATCH 03/18] timer fix --- src/textual/timer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/timer.py b/src/textual/timer.py index b4ecbd34aa..6d44c1fa1d 100644 --- a/src/textual/timer.py +++ b/src/textual/timer.py @@ -172,4 +172,4 @@ async def _tick(self, *, next_timer: float, count: int) -> None: count=count, callback=self._callback, ) - await self.target._post_priority_message(event) + await self.target.post_message(event) From 0f8761b009cf9c4d7adee4dd5978a45f1fc302c9 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 5 Mar 2023 11:22:09 +0000 Subject: [PATCH 04/18] test fixes --- src/textual/_xterm_parser.py | 2 +- src/textual/app.py | 2 +- src/textual/drivers/linux_driver.py | 2 +- src/textual/drivers/win32.py | 2 +- src/textual/message.py | 6 ++++-- src/textual/message_pump.py | 17 +---------------- src/textual/messages.py | 1 - src/textual/scrollbar.py | 9 +++------ src/textual/widget.py | 6 ++---- src/textual/widgets/_data_table.py | 17 ++++++----------- src/textual/widgets/_directory_tree.py | 1 + src/textual/widgets/_list_item.py | 1 + src/textual/widgets/_radio_set.py | 10 +++++----- src/textual/widgets/_tree.py | 9 ++++----- tests/test_data_table.py | 15 +++------------ tests/test_message_pump.py | 6 +++--- tests/test_paste.py | 2 +- tests/test_xterm_parser.py | 4 +--- tests/toggles/test_checkbox.py | 2 +- tests/toggles/test_radiobutton.py | 2 +- tests/toggles/test_radioset.py | 4 ++-- 21 files changed, 43 insertions(+), 77 deletions(-) diff --git a/src/textual/_xterm_parser.py b/src/textual/_xterm_parser.py index e1b2169c15..fc42583905 100644 --- a/src/textual/_xterm_parser.py +++ b/src/textual/_xterm_parser.py @@ -112,7 +112,7 @@ def reissue_sequence_as_keys(reissue_sequence: str) -> None: # the full escape code was. pasted_text = "".join(paste_buffer[:-1]) # Note the removal of NUL characters: https://github.com/Textualize/textual/issues/1661 - on_token(events.Paste(text=pasted_text.replace("\x00", ""))) + on_token(events.Paste(pasted_text.replace("\x00", ""))) paste_buffer.clear() character = ESC if use_prior_escape else (yield read1()) diff --git a/src/textual/app.py b/src/textual/app.py index 68a7c4f986..74b4e565ee 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -878,7 +878,7 @@ async def _press_keys(self, keys: Iterable[str]) -> None: except KeyError: char = key if len(key) == 1 else None print(f"press {key!r} (char={char!r})") - key_event = events.Key(app, key, char) + key_event = events.Key(key, char) driver.send_event(key_event) await wait_for_idle(0) diff --git a/src/textual/drivers/linux_driver.py b/src/textual/drivers/linux_driver.py index 6b70cfbcd9..502de71edd 100644 --- a/src/textual/drivers/linux_driver.py +++ b/src/textual/drivers/linux_driver.py @@ -217,7 +217,7 @@ def more_data() -> bool: return True return False - parser = XTermParser(self._target, more_data, self._debug) + parser = XTermParser(more_data, self._debug) feed = parser.feed utf8_decoder = getincrementaldecoder("utf-8")().decode diff --git a/src/textual/drivers/win32.py b/src/textual/drivers/win32.py index 30a335bd98..37edb403c3 100644 --- a/src/textual/drivers/win32.py +++ b/src/textual/drivers/win32.py @@ -224,7 +224,7 @@ def __init__( def run(self) -> None: exit_requested = self.exit_event.is_set - parser = XTermParser(self.target, lambda: False) + parser = XTermParser(lambda: False) try: read_count = wintypes.DWORD(0) diff --git a/src/textual/message.py b/src/textual/message.py index fb38837264..dc11fed100 100644 --- a/src/textual/message.py +++ b/src/textual/message.py @@ -5,6 +5,7 @@ import rich.repr from . import _clock +from ._context import active_message_pump from ._types import MessageTarget as MessageTarget from .case import camel_to_snake @@ -17,6 +18,7 @@ class Message: """Base class for a message.""" __slots__ = [ + "_sender", "time", "_forwarded", "_no_default_action", @@ -30,8 +32,8 @@ class Message: no_dispatch: ClassVar[bool] = False # Message may not be handled by client code namespace: ClassVar[str] = "" # Namespace to disambiguate messages - def __init__(self) -> None: - self._sender: MessageTarget | None = None + def __init__(self, sender: MessageTarget | None = None) -> None: + self._sender: MessageTarget | None = sender or active_message_pump.get(None) self.time: float = _clock.get_time_no_wait() self._forwarded = False self._no_default_action = False diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 500e23bd6a..174007a406 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -579,7 +579,7 @@ async def _on_message(self, message: Message) -> None: # Bubble messages up the DOM (if enabled on the message) if message.bubble and self._parent and not message._stop_propagation: - if message.sender == self._parent: + if message._sender is not None and message._sender == self._parent: # parent is sender, so we stop propagation after parent message.stop() if self.is_parent_active and not self._parent._closing: @@ -600,13 +600,10 @@ async def post_message(self, message: Message) -> bool: True if the messages was posted successfully, False if the message was not posted (because the message pump was in the process of closing). """ - if self._closing or self._closed: return False if not self.check_message_enabled(message): return True - if message._sender is None: - message._sender = self # Add a copy of the prevented message types to the message # This is so that prevented messages are honoured by the event's handler message._prevent.update(self._get_prevented_messages()) @@ -626,24 +623,12 @@ def post_message_no_wait(self, message: Message) -> bool: return False if not self.check_message_enabled(message): return False - if message._sender is None: - message._sender = self # Add a copy of the prevented message types to the message # This is so that prevented messages are honoured by the event's handler message._prevent.update(self._get_prevented_messages()) self._message_queue.put_nowait(message) return True - async def _post_message_from_child(self, message: Message) -> bool: - if self._closing or self._closed: - return False - return await self.post_message(message) - - def _post_message_from_child_no_wait(self, message: Message) -> bool: - if self._closing or self._closed: - return False - return self.post_message_no_wait(message) - async def on_callback(self, event: events.Callback) -> None: await invoke(event.callback) diff --git a/src/textual/messages.py b/src/textual/messages.py index d849cb69d8..91a54b2811 100644 --- a/src/textual/messages.py +++ b/src/textual/messages.py @@ -9,7 +9,6 @@ from .message import Message if TYPE_CHECKING: - from .message_pump import MessagePump from .widget import Widget diff --git a/src/textual/scrollbar.py b/src/textual/scrollbar.py index 06b2db8277..1866d8ca0b 100644 --- a/src/textual/scrollbar.py +++ b/src/textual/scrollbar.py @@ -10,7 +10,6 @@ from rich.style import Style, StyleType from . import events -from ._types import MessageTarget from .geometry import Offset from .message import Message from .reactive import Reactive @@ -300,12 +299,10 @@ def _on_leave(self, event: events.Leave) -> None: self.mouse_over = False def action_scroll_down(self) -> None: - self.post_message_no_wait( - ScrollDown(self) if self.vertical else ScrollRight(self) - ) + self.post_message_no_wait(ScrollDown() if self.vertical else ScrollRight()) def action_scroll_up(self) -> None: - self.post_message_no_wait(ScrollUp(self) if self.vertical else ScrollLeft(self)) + self.post_message_no_wait(ScrollUp() if self.vertical else ScrollLeft()) def action_grab(self) -> None: self.capture_mouse() @@ -358,7 +355,7 @@ async def _on_mouse_move(self, event: events.MouseMove) -> None: * (virtual_size / self.window_size) ) ) - await self.post_message(ScrollTo(self, x=x, y=y)) + await self.post_message(ScrollTo(x=x, y=y)) event.stop() async def _on_click(self, event: events.Click) -> None: diff --git a/src/textual/widget.py b/src/textual/widget.py index bbdd06ac39..42cc442a0d 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -2588,8 +2588,6 @@ async def post_message(self, message: Message) -> bool: Returns: True if the message was posted, False if this widget was closed / closing. """ - if message._sender is None: - message._sender = self if not self.check_message_enabled(message): return True if not self.is_running: @@ -2731,12 +2729,12 @@ def _on_enter(self, event: events.Enter) -> None: def _on_focus(self, event: events.Focus) -> None: self.has_focus = True self.refresh() - self.post_message_no_wait(events.DescendantFocus(self)) + self.post_message_no_wait(events.DescendantFocus()) def _on_blur(self, event: events.Blur) -> None: self.has_focus = False self.refresh() - self.post_message_no_wait(events.DescendantBlur(self)) + self.post_message_no_wait(events.DescendantBlur()) def _on_descendant_blur(self, event: events.DescendantBlur) -> None: if self._has_focus_within: diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py index 7784c7b80b..ee5599c3e5 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -911,9 +911,7 @@ def _highlight_row(self, row_index: int) -> None: is_valid_row = row_index < len(self._data) if is_valid_row: row_key = self._row_locations.get_key(row_index) - self.post_message_no_wait( - DataTable.RowHighlighted(self, row_index, row_key) - ) + self.post_message_no_wait(DataTable.RowHighlighted(row_index, row_key)) def _highlight_column(self, column_index: int) -> None: """Apply highlighting to the column at the given index, and post event.""" @@ -921,7 +919,7 @@ def _highlight_column(self, column_index: int) -> None: if column_index < len(self.columns): column_key = self._column_locations.get_key(column_index) self.post_message_no_wait( - DataTable.ColumnHighlighted(self, column_index, column_key) + DataTable.ColumnHighlighted(column_index, column_key) ) def validate_cursor_coordinate(self, value: Coordinate) -> Coordinate: @@ -1819,14 +1817,12 @@ def on_click(self, event: events.Click) -> None: # Header clicks work even if cursor is off, and doesn't move the cursor. column = self.ordered_columns[column_index] message = DataTable.HeaderSelected( - self, column.key, column_index, label=column.label + column.key, column_index, label=column.label ) self.post_message_no_wait(message) elif is_row_label_click: row = self.ordered_rows[row_index] - message = DataTable.RowLabelSelected( - self, row.key, row_index, label=row.label - ) + message = DataTable.RowLabelSelected(row.key, row_index, label=row.label) self.post_message_no_wait(message) elif self.show_cursor and self.cursor_type != "none": # Only post selection events if there is a visible row/col/cell cursor. @@ -1886,7 +1882,6 @@ def _post_selected_message(self): if cursor_type == "cell": self.post_message_no_wait( DataTable.CellSelected( - self, self.get_cell_at(cursor_coordinate), coordinate=cursor_coordinate, cell_key=cell_key, @@ -1895,10 +1890,10 @@ def _post_selected_message(self): elif cursor_type == "row": row_index, _ = cursor_coordinate row_key, _ = cell_key - self.post_message_no_wait(DataTable.RowSelected(self, row_index, row_key)) + self.post_message_no_wait(DataTable.RowSelected(row_index, row_key)) elif cursor_type == "column": _, column_index = cursor_coordinate _, column_key = cell_key self.post_message_no_wait( - DataTable.ColumnSelected(self, column_index, column_key) + DataTable.ColumnSelected(column_index, column_key) ) diff --git a/src/textual/widgets/_directory_tree.py b/src/textual/widgets/_directory_tree.py index cc95dfb0bb..d09409e620 100644 --- a/src/textual/widgets/_directory_tree.py +++ b/src/textual/widgets/_directory_tree.py @@ -79,6 +79,7 @@ class FileSelected(Message, bubble=True): def __init__(self, path: str) -> None: self.path: str = path + super().__init__() def __init__( self, diff --git a/src/textual/widgets/_list_item.py b/src/textual/widgets/_list_item.py index a6567fec05..120593de54 100644 --- a/src/textual/widgets/_list_item.py +++ b/src/textual/widgets/_list_item.py @@ -45,6 +45,7 @@ class _ChildClicked(Message): def __init__(self, item: ListItem) -> None: self.item = item + super().__init__() def on_click(self, event: events.Click) -> None: self.post_message_no_wait(self._ChildClicked(self)) diff --git a/src/textual/widgets/_radio_set.py b/src/textual/widgets/_radio_set.py index 819382fea9..19455c5c2b 100644 --- a/src/textual/widgets/_radio_set.py +++ b/src/textual/widgets/_radio_set.py @@ -44,7 +44,7 @@ def __init__(self, radio_set: RadioSet, pressed: RadioButton) -> None: pressed: The radio button that was pressed. """ super().__init__() - self.input = radio_set # TODO: Why is this called "input ?" + self.radio_set = radio_set """A reference to the `RadioSet` that was changed.""" self.pressed = pressed """The `RadioButton` that was pressed to make the change.""" @@ -113,16 +113,16 @@ def on_radio_button_changed(self, event: RadioButton.Changed) -> None: event: The event. """ # If the button is changing to be the pressed button... - if event.input.value: + if event.toggle_button.value: # ...send off a message to say that the pressed state has # changed. self.post_message_no_wait( - self.Changed(self, cast(RadioButton, event.input)) + self.Changed(self, cast(RadioButton, event.toggle_button)) ) # ...then look for the button that was previously the pressed # one and unpress it. for button in self._buttons.filter(".-on"): - if button != event.input: + if button != event.toggle_button: button.value = False break else: @@ -133,7 +133,7 @@ def on_radio_button_changed(self, event: RadioButton.Changed) -> None: event.stop() if not self._buttons.filter(".-on"): with self.prevent(RadioButton.Changed): - event.input.value = True + event.toggle_button.value = True @property def pressed_button(self) -> RadioButton | None: diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index dd5ad8c58d..0159ddb6c4 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -14,7 +14,6 @@ from .._immutable_sequence_view import ImmutableSequenceView from .._loop import loop_last from .._segment_tools import line_pad -from .._types import MessageTarget from ..binding import Binding, BindingType from ..geometry import Region, Size, clamp from ..message import Message @@ -771,7 +770,7 @@ def watch_cursor_line(self, previous_line: int, line: int) -> None: node._selected = True self._cursor_node = node if previous_node != node: - self.post_message_no_wait(self.NodeHighlighted(self, node)) + self.post_message_no_wait(self.NodeHighlighted(node)) def watch_guide_depth(self, guide_depth: int) -> None: self._invalidate() @@ -1019,10 +1018,10 @@ def _toggle_node(self, node: TreeNode[TreeDataType]) -> None: return if node.is_expanded: node.collapse() - self.post_message_no_wait(self.NodeCollapsed(self, node)) + self.post_message_no_wait(self.NodeCollapsed(node)) else: node.expand() - self.post_message_no_wait(self.NodeExpanded(self, node)) + self.post_message_no_wait(self.NodeExpanded(node)) async def _on_click(self, event: events.Click) -> None: meta = event.style.meta @@ -1109,4 +1108,4 @@ def action_select_cursor(self) -> None: node = line.path[-1] if self.auto_expand: self._toggle_node(node) - self.post_message_no_wait(self.NodeSelected(self, node)) + self.post_message_no_wait(self.NodeSelected(node)) diff --git a/tests/test_data_table.py b/tests/test_data_table.py index d6129d1107..e9eb1c9fe6 100644 --- a/tests/test_data_table.py +++ b/tests/test_data_table.py @@ -10,7 +10,6 @@ from textual.coordinate import Coordinate from textual.events import Click, MouseMove from textual.message import Message -from textual.message_pump import MessagePump from textual.widgets import DataTable from textual.widgets.data_table import ( CellDoesNotExist, @@ -607,7 +606,7 @@ async def test_on_click_row_cursor(): async with app.run_test(): table = app.query_one(DataTable) table.cursor_type = "row" - click = make_click_event(app) + click = make_click_event() table.add_column("ABC") table.add_row("123") row_key = table.add_row("456") @@ -616,12 +615,11 @@ async def test_on_click_row_cursor(): assert app.message_names == ["RowHighlighted", "RowHighlighted", "RowSelected"] row_highlighted: DataTable.RowHighlighted = app.messages[1] - assert row_highlighted.sender is table + assert row_highlighted.row_key == row_key assert row_highlighted.cursor_row == 1 row_selected: DataTable.RowSelected = app.messages[2] - assert row_selected.sender is table assert row_selected.row_key == row_key assert row_highlighted.cursor_row == 1 @@ -636,7 +634,7 @@ async def test_on_click_column_cursor(): column_key = table.add_column("ABC") table.add_row("123") table.add_row("456") - click = make_click_event(app) + click = make_click_event() table.on_click(event=click) await wait_for_idle(0) assert app.message_names == [ @@ -645,12 +643,10 @@ async def test_on_click_column_cursor(): "ColumnSelected", ] column_highlighted: DataTable.ColumnHighlighted = app.messages[1] - assert column_highlighted.sender is table assert column_highlighted.column_key == column_key assert column_highlighted.cursor_column == 0 column_selected: DataTable.ColumnSelected = app.messages[2] - assert column_selected.sender is table assert column_selected.column_key == column_key assert column_highlighted.cursor_column == 0 @@ -666,7 +662,6 @@ async def test_hover_coordinate(): assert table.hover_coordinate == Coordinate(0, 0) mouse_move = MouseMove( - sender=app, x=1, y=2, delta_x=0, @@ -691,7 +686,6 @@ async def test_header_selected(): column_key = table.add_column("number") table.add_row(3) click_event = Click( - sender=table, x=3, y=0, delta_x=0, @@ -705,7 +699,6 @@ async def test_header_selected(): table.on_click(click_event) await pilot.pause() message: DataTable.HeaderSelected = app.messages[-1] - assert message.sender is table assert message.label == Text("number") assert message.column_index == 0 assert message.column_key == column_key @@ -726,7 +719,6 @@ async def test_row_label_selected(): table.add_column("number") row_key = table.add_row(3, label="A") click_event = Click( - sender=table, x=1, y=1, delta_x=0, @@ -740,7 +732,6 @@ async def test_row_label_selected(): table.on_click(click_event) await pilot.pause() message: DataTable.RowLabelSelected = app.messages[-1] - assert message.sender is table assert message.label == Text("A") assert message.row_index == 0 assert message.row_key == row_key diff --git a/tests/test_message_pump.py b/tests/test_message_pump.py index 9667b4cbc6..c02978e690 100644 --- a/tests/test_message_pump.py +++ b/tests/test_message_pump.py @@ -19,7 +19,7 @@ def key_ctrl_i(self): async def test_dispatch_key_valid_key(): widget = ValidWidget() - result = await widget.dispatch_key(Key(widget, key="x", character="x")) + result = await widget.dispatch_key(Key(key="x", character="x")) assert result is True assert widget.called_by == widget.key_x @@ -28,7 +28,7 @@ async def test_dispatch_key_valid_key_alias(): """When you press tab or ctrl+i, it comes through as a tab key event, but handlers for tab and ctrl+i are both considered valid.""" widget = ValidWidget() - result = await widget.dispatch_key(Key(widget, key="tab", character="\t")) + result = await widget.dispatch_key(Key(key="tab", character="\t")) assert result is True assert widget.called_by == widget.key_ctrl_i @@ -54,7 +54,7 @@ async def test_dispatch_key_raises_when_conflicting_handler_aliases(): In the terminal, they're the same thing, so we fail fast via exception here.""" widget = DuplicateHandlersWidget() with pytest.raises(DuplicateKeyHandlers): - await widget.dispatch_key(Key(widget, key="tab", character="\t")) + await widget.dispatch_key(Key(key="tab", character="\t")) assert widget.called_by == widget.key_tab diff --git a/tests/test_paste.py b/tests/test_paste.py index e8d7be34e6..dba5de7d13 100644 --- a/tests/test_paste.py +++ b/tests/test_paste.py @@ -11,7 +11,7 @@ def on_paste(self, event): app = PasteApp() async with app.run_test() as pilot: - await app.post_message(events.Paste(sender=app, text="Hello")) + await app.post_message(events.Paste(text="Hello")) await pilot.pause(0) assert len(paste_events) == 1 diff --git a/tests/test_xterm_parser.py b/tests/test_xterm_parser.py index 92ff1f31c7..2fbf46b291 100644 --- a/tests/test_xterm_parser.py +++ b/tests/test_xterm_parser.py @@ -34,7 +34,7 @@ def chunks(data, size): @pytest.fixture def parser(): - return XTermParser(sender=mock.sentinel, more_data=lambda: False) + return XTermParser(more_data=lambda: False) @pytest.mark.parametrize("chunk_size", [2, 3, 4, 5, 6]) @@ -65,7 +65,6 @@ def test_bracketed_paste(parser): assert len(events) == 1 assert isinstance(events[0], Paste) assert events[0].text == pasted_text - assert events[0].sender == mock.sentinel def test_bracketed_paste_content_contains_escape_codes(parser): @@ -302,7 +301,6 @@ def test_terminal_mode_reporting_synchronized_output_supported(parser): events = list(parser.feed(sequence)) assert len(events) == 1 assert isinstance(events[0], TerminalSupportsSynchronizedOutput) - assert events[0].sender == mock.sentinel def test_terminal_mode_reporting_synchronized_output_not_supported(parser): diff --git a/tests/toggles/test_checkbox.py b/tests/toggles/test_checkbox.py index 04ad687571..93b1fadac8 100644 --- a/tests/toggles/test_checkbox.py +++ b/tests/toggles/test_checkbox.py @@ -15,7 +15,7 @@ def compose(self) -> ComposeResult: yield Checkbox(value=True, id="cb3") def on_checkbox_changed(self, event: Checkbox.Changed) -> None: - self.events_received.append((event.input.id, event.input.value)) + self.events_received.append((event.toggle_button.id, event.toggle_button.value)) async def test_checkbox_initial_state() -> None: diff --git a/tests/toggles/test_radiobutton.py b/tests/toggles/test_radiobutton.py index a0a012ea01..9d5932eb4f 100644 --- a/tests/toggles/test_radiobutton.py +++ b/tests/toggles/test_radiobutton.py @@ -15,7 +15,7 @@ def compose(self) -> ComposeResult: yield RadioButton(value=True, id="rb3") def on_radio_button_changed(self, event: RadioButton.Changed) -> None: - self.events_received.append((event.input.id, event.input.value)) + self.events_received.append((event.toggle_button.id, event.toggle_button.value)) async def test_radio_button_initial_state() -> None: diff --git a/tests/toggles/test_radioset.py b/tests/toggles/test_radioset.py index 1a38fe7ffb..89bc0c80b5 100644 --- a/tests/toggles/test_radioset.py +++ b/tests/toggles/test_radioset.py @@ -19,9 +19,9 @@ def compose(self) -> ComposeResult: def on_radio_set_changed(self, event: RadioSet.Changed) -> None: self.events_received.append( ( - event.input.id, + event.radio_set.id, event.index, - [button.value for button in event.input.query(RadioButton)], + [button.value for button in event.radio_set.query(RadioButton)], ) ) From c43a9c52345ae028177cbb505e653cbf94c17205 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 5 Mar 2023 11:46:36 +0000 Subject: [PATCH 05/18] drop async version of post_message --- docs/examples/events/custom01.py | 4 +-- src/textual/_types.py | 8 ++--- src/textual/app.py | 36 ++++++++++----------- src/textual/driver.py | 2 +- src/textual/drivers/headless_driver.py | 2 +- src/textual/drivers/linux_driver.py | 2 +- src/textual/message.py | 4 +-- src/textual/message_pump.py | 24 ++++++-------- src/textual/reactive.py | 6 ++-- src/textual/screen.py | 44 ++++++++++++-------------- src/textual/scrollbar.py | 6 ++-- src/textual/widget.py | 20 ++++++------ src/textual/widgets/_button.py | 4 +-- src/textual/widgets/_data_table.py | 20 +++++------- src/textual/widgets/_directory_tree.py | 4 +-- src/textual/widgets/_input.py | 4 +-- src/textual/widgets/_list_item.py | 2 +- src/textual/widgets/_list_view.py | 6 ++-- src/textual/widgets/_markdown.py | 6 ++-- src/textual/widgets/_radio_set.py | 2 +- src/textual/widgets/_switch.py | 2 +- src/textual/widgets/_toggle_button.py | 2 +- src/textual/widgets/_tree.py | 8 ++--- tests/test_paste.py | 2 +- 24 files changed, 101 insertions(+), 119 deletions(-) diff --git a/docs/examples/events/custom01.py b/docs/examples/events/custom01.py index b850494db9..e632babd63 100644 --- a/docs/examples/events/custom01.py +++ b/docs/examples/events/custom01.py @@ -24,9 +24,9 @@ def on_mount(self) -> None: self.styles.background = Color.parse("#ffffff33") self.styles.border = ("tall", self.color) - async def on_click(self) -> None: + def on_click(self) -> None: # The post_message method sends an event to be handled in the DOM - await self.post_message(self.Selected(self.color)) + self.post_message(self.Selected(self.color)) def render(self) -> str: return str(self.color) diff --git a/src/textual/_types.py b/src/textual/_types.py index 6d543d0ffc..47c4cbdc6f 100644 --- a/src/textual/_types.py +++ b/src/textual/_types.py @@ -8,18 +8,18 @@ class MessageTarget(Protocol): - async def post_message(self, message: "Message") -> bool: + async def _post_message(self, message: "Message") -> bool: ... - def post_message_no_wait(self, message: "Message") -> bool: + def post_message(self, message: "Message") -> bool: ... class EventTarget(Protocol): - async def post_message(self, message: "Message") -> bool: + async def _post_message(self, message: "Message") -> bool: ... - def post_message_no_wait(self, message: "Message") -> bool: + def post_message(self, message: "Message") -> bool: ... diff --git a/src/textual/app.py b/src/textual/app.py index 74b4e565ee..705171d505 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -525,7 +525,7 @@ def exit( """ self._exit = True self._return_value = result - self.post_message_no_wait(messages.ExitApp()) + self.post_message(messages.ExitApp()) if message: self._exit_renderables.append(message) @@ -1272,7 +1272,7 @@ def _replace_screen(self, screen: Screen) -> Screen: The screen that was replaced. """ - screen.post_message_no_wait(events.ScreenSuspend()) + screen.post_message(events.ScreenSuspend()) self.log.system(f"{screen} SUSPENDED") if not self.is_screen_installed(screen) and screen not in self._screen_stack: screen.remove() @@ -1288,7 +1288,7 @@ def push_screen(self, screen: Screen | str) -> AwaitMount: """ next_screen, await_mount = self._get_screen(screen) self._screen_stack.append(next_screen) - self.screen.post_message_no_wait(events.ScreenResume()) + self.screen.post_message(events.ScreenResume()) self.log.system(f"{self.screen} is current (PUSHED)") return await_mount @@ -1303,7 +1303,7 @@ def switch_screen(self, screen: Screen | str) -> AwaitMount: self._replace_screen(self._screen_stack.pop()) next_screen, await_mount = self._get_screen(screen) self._screen_stack.append(next_screen) - self.screen.post_message_no_wait(events.ScreenResume()) + self.screen.post_message(events.ScreenResume()) self.log.system(f"{self.screen} is current (SWITCHED)") return await_mount return AwaitMount(self.screen, []) @@ -1382,7 +1382,7 @@ def pop_screen(self) -> Screen: ) previous_screen = self._replace_screen(screen_stack.pop()) self.screen._screen_resized(self.size) - self.screen.post_message_no_wait(events.ScreenResume()) + self.screen.post_message(events.ScreenResume()) self.log.system(f"{self.screen} is active") return previous_screen @@ -1395,7 +1395,7 @@ def set_focus(self, widget: Widget | None, scroll_visible: bool = True) -> None: """ self.screen.set_focus(widget, scroll_visible) - async def _set_mouse_over(self, widget: Widget | None) -> None: + def _set_mouse_over(self, widget: Widget | None) -> None: """Called when the mouse is over another widget. Args: @@ -1404,16 +1404,16 @@ async def _set_mouse_over(self, widget: Widget | None) -> None: if widget is None: if self.mouse_over is not None: try: - await self.mouse_over.post_message(events.Leave()) + self.mouse_over.post_message(events.Leave()) finally: self.mouse_over = None else: if self.mouse_over is not widget: try: if self.mouse_over is not None: - await self.mouse_over._forward_event(events.Leave()) + self.mouse_over._forward_event(events.Leave()) if widget is not None: - await widget._forward_event(events.Enter()) + widget._forward_event(events.Enter()) finally: self.mouse_over = widget @@ -1426,12 +1426,10 @@ def capture_mouse(self, widget: Widget | None) -> None: if widget == self.mouse_captured: return if self.mouse_captured is not None: - self.mouse_captured.post_message_no_wait( - events.MouseRelease(self.mouse_position) - ) + self.mouse_captured.post_message(events.MouseRelease(self.mouse_position)) self.mouse_captured = widget if widget is not None: - widget.post_message_no_wait(events.MouseCapture(self.mouse_position)) + widget.post_message(events.MouseCapture(self.mouse_position)) def panic(self, *renderables: RenderableType) -> None: """Exits the app then displays a message. @@ -1953,19 +1951,19 @@ async def on_event(self, event: events.Event) -> None: if isinstance(event, events.MouseEvent): # Record current mouse position on App self.mouse_position = Offset(event.x, event.y) - await self.screen._forward_event(event) + self.screen._forward_event(event) elif isinstance(event, events.Key): if not await self.check_bindings(event.key, priority=True): forward_target = self.focused or self.screen - await forward_target._forward_event(event) + forward_target._forward_event(event) else: - await self.screen._forward_event(event) + self.screen._forward_event(event) elif isinstance(event, events.Paste) and not event.is_forwarded: if self.focused is not None: - await self.focused._forward_event(event) + self.focused._forward_event(event) else: - await self.screen._forward_event(event) + self.screen._forward_event(event) else: await super().on_event(event) @@ -2092,7 +2090,7 @@ async def _on_shutdown_request(self, event: events.ShutdownRequest) -> None: async def _on_resize(self, event: events.Resize) -> None: event.stop() - await self.screen.post_message(event) + self.screen.post_message(event) def _detach_from_dom(self, widgets: list[Widget]) -> list[Widget]: """Detach a list of widgets from the DOM. diff --git a/src/textual/driver.py b/src/textual/driver.py index 5e470b6971..7707d736bc 100644 --- a/src/textual/driver.py +++ b/src/textual/driver.py @@ -34,7 +34,7 @@ def is_headless(self) -> bool: def send_event(self, event: events.Event) -> None: asyncio.run_coroutine_threadsafe( - self._target.post_message(event), loop=self._loop + self._target._post_message(event), loop=self._loop ) def process_event(self, event: events.Event) -> None: diff --git a/src/textual/drivers/headless_driver.py b/src/textual/drivers/headless_driver.py index 4c4b1b0d05..3362354666 100644 --- a/src/textual/drivers/headless_driver.py +++ b/src/textual/drivers/headless_driver.py @@ -41,7 +41,7 @@ def send_size_event(): textual_size = Size(width, height) event = events.Resize(textual_size, textual_size) asyncio.run_coroutine_threadsafe( - self._target.post_message(event), + self._target._post_message(event), loop=loop, ) diff --git a/src/textual/drivers/linux_driver.py b/src/textual/drivers/linux_driver.py index 502de71edd..c5fd1b368d 100644 --- a/src/textual/drivers/linux_driver.py +++ b/src/textual/drivers/linux_driver.py @@ -99,7 +99,7 @@ def send_size_event(): textual_size = Size(width, height) event = events.Resize(textual_size, textual_size) asyncio.run_coroutine_threadsafe( - self._target.post_message(event), + self._target._post_message(event), loop=loop, ) diff --git a/src/textual/message.py b/src/textual/message.py index dc11fed100..168a56b6b0 100644 --- a/src/textual/message.py +++ b/src/textual/message.py @@ -115,10 +115,10 @@ def stop(self, stop: bool = True) -> Message: self._stop_propagation = stop return self - async def _bubble_to(self, widget: MessagePump) -> None: + def _bubble_to(self, widget: MessagePump) -> None: """Bubble to a widget (typically the parent). Args: widget: Target of bubble. """ - await widget.post_message(self) + widget.post_message(self) diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 174007a406..a4590dd4e7 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -340,7 +340,7 @@ def call_after_refresh(self, callback: Callable, *args, **kwargs) -> None: # out anything already pending in our own queue. message = messages.InvokeLater(partial(callback, *args, **kwargs)) - self.post_message_no_wait(message) + self.post_message(message) def call_later(self, callback: Callable, *args, **kwargs) -> None: """Schedule a callback to run after all messages are processed in this object. @@ -352,7 +352,7 @@ def call_later(self, callback: Callable, *args, **kwargs) -> None: **kwargs: Keyword arguments to pass to the callable. """ message = events.Callback(callback=partial(callback, *args, **kwargs)) - self.post_message_no_wait(message) + self.post_message(message) def call_next(self, callback: Callable, *args, **kwargs) -> None: """Schedule a callback to run immediately after processing the current message. @@ -583,16 +583,18 @@ async def _on_message(self, message: Message) -> None: # parent is sender, so we stop propagation after parent message.stop() if self.is_parent_active and not self._parent._closing: - await message._bubble_to(self._parent) + message._bubble_to(self._parent) def check_idle(self) -> None: """Prompt the message pump to call idle if the queue is empty.""" if self._message_queue.empty(): - self.post_message_no_wait(messages.Prompt()) + self.post_message(messages.Prompt()) - async def post_message(self, message: Message) -> bool: + async def _post_message(self, message: Message) -> bool: """Post a message or an event to this message pump. + This is an internal method for use where a coroutine is required. + Args: message: A message object. @@ -600,17 +602,9 @@ async def post_message(self, message: Message) -> bool: True if the messages was posted successfully, False if the message was not posted (because the message pump was in the process of closing). """ - if self._closing or self._closed: - return False - if not self.check_message_enabled(message): - return True - # Add a copy of the prevented message types to the message - # This is so that prevented messages are honoured by the event's handler - message._prevent.update(self._get_prevented_messages()) - await self._message_queue.put(message) - return True + return self.post_message(message) - def post_message_no_wait(self, message: Message) -> bool: + def post_message(self, message: Message) -> bool: """Posts a message on the queue. Args: diff --git a/src/textual/reactive.py b/src/textual/reactive.py index f415325951..8209fe0249 100644 --- a/src/textual/reactive.py +++ b/src/textual/reactive.py @@ -210,9 +210,7 @@ async def await_watcher(awaitable: Awaitable) -> None: _rich_traceback_omit = True await awaitable # Watcher may have changed the state, so run compute again - obj.post_message_no_wait( - events.Callback(callback=partial(Reactive._compute, obj)) - ) + obj.post_message(events.Callback(callback=partial(Reactive._compute, obj))) def invoke_watcher( watch_function: Callable, old_value: object, value: object @@ -235,7 +233,7 @@ def invoke_watcher( watch_result = watch_function() if isawaitable(watch_result): # Result is awaitable, so we need to await it within an async context - obj.post_message_no_wait( + obj.post_message( events.Callback(callback=partial(await_watcher, watch_result)) ) diff --git a/src/textual/screen.py b/src/textual/screen.py index 7ffce3e1ee..679aee4a56 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -330,20 +330,20 @@ def set_focus(self, widget: Widget | None, scroll_visible: bool = True) -> None: if widget is None: # No focus, so blur currently focused widget if it exists if self.focused is not None: - self.focused.post_message_no_wait(events.Blur()) + self.focused.post_message(events.Blur()) self.focused = None self.log.debug("focus was removed") elif widget.focusable: if self.focused != widget: if self.focused is not None: # Blur currently focused widget - self.focused.post_message_no_wait(events.Blur()) + self.focused.post_message(events.Blur()) # Change focus self.focused = widget # Send focus event if scroll_visible: self.screen.scroll_to_widget(widget) - widget.post_message_no_wait(events.Focus()) + widget.post_message(events.Focus()) self.log.debug(widget, "was focused") async def _on_idle(self, event: events.Idle) -> None: @@ -381,7 +381,7 @@ def _on_timer_update(self) -> None: self.app._display(self, self._compositor.render()) self._dirty_widgets.clear() if self._callbacks: - self.post_message_no_wait(events.InvokeCallbacks()) + self.post_message(events.InvokeCallbacks()) self.update_timer.pause() @@ -439,7 +439,7 @@ def _refresh_layout( if widget._size_updated( region.size, virtual_size, container_size, layout=False ): - widget.post_message_no_wait( + widget.post_message( ResizeEvent( region.size, virtual_size, container_size ) @@ -451,7 +451,7 @@ def _refresh_layout( Show = events.Show for widget in hidden: - widget.post_message_no_wait(Hide()) + widget.post_message(Hide()) # We want to send a resize event to widgets that were just added or change since last layout send_resize = shown | resized @@ -467,12 +467,12 @@ def _refresh_layout( ) in layers: widget._size_updated(region.size, virtual_size, container_size) if widget in send_resize: - widget.post_message_no_wait( + widget.post_message( ResizeEvent(region.size, virtual_size, container_size) ) for widget in shown: - widget.post_message_no_wait(Show()) + widget.post_message(Show()) except Exception as error: self.app._handle_exception(error) @@ -480,7 +480,7 @@ def _refresh_layout( display_update = self._compositor.render(full=full) self.app._display(self, display_update) if not self.app._dom_ready: - self.app.post_message_no_wait(events.Ready()) + self.app.post_message(events.Ready()) self.app._dom_ready = True async def _on_update(self, message: messages.Update) -> None: @@ -516,7 +516,7 @@ async def _on_resize(self, event: events.Resize) -> None: event.stop() self._screen_resized(event.size) - async def _handle_mouse_move(self, event: events.MouseMove) -> None: + def _handle_mouse_move(self, event: events.MouseMove) -> None: try: if self.app.mouse_captured: widget = self.app.mouse_captured @@ -524,9 +524,9 @@ async def _handle_mouse_move(self, event: events.MouseMove) -> None: else: widget, region = self.get_widget_at(event.x, event.y) except errors.NoWidget: - await self.app._set_mouse_over(None) + self.app._set_mouse_over(None) else: - await self.app._set_mouse_over(widget) + self.app._set_mouse_over(widget) mouse_event = events.MouseMove( event.x - region.x, event.y - region.y, @@ -542,18 +542,18 @@ async def _handle_mouse_move(self, event: events.MouseMove) -> None: ) widget.hover_style = event.style mouse_event._set_forwarded() - await widget._forward_event(mouse_event) + widget._forward_event(mouse_event) - async def _forward_event(self, event: events.Event) -> None: + def _forward_event(self, event: events.Event) -> None: if event.is_forwarded: return event._set_forwarded() if isinstance(event, (events.Enter, events.Leave)): - await self.post_message(event) + self.post_message(event) elif isinstance(event, events.MouseMove): event.style = self.get_style_at(event.screen_x, event.screen_y) - await self._handle_mouse_move(event) + self._handle_mouse_move(event) elif isinstance(event, events.MouseEvent): try: @@ -573,11 +573,9 @@ async def _forward_event(self, event: events.Event) -> None: event.style = self.get_style_at(event.screen_x, event.screen_y) if widget is self: event._set_forwarded() - await self.post_message(event) + self.post_message(event) else: - await widget._forward_event( - event._apply_offset(-region.x, -region.y) - ) + widget._forward_event(event._apply_offset(-region.x, -region.y)) elif isinstance(event, (events.MouseScrollDown, events.MouseScrollUp)): try: @@ -587,8 +585,8 @@ async def _forward_event(self, event: events.Event) -> None: scroll_widget = widget if scroll_widget is not None: if scroll_widget is self: - await self.post_message(event) + self.post_message(event) else: - await scroll_widget._forward_event(event) + scroll_widget._forward_event(event) else: - await self.post_message(event) + self.post_message(event) diff --git a/src/textual/scrollbar.py b/src/textual/scrollbar.py index 1866d8ca0b..bc2ed6dd7c 100644 --- a/src/textual/scrollbar.py +++ b/src/textual/scrollbar.py @@ -299,10 +299,10 @@ def _on_leave(self, event: events.Leave) -> None: self.mouse_over = False def action_scroll_down(self) -> None: - self.post_message_no_wait(ScrollDown() if self.vertical else ScrollRight()) + self.post_message(ScrollDown() if self.vertical else ScrollRight()) def action_scroll_up(self) -> None: - self.post_message_no_wait(ScrollUp() if self.vertical else ScrollLeft()) + self.post_message(ScrollUp() if self.vertical else ScrollLeft()) def action_grab(self) -> None: self.capture_mouse() @@ -355,7 +355,7 @@ async def _on_mouse_move(self, event: events.MouseMove) -> None: * (virtual_size / self.window_size) ) ) - await self.post_message(ScrollTo(x=x, y=y)) + self.post_message(ScrollTo(x=x, y=y)) event.stop() async def _on_click(self, event: events.Click) -> None: diff --git a/src/textual/widget.py b/src/textual/widget.py index 42cc442a0d..d283964248 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -2491,9 +2491,9 @@ def get_style_at(self, x: int, y: int) -> Style: return Style() return self.screen.get_style_at(*screen_offset) - async def _forward_event(self, event: events.Event) -> None: + def _forward_event(self, event: events.Event) -> None: event._set_forwarded() - await self.post_message(event) + self.post_message(event) def _refresh_scroll(self) -> None: """Refreshes the scroll position.""" @@ -2579,7 +2579,7 @@ async def action(self, action: str) -> None: """ await self.app.action(action, self) - async def post_message(self, message: Message) -> bool: + def post_message(self, message: Message) -> bool: """Post a message to this widget. Args: @@ -2588,11 +2588,9 @@ async def post_message(self, message: Message) -> bool: Returns: True if the message was posted, False if this widget was closed / closing. """ - if not self.check_message_enabled(message): - return True if not self.is_running: self.log.warning(self, f"IS NOT RUNNING, {message!r} not sent") - return await super().post_message(message) + return super().post_message(message) async def _on_idle(self, event: events.Idle) -> None: """Called when there are no more events on the queue. @@ -2608,13 +2606,13 @@ async def _on_idle(self, event: events.Idle) -> None: else: if self._scroll_required: self._scroll_required = False - screen.post_message_no_wait(messages.UpdateScroll()) + screen.post_message(messages.UpdateScroll()) if self._repaint_required: self._repaint_required = False - screen.post_message_no_wait(messages.Update(self)) + screen.post_message(messages.Update(self)) if self._layout_required: self._layout_required = False - screen.post_message_no_wait(messages.Layout()) + screen.post_message(messages.Layout()) def focus(self, scroll_visible: bool = True) -> None: """Give focus to this widget. @@ -2729,12 +2727,12 @@ def _on_enter(self, event: events.Enter) -> None: def _on_focus(self, event: events.Focus) -> None: self.has_focus = True self.refresh() - self.post_message_no_wait(events.DescendantFocus()) + self.post_message(events.DescendantFocus()) def _on_blur(self, event: events.Blur) -> None: self.has_focus = False self.refresh() - self.post_message_no_wait(events.DescendantBlur()) + self.post_message(events.DescendantBlur()) def _on_descendant_blur(self, event: events.DescendantBlur) -> None: if self._has_focus_within: diff --git a/src/textual/widgets/_button.py b/src/textual/widgets/_button.py index c9e4be6bf0..bebc1bf4a1 100644 --- a/src/textual/widgets/_button.py +++ b/src/textual/widgets/_button.py @@ -235,7 +235,7 @@ def press(self) -> None: # Manage the "active" effect: self._start_active_affect() # ...and let other components know that we've just been clicked: - self.post_message_no_wait(Button.Pressed(self)) + self.post_message(Button.Pressed(self)) def _start_active_affect(self) -> None: """Start a small animation to show the button was clicked.""" @@ -247,7 +247,7 @@ def _start_active_affect(self) -> None: async def _on_key(self, event: events.Key) -> None: if event.key == "enter" and not self.disabled: self._start_active_affect() - await self.post_message(Button.Pressed(self)) + self.post_message(Button.Pressed(self)) @classmethod def success( diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py index ee5599c3e5..8eac31f429 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -880,7 +880,7 @@ def _highlight_coordinate(self, coordinate: Coordinate) -> None: return else: cell_key = self.coordinate_to_cell_key(coordinate) - self.post_message_no_wait( + self.post_message( DataTable.CellHighlighted( cell_value, coordinate=coordinate, cell_key=cell_key ) @@ -911,16 +911,14 @@ def _highlight_row(self, row_index: int) -> None: is_valid_row = row_index < len(self._data) if is_valid_row: row_key = self._row_locations.get_key(row_index) - self.post_message_no_wait(DataTable.RowHighlighted(row_index, row_key)) + self.post_message(DataTable.RowHighlighted(row_index, row_key)) def _highlight_column(self, column_index: int) -> None: """Apply highlighting to the column at the given index, and post event.""" self.refresh_column(column_index) if column_index < len(self.columns): column_key = self._column_locations.get_key(column_index) - self.post_message_no_wait( - DataTable.ColumnHighlighted(column_index, column_key) - ) + self.post_message(DataTable.ColumnHighlighted(column_index, column_key)) def validate_cursor_coordinate(self, value: Coordinate) -> Coordinate: return self._clamp_cursor_coordinate(value) @@ -1819,11 +1817,11 @@ def on_click(self, event: events.Click) -> None: message = DataTable.HeaderSelected( column.key, column_index, label=column.label ) - self.post_message_no_wait(message) + self.post_message(message) elif is_row_label_click: row = self.ordered_rows[row_index] message = DataTable.RowLabelSelected(row.key, row_index, label=row.label) - self.post_message_no_wait(message) + self.post_message(message) elif self.show_cursor and self.cursor_type != "none": # Only post selection events if there is a visible row/col/cell cursor. self.cursor_coordinate = Coordinate(row_index, column_index) @@ -1880,7 +1878,7 @@ def _post_selected_message(self): cursor_type = self.cursor_type cell_key = self.coordinate_to_cell_key(cursor_coordinate) if cursor_type == "cell": - self.post_message_no_wait( + self.post_message( DataTable.CellSelected( self.get_cell_at(cursor_coordinate), coordinate=cursor_coordinate, @@ -1890,10 +1888,8 @@ def _post_selected_message(self): elif cursor_type == "row": row_index, _ = cursor_coordinate row_key, _ = cell_key - self.post_message_no_wait(DataTable.RowSelected(row_index, row_key)) + self.post_message(DataTable.RowSelected(row_index, row_key)) elif cursor_type == "column": _, column_index = cursor_coordinate _, column_key = cell_key - self.post_message_no_wait( - DataTable.ColumnSelected(column_index, column_key) - ) + self.post_message(DataTable.ColumnSelected(column_index, column_key)) diff --git a/src/textual/widgets/_directory_tree.py b/src/textual/widgets/_directory_tree.py index d09409e620..d16d513090 100644 --- a/src/textual/widgets/_directory_tree.py +++ b/src/textual/widgets/_directory_tree.py @@ -176,7 +176,7 @@ def on_tree_node_expanded(self, event: Tree.NodeSelected) -> None: if not dir_entry.loaded: self.load_directory(event.node) else: - self.post_message_no_wait(self.FileSelected(self, dir_entry.path)) + self.post_message(self.FileSelected(dir_entry.path)) def on_tree_node_selected(self, event: Tree.NodeSelected) -> None: event.stop() @@ -184,4 +184,4 @@ def on_tree_node_selected(self, event: Tree.NodeSelected) -> None: if dir_entry is None: return if not dir_entry.is_dir: - self.post_message_no_wait(self.FileSelected(self, dir_entry.path)) + self.post_message(self.FileSelected(dir_entry.path)) diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index 9372e61c8d..74c0d50bea 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -243,7 +243,7 @@ def watch_cursor_position(self, cursor_position: int) -> None: async def watch_value(self, value: str) -> None: if self.styles.auto_dimensions: self.refresh(layout=True) - await self.post_message(self.Changed(self, value)) + self.post_message(self.Changed(self, value)) @property def cursor_width(self) -> int: @@ -479,4 +479,4 @@ def action_delete_left_all(self) -> None: async def action_submit(self) -> None: """Handle a submit action (normally the user hitting Enter in the input).""" - await self.post_message(self.Submitted(self, self.value)) + self.post_message(self.Submitted(self, self.value)) diff --git a/src/textual/widgets/_list_item.py b/src/textual/widgets/_list_item.py index 120593de54..75a1be7e9e 100644 --- a/src/textual/widgets/_list_item.py +++ b/src/textual/widgets/_list_item.py @@ -48,7 +48,7 @@ def __init__(self, item: ListItem) -> None: super().__init__() def on_click(self, event: events.Click) -> None: - self.post_message_no_wait(self._ChildClicked(self)) + self.post_message(self._ChildClicked(self)) def watch_highlighted(self, value: bool) -> None: self.set_class(value, "--highlight") diff --git a/src/textual/widgets/_list_view.py b/src/textual/widgets/_list_view.py index 53b1250415..013c51ead6 100644 --- a/src/textual/widgets/_list_view.py +++ b/src/textual/widgets/_list_view.py @@ -145,7 +145,7 @@ def watch_index(self, old_index: int, new_index: int) -> None: new_child = None self._scroll_highlighted_region() - self.post_message_no_wait(self.Highlighted(self, new_child)) + self.post_message(self.Highlighted(self, new_child)) def append(self, item: ListItem) -> AwaitMount: """Append a new ListItem to the end of the ListView. @@ -178,7 +178,7 @@ def action_select_cursor(self) -> None: selected_child = self.highlighted_child if selected_child is None: return - self.post_message_no_wait(self.Selected(self, selected_child)) + self.post_message(self.Selected(self, selected_child)) def action_cursor_down(self) -> None: """Highlight the next item in the list.""" @@ -197,7 +197,7 @@ def action_cursor_up(self) -> None: def on_list_item__child_clicked(self, event: ListItem._ChildClicked) -> None: self.focus() self.index = self._nodes.index(event.item) - self.post_message_no_wait(self.Selected(self, event.item)) + self.post_message(self.Selected(self, event.item)) def _scroll_highlighted_region(self) -> None: """Used to keep the highlighted index within vision""" diff --git a/src/textual/widgets/_markdown.py b/src/textual/widgets/_markdown.py index c8203a5a23..f352683f7c 100644 --- a/src/textual/widgets/_markdown.py +++ b/src/textual/widgets/_markdown.py @@ -100,7 +100,7 @@ def set_content(self, text: Text) -> None: async def action_link(self, href: str) -> None: """Called on link click.""" - await self.post_message(Markdown.LinkClicked(href)) + self.post_message(Markdown.LinkClicked(href)) class MarkdownHeader(MarkdownBlock): @@ -700,7 +700,7 @@ async def update(self, markdown: str) -> None: ) ) - await self.post_message(Markdown.TableOfContentsUpdated(table_of_contents)) + self.post_message(Markdown.TableOfContentsUpdated(table_of_contents)) with self.app.batch_update(): await self.query("MarkdownBlock").remove() await self.mount_all(output) @@ -756,7 +756,7 @@ def set_table_of_contents(self, table_of_contents: TableOfContentsType) -> None: async def on_tree_node_selected(self, message: Tree.NodeSelected) -> None: node_data = message.node.data if node_data is not None: - await self.post_message( + await self._post_message( Markdown.TableOfContentsSelected(node_data["block_id"]) ) diff --git a/src/textual/widgets/_radio_set.py b/src/textual/widgets/_radio_set.py index 19455c5c2b..cd0ef59d53 100644 --- a/src/textual/widgets/_radio_set.py +++ b/src/textual/widgets/_radio_set.py @@ -116,7 +116,7 @@ def on_radio_button_changed(self, event: RadioButton.Changed) -> None: if event.toggle_button.value: # ...send off a message to say that the pressed state has # changed. - self.post_message_no_wait( + self.post_message( self.Changed(self, cast(RadioButton, event.toggle_button)) ) # ...then look for the button that was previously the pressed diff --git a/src/textual/widgets/_switch.py b/src/textual/widgets/_switch.py index e5dc09a752..a31e1e258b 100644 --- a/src/textual/widgets/_switch.py +++ b/src/textual/widgets/_switch.py @@ -124,7 +124,7 @@ def watch_value(self, value: bool) -> None: self.animate("slider_pos", target_slider_pos, duration=0.3) else: self.slider_pos = target_slider_pos - self.post_message_no_wait(self.Changed(self, self.value)) + self.post_message(self.Changed(self, self.value)) def watch_slider_pos(self, slider_pos: float) -> None: self.set_class(slider_pos == 1, "-on") diff --git a/src/textual/widgets/_toggle_button.py b/src/textual/widgets/_toggle_button.py index ef8b2df11e..ce2b2b44cc 100644 --- a/src/textual/widgets/_toggle_button.py +++ b/src/textual/widgets/_toggle_button.py @@ -239,4 +239,4 @@ def watch_value(self) -> None: `False`. Subsequently a related `Changed` event will be posted. """ self.set_class(self.value, "-on") - self.post_message_no_wait(self.Changed(self, self.value)) + self.post_message(self.Changed(self, self.value)) diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index 0159ddb6c4..e5a9959312 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -770,7 +770,7 @@ def watch_cursor_line(self, previous_line: int, line: int) -> None: node._selected = True self._cursor_node = node if previous_node != node: - self.post_message_no_wait(self.NodeHighlighted(node)) + self.post_message(self.NodeHighlighted(node)) def watch_guide_depth(self, guide_depth: int) -> None: self._invalidate() @@ -1018,10 +1018,10 @@ def _toggle_node(self, node: TreeNode[TreeDataType]) -> None: return if node.is_expanded: node.collapse() - self.post_message_no_wait(self.NodeCollapsed(node)) + self.post_message(self.NodeCollapsed(node)) else: node.expand() - self.post_message_no_wait(self.NodeExpanded(node)) + self.post_message(self.NodeExpanded(node)) async def _on_click(self, event: events.Click) -> None: meta = event.style.meta @@ -1108,4 +1108,4 @@ def action_select_cursor(self) -> None: node = line.path[-1] if self.auto_expand: self._toggle_node(node) - self.post_message_no_wait(self.NodeSelected(node)) + self.post_message(self.NodeSelected(node)) diff --git a/tests/test_paste.py b/tests/test_paste.py index dba5de7d13..774ad50386 100644 --- a/tests/test_paste.py +++ b/tests/test_paste.py @@ -11,7 +11,7 @@ def on_paste(self, event): app = PasteApp() async with app.run_test() as pilot: - await app.post_message(events.Paste(text="Hello")) + app.post_message(events.Paste(text="Hello")) await pilot.pause(0) assert len(paste_events) == 1 From 549d9705d37d619932bfbae2ec742ecdc9543618 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 5 Mar 2023 12:05:39 +0000 Subject: [PATCH 06/18] extended docs --- docs/guide/events.md | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/docs/guide/events.md b/docs/guide/events.md index 09f9a7043c..8c8c7874a8 100644 --- a/docs/guide/events.md +++ b/docs/guide/events.md @@ -107,16 +107,11 @@ The message class is defined within the widget class itself. This is not strictl - It reduces the amount of imports. If you import `ColorButton`, you have access to the message class via `ColorButton.Selected`. - It creates a namespace for the handler. So rather than `on_selected`, the handler name becomes `on_color_button_selected`. This makes it less likely that your chosen name will clash with another message. +### Sending messages -## Sending messages - -In the previous example we used [post_message()][textual.message_pump.MessagePump.post_message] to send an event to its parent. We could also have used [post_message_no_wait()][textual.message_pump.MessagePump.post_message_no_wait] for non async code. Sending messages in this way allows you to write custom widgets without needing to know in what context they will be used. - -There are other ways of sending (posting) messages, which you may need to use less frequently. - -- [post_message][textual.message_pump.MessagePump.post_message] To post a message to a particular widget. -- [post_message_no_wait][textual.message_pump.MessagePump.post_message_no_wait] The non-async version of `post_message`. +To send a message call the [post_message()][textual.message_pump.MessagePump.post_message] method. This will place a message on the widget's message queue and run any message handlers. +It is common for widgets to send messages to themselves, and allow them to bubble. This is so a base class has an opportunity to handle the message. We do this in the example above, which means a subclass could add a `on_color_button_selected` if it wanted to handle the message itself. ## Preventing messages From cf7eb05e89b6fec3dedb29e0e48666b2803755d4 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 5 Mar 2023 12:16:08 +0000 Subject: [PATCH 07/18] fix no app --- src/textual/widget.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/textual/widget.py b/src/textual/widget.py index d283964248..a2560830f5 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -42,7 +42,7 @@ from ._asyncio import create_task from ._cache import FIFOCache from ._compose import compose -from ._context import active_app +from ._context import NoActiveAppError, active_app from ._easing import DEFAULT_SCROLL_EASING from ._layout import Layout from ._segment_tools import align_lines @@ -2588,8 +2588,12 @@ def post_message(self, message: Message) -> bool: Returns: True if the message was posted, False if this widget was closed / closing. """ + if not self.is_running: - self.log.warning(self, f"IS NOT RUNNING, {message!r} not sent") + try: + self.log.warning(self, f"IS NOT RUNNING, {message!r} not sent") + except NoActiveAppError: + pass return super().post_message(message) async def _on_idle(self, event: events.Idle) -> None: From a2fb0148a497740ac4b5086a4f9502588c287d3a Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 5 Mar 2023 12:25:53 +0000 Subject: [PATCH 08/18] Added control properties --- src/textual/widgets/_button.py | 5 ++ src/textual/widgets/_data_table.py | 95 +++++++++++++++++++++++---- src/textual/widgets/_input.py | 10 +++ src/textual/widgets/_switch.py | 5 ++ src/textual/widgets/_toggle_button.py | 5 ++ 5 files changed, 109 insertions(+), 11 deletions(-) diff --git a/src/textual/widgets/_button.py b/src/textual/widgets/_button.py index bebc1bf4a1..34a82a195c 100644 --- a/src/textual/widgets/_button.py +++ b/src/textual/widgets/_button.py @@ -169,6 +169,11 @@ def __init__(self, button: Button) -> None: self.button = button super().__init__() + @property + def control(self) -> Button: + """Alias for the button.""" + return self.button + def __init__( self, label: TextType | None = None, diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py index 8eac31f429..9759989d3d 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -319,10 +319,13 @@ class CellHighlighted(Message, bubble=True): def __init__( self, + data_table: DataTable, value: CellType, coordinate: Coordinate, cell_key: CellKey, ) -> None: + self.data_table = data_table + """The data table.""" self.value: CellType = value """The value in the highlighted cell.""" self.coordinate: Coordinate = coordinate @@ -336,6 +339,11 @@ def __rich_repr__(self) -> rich.repr.Result: yield "coordinate", self.coordinate yield "cell_key", self.cell_key + @property + def control(self) -> DataTable: + """Alias for the data table.""" + return self.data_table + class CellSelected(Message, bubble=True): """Posted by the `DataTable` widget when a cell is selected. @@ -346,10 +354,13 @@ class CellSelected(Message, bubble=True): def __init__( self, + data_table: DataTable, value: CellType, coordinate: Coordinate, cell_key: CellKey, ) -> None: + self.data_table = data_table + """The data table.""" self.value: CellType = value """The value in the cell that was selected.""" self.coordinate: Coordinate = coordinate @@ -363,6 +374,11 @@ def __rich_repr__(self) -> rich.repr.Result: yield "coordinate", self.coordinate yield "cell_key", self.cell_key + @property + def control(self) -> DataTable: + """Alias for the data table.""" + return self.data_table + class RowHighlighted(Message, bubble=True): """Posted when a row is highlighted. @@ -372,7 +388,11 @@ class RowHighlighted(Message, bubble=True): widget in the DOM. """ - def __init__(self, cursor_row: int, row_key: RowKey) -> None: + def __init__( + self, data_table: DataTable, cursor_row: int, row_key: RowKey + ) -> None: + self.data_table = data_table + """The data table.""" self.cursor_row: int = cursor_row """The y-coordinate of the cursor that highlighted the row.""" self.row_key: RowKey = row_key @@ -383,6 +403,11 @@ def __rich_repr__(self) -> rich.repr.Result: yield "cursor_row", self.cursor_row yield "row_key", self.row_key + @property + def control(self) -> DataTable: + """Alias for the data table.""" + return self.data_table + class RowSelected(Message, bubble=True): """Posted when a row is selected. @@ -392,7 +417,11 @@ class RowSelected(Message, bubble=True): widget in the DOM. """ - def __init__(self, cursor_row: int, row_key: RowKey) -> None: + def __init__( + self, data_table: DataTable, cursor_row: int, row_key: RowKey + ) -> None: + self.data_table = data_table + """The data table.""" self.cursor_row: int = cursor_row """The y-coordinate of the cursor that made the selection.""" self.row_key: RowKey = row_key @@ -403,6 +432,11 @@ def __rich_repr__(self) -> rich.repr.Result: yield "cursor_row", self.cursor_row yield "row_key", self.row_key + @property + def control(self) -> DataTable: + """Alias for the data table.""" + return self.data_table + class ColumnHighlighted(Message, bubble=True): """Posted when a column is highlighted. @@ -412,7 +446,11 @@ class ColumnHighlighted(Message, bubble=True): widget in the DOM. """ - def __init__(self, cursor_column: int, column_key: ColumnKey) -> None: + def __init__( + self, data_table: DataTable, cursor_column: int, column_key: ColumnKey + ) -> None: + self.data_table = data_table + """The data table.""" self.cursor_column: int = cursor_column """The x-coordinate of the column that was highlighted.""" self.column_key = column_key @@ -423,6 +461,11 @@ def __rich_repr__(self) -> rich.repr.Result: yield "cursor_column", self.cursor_column yield "column_key", self.column_key + @property + def control(self) -> DataTable: + """Alias for the data table.""" + return self.data_table + class ColumnSelected(Message, bubble=True): """Posted when a column is selected. @@ -432,7 +475,11 @@ class ColumnSelected(Message, bubble=True): widget in the DOM. """ - def __init__(self, cursor_column: int, column_key: ColumnKey) -> None: + def __init__( + self, data_table: DataTable, cursor_column: int, column_key: ColumnKey + ) -> None: + self.data_table = data_table + """The data table.""" self.cursor_column: int = cursor_column """The x-coordinate of the column that was selected.""" self.column_key = column_key @@ -443,15 +490,23 @@ def __rich_repr__(self) -> rich.repr.Result: yield "cursor_column", self.cursor_column yield "column_key", self.column_key + @property + def control(self) -> DataTable: + """Alias for the data table.""" + return self.data_table + class HeaderSelected(Message, bubble=True): """Posted when a column header/label is clicked.""" def __init__( self, + data_table: DataTable, column_key: ColumnKey, column_index: int, label: Text, ): + self.data_table = data_table + """The data table.""" self.column_key = column_key """The key for the column.""" self.column_index = column_index @@ -465,15 +520,23 @@ def __rich_repr__(self) -> rich.repr.Result: yield "column_index", self.column_index yield "label", self.label.plain + @property + def control(self) -> DataTable: + """Alias for the data table.""" + return self.data_table + class RowLabelSelected(Message, bubble=True): """Posted when a row label is clicked.""" def __init__( self, + data_table: DataTable, row_key: RowKey, row_index: int, label: Text, ): + self.data_table = data_table + """The data table.""" self.row_key = row_key """The key for the column.""" self.row_index = row_index @@ -487,6 +550,11 @@ def __rich_repr__(self) -> rich.repr.Result: yield "row_index", self.row_index yield "label", self.label.plain + @property + def control(self) -> DataTable: + """Alias for the data table.""" + return self.data_table + def __init__( self, *, @@ -882,7 +950,7 @@ def _highlight_coordinate(self, coordinate: Coordinate) -> None: cell_key = self.coordinate_to_cell_key(coordinate) self.post_message( DataTable.CellHighlighted( - cell_value, coordinate=coordinate, cell_key=cell_key + self, cell_value, coordinate=coordinate, cell_key=cell_key ) ) @@ -911,14 +979,16 @@ def _highlight_row(self, row_index: int) -> None: is_valid_row = row_index < len(self._data) if is_valid_row: row_key = self._row_locations.get_key(row_index) - self.post_message(DataTable.RowHighlighted(row_index, row_key)) + self.post_message(DataTable.RowHighlighted(self, row_index, row_key)) def _highlight_column(self, column_index: int) -> None: """Apply highlighting to the column at the given index, and post event.""" self.refresh_column(column_index) if column_index < len(self.columns): column_key = self._column_locations.get_key(column_index) - self.post_message(DataTable.ColumnHighlighted(column_index, column_key)) + self.post_message( + DataTable.ColumnHighlighted(self, column_index, column_key) + ) def validate_cursor_coordinate(self, value: Coordinate) -> Coordinate: return self._clamp_cursor_coordinate(value) @@ -1815,12 +1885,14 @@ def on_click(self, event: events.Click) -> None: # Header clicks work even if cursor is off, and doesn't move the cursor. column = self.ordered_columns[column_index] message = DataTable.HeaderSelected( - column.key, column_index, label=column.label + self, column.key, column_index, label=column.label ) self.post_message(message) elif is_row_label_click: row = self.ordered_rows[row_index] - message = DataTable.RowLabelSelected(row.key, row_index, label=row.label) + message = DataTable.RowLabelSelected( + self, row.key, row_index, label=row.label + ) self.post_message(message) elif self.show_cursor and self.cursor_type != "none": # Only post selection events if there is a visible row/col/cell cursor. @@ -1880,6 +1952,7 @@ def _post_selected_message(self): if cursor_type == "cell": self.post_message( DataTable.CellSelected( + self, self.get_cell_at(cursor_coordinate), coordinate=cursor_coordinate, cell_key=cell_key, @@ -1888,8 +1961,8 @@ def _post_selected_message(self): elif cursor_type == "row": row_index, _ = cursor_coordinate row_key, _ = cell_key - self.post_message(DataTable.RowSelected(row_index, row_key)) + self.post_message(DataTable.RowSelected(self, row_index, row_key)) elif cursor_type == "column": _, column_index = cursor_coordinate _, column_key = cell_key - self.post_message(DataTable.ColumnSelected(column_index, column_key)) + self.post_message(DataTable.ColumnSelected(self, column_index, column_key)) diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index 74c0d50bea..da31e99041 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -151,6 +151,11 @@ def __init__(self, input: Input, value: str) -> None: self.input: Input = input self.value: str = value + @property + def control(self) -> Input: + """Alias for self.input.""" + return self.input + class Submitted(Message, bubble=True): """Posted when the enter key is pressed within an `Input`. @@ -167,6 +172,11 @@ def __init__(self, input: Input, value: str) -> None: self.input: Input = input self.value: str = value + @property + def control(self) -> Input: + """Alias for self.input.""" + return self.input + def __init__( self, value: str | None = None, diff --git a/src/textual/widgets/_switch.py b/src/textual/widgets/_switch.py index a31e1e258b..3f9044df49 100644 --- a/src/textual/widgets/_switch.py +++ b/src/textual/widgets/_switch.py @@ -92,6 +92,11 @@ def __init__(self, switch: Switch, value: bool) -> None: self.value: bool = value self.switch: Switch = switch + @property + def control(self) -> Switch: + """Alias for self.switch.""" + return self.switch + def __init__( self, value: bool = False, diff --git a/src/textual/widgets/_toggle_button.py b/src/textual/widgets/_toggle_button.py index ce2b2b44cc..7bbec9e055 100644 --- a/src/textual/widgets/_toggle_button.py +++ b/src/textual/widgets/_toggle_button.py @@ -231,6 +231,11 @@ def __init__(self, toggle_button: ToggleButton, value: bool) -> None: self.value = value """The value of the toggle button after the change.""" + @property + def control(self) -> ToggleButton: + """Alias for self.toggle_button.""" + return self.toggle_button + def watch_value(self) -> None: """React to the value being changed. From 1cb002ce047a5af77d0f0a97a7c7c5dbaf66492f Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 5 Mar 2023 12:31:37 +0000 Subject: [PATCH 09/18] changelog --- CHANGELOG.md | 8 ++++++++ src/textual/_animator.py | 2 +- src/textual/dom.py | 2 +- src/textual/message_pump.py | 2 +- src/textual/timer.py | 8 +------- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be29ca5103..cab03b7d63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [0.14.0] - Unreleased + +### Changes + +- There is now only `post_message` to post events, which is non-async https://github.com/Textualize/textual/pull/1940 +- The Timer class now has just one method to stop it, `Timer.stop` which is non sync https://github.com/Textualize/textual/pull/1940 +- Messages don't require a `sender` in their constructor https://github.com/Textualize/textual/pull/1940 + ## [0.13.0] - 2023-03-02 ### Added diff --git a/src/textual/_animator.py b/src/textual/_animator.py index 0814cc4bc0..564f87da87 100644 --- a/src/textual/_animator.py +++ b/src/textual/_animator.py @@ -200,7 +200,7 @@ async def start(self) -> None: async def stop(self) -> None: """Stop the animator task.""" try: - await self._timer.stop() + self._timer.stop() except asyncio.CancelledError: pass finally: diff --git a/src/textual/dom.py b/src/textual/dom.py index 52ccb35683..0a32f7f162 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -163,7 +163,7 @@ def auto_refresh(self) -> float | None: @auto_refresh.setter def auto_refresh(self, interval: float | None) -> None: if self._auto_refresh_timer is not None: - self._auto_refresh_timer.stop_no_wait() + self._auto_refresh_timer.stop() self._auto_refresh_timer = None if interval is not None: self._auto_refresh_timer = self.set_interval( diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index a4590dd4e7..4b6f5841de 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -382,7 +382,7 @@ async def _close_messages(self, wait: bool = True) -> None: self._closing = True stop_timers = list(self._timers) for timer in stop_timers: - await timer.stop() + timer.stop() self._timers.clear() await self._message_queue.put(events.Unmount()) Reactive._reset_object(self) diff --git a/src/textual/timer.py b/src/textual/timer.py index 6d44c1fa1d..f2e8ba25ee 100644 --- a/src/textual/timer.py +++ b/src/textual/timer.py @@ -89,14 +89,8 @@ def start(self) -> Task: self._task = create_task(self._run_timer(), name=self.name) return self._task - def stop_no_wait(self) -> None: + def stop(self) -> None: """Stop the timer.""" - if self._task is not None: - self._task.cancel() - self._task = None - - async def stop(self) -> None: - """Stop the timer, and block until it exits.""" if self._task is not None: self._active.set() self._task.cancel() From 8355335ba71c9764ac05b07f4413719ee77eac28 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 5 Mar 2023 12:36:26 +0000 Subject: [PATCH 10/18] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cab03b7d63..5f45eda999 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - There is now only `post_message` to post events, which is non-async https://github.com/Textualize/textual/pull/1940 - The Timer class now has just one method to stop it, `Timer.stop` which is non sync https://github.com/Textualize/textual/pull/1940 - Messages don't require a `sender` in their constructor https://github.com/Textualize/textual/pull/1940 +- Many messages have grown a `control` property which returns the control they relate to. https://github.com/Textualize/textual/pull/1940 ## [0.13.0] - 2023-03-02 From 355e62855680e9f27383474fcb61a5599d8fb730 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 5 Mar 2023 13:13:36 +0000 Subject: [PATCH 11/18] changelog --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f45eda999..e1c337fa04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,15 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Messages don't require a `sender` in their constructor https://github.com/Textualize/textual/pull/1940 - Many messages have grown a `control` property which returns the control they relate to. https://github.com/Textualize/textual/pull/1940 + +### Added + +- Added `data_table` attribute to DataTable events https://github.com/Textualize/textual/pull/1940 +- Added `list_view` attribute to `ListView` events https://github.com/Textualize/textual/pull/1940 +- Added `radio_set` attribute to `RadioSet` events https://github.com/Textualize/textual/pull/1940 +- Added `switch` attribute to `Switch` events https://github.com/Textualize/textual/pull/1940 +- Added `toggle_button` attribute to RadioButton and Checkbox events https://github.com/Textualize/textual/pull/1940 + ## [0.13.0] - 2023-03-02 ### Added From 17ac73087c84a451f3b182ec3215446b742b92ab Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 5 Mar 2023 13:30:58 +0000 Subject: [PATCH 12/18] fix for stopping timers --- src/textual/app.py | 2 +- src/textual/message_pump.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/textual/app.py b/src/textual/app.py index 705171d505..a17a5825bd 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -1573,7 +1573,7 @@ async def invoke_ready_callback() -> None: await self.animator.stop() finally: for timer in list(self._timers): - await timer.stop() + timer.stop() self._running = True try: diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 4b6f5841de..fd80a68744 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -419,7 +419,7 @@ async def _process_messages(self) -> None: finally: self._running = False for timer in list(self._timers): - await timer.stop() + timer.stop() async def _pre_process(self) -> None: """Procedure to run before processing messages.""" From 8c910c158411b08f8f6b9254f534296a9d214b70 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 5 Mar 2023 14:43:56 +0000 Subject: [PATCH 13/18] changelog --- CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1c337fa04..4d4362b5bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Changes -- There is now only `post_message` to post events, which is non-async https://github.com/Textualize/textual/pull/1940 -- The Timer class now has just one method to stop it, `Timer.stop` which is non sync https://github.com/Textualize/textual/pull/1940 -- Messages don't require a `sender` in their constructor https://github.com/Textualize/textual/pull/1940 +- Breaking change: There is now only `post_message` to post events, which is non-async, `post_message_no_wait` was dropped. https://github.com/Textualize/textual/pull/1940 +- Breaking change: The Timer class now has just one method to stop it, `Timer.stop` which is non sync https://github.com/Textualize/textual/pull/1940 +- Breaking change: Messages don't require a `sender` in their constructor https://github.com/Textualize/textual/pull/1940 - Many messages have grown a `control` property which returns the control they relate to. https://github.com/Textualize/textual/pull/1940 @@ -21,7 +21,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added `list_view` attribute to `ListView` events https://github.com/Textualize/textual/pull/1940 - Added `radio_set` attribute to `RadioSet` events https://github.com/Textualize/textual/pull/1940 - Added `switch` attribute to `Switch` events https://github.com/Textualize/textual/pull/1940 -- Added `toggle_button` attribute to RadioButton and Checkbox events https://github.com/Textualize/textual/pull/1940 +- Breaking change: Added `toggle_button` attribute to RadioButton and Checkbox events, replaces `input` https://github.com/Textualize/textual/pull/1940 ## [0.13.0] - 2023-03-02 From 9d56314d9509d75269ff0f8ca246c22cdf8e6496 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 6 Mar 2023 09:42:06 +0000 Subject: [PATCH 14/18] added aliases to radio and checkbox --- src/textual/widgets/_checkbox.py | 8 ++++++++ src/textual/widgets/_radio_button.py | 8 ++++++++ src/textual/widgets/_radio_set.py | 10 ++++------ src/textual/widgets/_toggle_button.py | 4 ++-- tests/toggles/test_checkbox.py | 2 +- tests/toggles/test_radiobutton.py | 2 +- 6 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/textual/widgets/_checkbox.py b/src/textual/widgets/_checkbox.py index 2f9b19a6a1..a2cb013a9f 100644 --- a/src/textual/widgets/_checkbox.py +++ b/src/textual/widgets/_checkbox.py @@ -1,5 +1,7 @@ """Provides a check box widget.""" +from __future__ import annotations + from ._toggle_button import ToggleButton @@ -14,3 +16,9 @@ class Changed(ToggleButton.Changed): # https://github.com/Textualize/textual/issues/1814 namespace = "checkbox" + + @property + def checkbox(self) -> Checkbox: + """The checkbox that was changed.""" + assert isinstance(self._toggle_button, Checkbox) + return self._toggle_button diff --git a/src/textual/widgets/_radio_button.py b/src/textual/widgets/_radio_button.py index c04bdb8dee..4acc889f1a 100644 --- a/src/textual/widgets/_radio_button.py +++ b/src/textual/widgets/_radio_button.py @@ -1,5 +1,7 @@ """Provides a radio button widget.""" +from __future__ import annotations + from ._toggle_button import ToggleButton @@ -21,3 +23,9 @@ class Changed(ToggleButton.Changed): # https://github.com/Textualize/textual/issues/1814 namespace = "radio_button" + + @property + def radio_button(self) -> RadioButton: + """The radio button that was changed.""" + assert isinstance(self._toggle_button, RadioButton) + return self._toggle_button diff --git a/src/textual/widgets/_radio_set.py b/src/textual/widgets/_radio_set.py index cd0ef59d53..bbc8e6f18e 100644 --- a/src/textual/widgets/_radio_set.py +++ b/src/textual/widgets/_radio_set.py @@ -113,16 +113,14 @@ def on_radio_button_changed(self, event: RadioButton.Changed) -> None: event: The event. """ # If the button is changing to be the pressed button... - if event.toggle_button.value: + if event.radio_button.value: # ...send off a message to say that the pressed state has # changed. - self.post_message( - self.Changed(self, cast(RadioButton, event.toggle_button)) - ) + self.post_message(self.Changed(self, cast(RadioButton, event.radio_button))) # ...then look for the button that was previously the pressed # one and unpress it. for button in self._buttons.filter(".-on"): - if button != event.toggle_button: + if button != event.radio_button: button.value = False break else: @@ -133,7 +131,7 @@ def on_radio_button_changed(self, event: RadioButton.Changed) -> None: event.stop() if not self._buttons.filter(".-on"): with self.prevent(RadioButton.Changed): - event.toggle_button.value = True + event.radio_button.value = True @property def pressed_button(self) -> RadioButton | None: diff --git a/src/textual/widgets/_toggle_button.py b/src/textual/widgets/_toggle_button.py index 7bbec9e055..1b1ea753af 100644 --- a/src/textual/widgets/_toggle_button.py +++ b/src/textual/widgets/_toggle_button.py @@ -226,7 +226,7 @@ def __init__(self, toggle_button: ToggleButton, value: bool) -> None: value: The value of the toggle button. """ super().__init__() - self.toggle_button = toggle_button + self._toggle_button = toggle_button """A reference to the toggle button that was changed.""" self.value = value """The value of the toggle button after the change.""" @@ -234,7 +234,7 @@ def __init__(self, toggle_button: ToggleButton, value: bool) -> None: @property def control(self) -> ToggleButton: """Alias for self.toggle_button.""" - return self.toggle_button + return self._toggle_button def watch_value(self) -> None: """React to the value being changed. diff --git a/tests/toggles/test_checkbox.py b/tests/toggles/test_checkbox.py index 93b1fadac8..573953f234 100644 --- a/tests/toggles/test_checkbox.py +++ b/tests/toggles/test_checkbox.py @@ -15,7 +15,7 @@ def compose(self) -> ComposeResult: yield Checkbox(value=True, id="cb3") def on_checkbox_changed(self, event: Checkbox.Changed) -> None: - self.events_received.append((event.toggle_button.id, event.toggle_button.value)) + self.events_received.append((event.checkbox.id, event.checkbox.value)) async def test_checkbox_initial_state() -> None: diff --git a/tests/toggles/test_radiobutton.py b/tests/toggles/test_radiobutton.py index 9d5932eb4f..dc32e8c0f6 100644 --- a/tests/toggles/test_radiobutton.py +++ b/tests/toggles/test_radiobutton.py @@ -15,7 +15,7 @@ def compose(self) -> ComposeResult: yield RadioButton(value=True, id="rb3") def on_radio_button_changed(self, event: RadioButton.Changed) -> None: - self.events_received.append((event.toggle_button.id, event.toggle_button.value)) + self.events_received.append((event.radio_button.id, event.radio_button.value)) async def test_radio_button_initial_state() -> None: From c9712729908d130c999a0d6c8d6ed0af9de8af38 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 6 Mar 2023 10:01:07 +0000 Subject: [PATCH 15/18] Drop sender from Message init --- src/textual/message.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/textual/message.py b/src/textual/message.py index 168a56b6b0..22e877a18f 100644 --- a/src/textual/message.py +++ b/src/textual/message.py @@ -32,8 +32,8 @@ class Message: no_dispatch: ClassVar[bool] = False # Message may not be handled by client code namespace: ClassVar[str] = "" # Namespace to disambiguate messages - def __init__(self, sender: MessageTarget | None = None) -> None: - self._sender: MessageTarget | None = sender or active_message_pump.get(None) + def __init__(self) -> None: + self._sender: MessageTarget | None = active_message_pump.get(None) self.time: float = _clock.get_time_no_wait() self._forwarded = False self._no_default_action = False From eb4c95997057f0290ed02fed6ae12ecc005cc6c8 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 6 Mar 2023 10:05:09 +0000 Subject: [PATCH 16/18] drop time --- CHANGELOG.md | 2 +- src/textual/message.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d4362b5bf..76fb3117a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Breaking change: The Timer class now has just one method to stop it, `Timer.stop` which is non sync https://github.com/Textualize/textual/pull/1940 - Breaking change: Messages don't require a `sender` in their constructor https://github.com/Textualize/textual/pull/1940 - Many messages have grown a `control` property which returns the control they relate to. https://github.com/Textualize/textual/pull/1940 - +- Dropped `time` attribute from Messages https://github.com/Textualize/textual/pull/1940 ### Added diff --git a/src/textual/message.py b/src/textual/message.py index 22e877a18f..51921760a7 100644 --- a/src/textual/message.py +++ b/src/textual/message.py @@ -19,7 +19,6 @@ class Message: __slots__ = [ "_sender", - "time", "_forwarded", "_no_default_action", "_stop_propagation", @@ -34,7 +33,6 @@ class Message: def __init__(self) -> None: self._sender: MessageTarget | None = active_message_pump.get(None) - self.time: float = _clock.get_time_no_wait() self._forwarded = False self._no_default_action = False self._stop_propagation = False From 6e07458ced94861e9fcb524efd2779e81afa5d3c Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 6 Mar 2023 10:26:20 +0000 Subject: [PATCH 17/18] drop cast --- src/textual/widgets/_radio_set.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/widgets/_radio_set.py b/src/textual/widgets/_radio_set.py index bbc8e6f18e..461c77dca4 100644 --- a/src/textual/widgets/_radio_set.py +++ b/src/textual/widgets/_radio_set.py @@ -116,7 +116,7 @@ def on_radio_button_changed(self, event: RadioButton.Changed) -> None: if event.radio_button.value: # ...send off a message to say that the pressed state has # changed. - self.post_message(self.Changed(self, cast(RadioButton, event.radio_button))) + self.post_message(self.Changed(self, event.radio_button)) # ...then look for the button that was previously the pressed # one and unpress it. for button in self._buttons.filter(".-on"): From 0fa9236bb13cf77609fa62ef628737e490630f5d Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 6 Mar 2023 10:44:57 +0000 Subject: [PATCH 18/18] Added aliases --- src/textual/widgets/_checkbox.py | 5 +++++ src/textual/widgets/_radio_button.py | 5 +++++ src/textual/widgets/_toggle_button.py | 5 ----- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/textual/widgets/_checkbox.py b/src/textual/widgets/_checkbox.py index a2cb013a9f..510e47189c 100644 --- a/src/textual/widgets/_checkbox.py +++ b/src/textual/widgets/_checkbox.py @@ -22,3 +22,8 @@ def checkbox(self) -> Checkbox: """The checkbox that was changed.""" assert isinstance(self._toggle_button, Checkbox) return self._toggle_button + + @property + def control(self) -> Checkbox: + """An alias for self.checkbox""" + return self.checkbox diff --git a/src/textual/widgets/_radio_button.py b/src/textual/widgets/_radio_button.py index 4acc889f1a..8d847a9081 100644 --- a/src/textual/widgets/_radio_button.py +++ b/src/textual/widgets/_radio_button.py @@ -29,3 +29,8 @@ def radio_button(self) -> RadioButton: """The radio button that was changed.""" assert isinstance(self._toggle_button, RadioButton) return self._toggle_button + + @property + def control(self) -> RadioButton: + """Alias for self.radio_button""" + return self.radio_button diff --git a/src/textual/widgets/_toggle_button.py b/src/textual/widgets/_toggle_button.py index 1b1ea753af..dc1fee373c 100644 --- a/src/textual/widgets/_toggle_button.py +++ b/src/textual/widgets/_toggle_button.py @@ -231,11 +231,6 @@ def __init__(self, toggle_button: ToggleButton, value: bool) -> None: self.value = value """The value of the toggle button after the change.""" - @property - def control(self) -> ToggleButton: - """Alias for self.toggle_button.""" - return self._toggle_button - def watch_value(self) -> None: """React to the value being changed.