Drop explicit sender attribute from messages (#1940)

* remove sender

* removed priority post

* timer fix

* test fixes

* drop async version of post_message

* extended docs

* fix no app

* Added control properties

* changelog

* changelog

* changelog

* fix for stopping timers

* changelog

* added aliases to radio and checkbox

* Drop sender from Message init

* drop time

* drop cast

* Added aliases
This commit is contained in:
Will McGugan
2023-03-06 10:52:34 +00:00
committed by GitHub
parent cb84d9111c
commit 373fc95fc1
41 changed files with 390 additions and 403 deletions

View File

@@ -5,6 +5,24 @@ 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
- 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
- Dropped `time` attribute from Messages 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
- 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
### Added

View File

@@ -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
@@ -24,9 +24,9 @@ class ColorButton(Static):
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, self.color))
self.post_message(self.Selected(self.color))
def render(self) -> str:
return str(self.color)

View File

@@ -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

View File

@@ -182,7 +182,6 @@ class Animator:
self._timer = Timer(
app,
1 / frames_per_second,
app,
name="Animator",
callback=self,
pause=True,
@@ -201,7 +200,7 @@ class Animator:
async def stop(self) -> None:
"""Stop the animator task."""
try:
await self._timer.stop()
self._timer.stop()
except asyncio.CancelledError:
pass
finally:

View File

@@ -8,21 +8,18 @@ if TYPE_CHECKING:
class MessageTarget(Protocol):
async def post_message(self, message: "Message") -> bool:
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:
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:
...

View File

@@ -26,10 +26,7 @@ _re_bracketed_paste_end = re.compile(r"^\x1b\[201~$")
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 @@ class XTermParser(Parser[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 @@ class XTermParser(Parser[events.Event]):
button = (buttons + 1) & 3
event = event_class(
sender,
x,
y,
delta_x,
@@ -103,7 +99,7 @@ class XTermParser(Parser[events.Event]):
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 @@ class XTermParser(Parser[events.Event]):
# 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(pasted_text.replace("\x00", "")))
paste_buffer.clear()
character = ESC if use_prior_escape else (yield read1())
@@ -145,12 +139,12 @@ class XTermParser(Parser[events.Event]):
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 @@ class XTermParser(Parser[events.Event]):
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 @@ class XTermParser(Parser[events.Event]):
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 @@ class XTermParser(Parser[events.Event]):
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 @@ class XTermParser(Parser[events.Event]):
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)

View File

@@ -525,7 +525,7 @@ class App(Generic[ReturnType], DOMNode):
"""
self._exit = True
self._return_value = result
self.post_message_no_wait(messages.ExitApp(sender=self))
self.post_message(messages.ExitApp())
if message:
self._exit_renderables.append(message)
@@ -878,7 +878,7 @@ class App(Generic[ReturnType], DOMNode):
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)
@@ -1272,7 +1272,7 @@ class App(Generic[ReturnType], DOMNode):
The screen that was replaced.
"""
screen.post_message_no_wait(events.ScreenSuspend(self))
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 @@ class App(Generic[ReturnType], DOMNode):
"""
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(events.ScreenResume())
self.log.system(f"{self.screen} is current (PUSHED)")
return await_mount
@@ -1303,7 +1303,7 @@ class App(Generic[ReturnType], DOMNode):
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(events.ScreenResume())
self.log.system(f"{self.screen} is current (SWITCHED)")
return await_mount
return AwaitMount(self.screen, [])
@@ -1382,7 +1382,7 @@ class App(Generic[ReturnType], DOMNode):
)
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(events.ScreenResume())
self.log.system(f"{self.screen} is active")
return previous_screen
@@ -1395,7 +1395,7 @@ class App(Generic[ReturnType], DOMNode):
"""
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 @@ class App(Generic[ReturnType], DOMNode):
if widget is None:
if self.mouse_over is not None:
try:
await self.mouse_over.post_message(events.Leave(self))
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))
self.mouse_over._forward_event(events.Leave())
if widget is not None:
await widget._forward_event(events.Enter(self))
widget._forward_event(events.Enter())
finally:
self.mouse_over = widget
@@ -1426,12 +1426,10 @@ class App(Generic[ReturnType], DOMNode):
if widget == self.mouse_captured:
return
if self.mouse_captured is not None:
self.mouse_captured.post_message_no_wait(
events.MouseRelease(self, 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, self.mouse_position))
widget.post_message(events.MouseCapture(self.mouse_position))
def panic(self, *renderables: RenderableType) -> None:
"""Exits the app then displays a message.
@@ -1544,8 +1542,8 @@ class App(Generic[ReturnType], DOMNode):
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()
@@ -1575,11 +1573,11 @@ class App(Generic[ReturnType], DOMNode):
await self.animator.stop()
finally:
for timer in list(self._timers):
await timer.stop()
timer.stop()
self._running = True
try:
load_event = events.Load(sender=self)
load_event = events.Load()
await self._dispatch_message(load_event)
driver: Driver
@@ -1825,7 +1823,7 @@ class App(Generic[ReturnType], DOMNode):
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:
@@ -1953,19 +1951,19 @@ class App(Generic[ReturnType], DOMNode):
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 @@ class App(Generic[ReturnType], DOMNode):
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.

View File

@@ -163,7 +163,7 @@ class DOMNode(MessagePump):
@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(

View File

@@ -34,7 +34,7 @@ class Driver(ABC):
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:

View File

@@ -39,9 +39,9 @@ class HeadlessDriver(Driver):
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),
self._target._post_message(event),
loop=loop,
)

View File

@@ -97,9 +97,9 @@ class LinuxDriver(Driver):
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),
self._target._post_message(event),
loop=loop,
)
@@ -217,7 +217,7 @@ class LinuxDriver(Driver):
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

View File

@@ -224,7 +224,7 @@ class EventMonitor(threading.Thread):
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)

View File

@@ -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
@@ -28,9 +28,9 @@ class Event(Message):
@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 @@ class Action(Event):
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 @@ class Resize(Event, bubble=False):
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 @@ class MouseEvent(InputEvent, bubble=True):
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 @@ class MouseEvent(InputEvent, bubble=True):
@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 @@ class MouseEvent(InputEvent, bubble=True):
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:

View File

@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, ClassVar
import rich.repr
from . import _clock
from ._context import active_message_pump
from ._types import MessageTarget as MessageTarget
from .case import camel_to_snake
@@ -14,19 +15,10 @@ if TYPE_CHECKING:
@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",
"_sender",
"_forwarded",
"_no_default_action",
"_stop_propagation",
@@ -34,16 +26,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
self.time: float = _clock.get_time_no_wait()
def __init__(self) -> None:
self._sender: MessageTarget | None = active_message_pump.get(None)
self._forwarded = False
self._no_default_action = False
self._stop_propagation = False
@@ -55,7 +44,7 @@ class Message:
super().__init__()
def __rich_repr__(self) -> rich.repr.Result:
yield self.sender
yield from ()
def __init_subclass__(
cls,
@@ -73,6 +62,12 @@ class Message:
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
@@ -118,10 +113,10 @@ class 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)

View File

@@ -287,7 +287,6 @@ class MessagePump(metaclass=MessagePumpMeta):
timer = Timer(
self,
delay,
self,
name=name or f"set_timer#{Timer._timer_count}",
callback=callback,
repeat=0,
@@ -321,7 +320,6 @@ class MessagePump(metaclass=MessagePumpMeta):
timer = Timer(
self,
interval,
self,
name=name or f"set_interval#{Timer._timer_count}",
callback=callback,
repeat=repeat or None,
@@ -341,8 +339,8 @@ class MessagePump(metaclass=MessagePumpMeta):
# 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))
self.post_message_no_wait(message)
message = messages.InvokeLater(partial(callback, *args, **kwargs))
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.
@@ -353,8 +351,8 @@ class MessagePump(metaclass=MessagePumpMeta):
*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))
self.post_message_no_wait(message)
message = events.Callback(callback=partial(callback, *args, **kwargs))
self.post_message(message)
def call_next(self, callback: Callable, *args, **kwargs) -> None:
"""Schedule a callback to run immediately after processing the current message.
@@ -372,7 +370,7 @@ class MessagePump(metaclass=MessagePumpMeta):
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()
@@ -384,9 +382,9 @@ class MessagePump(metaclass=MessagePumpMeta):
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(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:
@@ -421,15 +419,15 @@ class MessagePump(metaclass=MessagePumpMeta):
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."""
# 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 +487,7 @@ class MessagePump(metaclass=MessagePumpMeta):
):
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
):
@@ -581,20 +579,22 @@ class MessagePump(metaclass=MessagePumpMeta):
# 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:
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(sender=self))
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.
@@ -602,40 +602,9 @@ class MessagePump(metaclass=MessagePumpMeta):
True if the messages was posted successfully, False if the message was not posted
(because the message pump was in the process of closing).
"""
return self.post_message(message)
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
# 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:
def post_message(self, message: Message) -> bool:
"""Posts a message on the queue.
Args:
@@ -654,16 +623,6 @@ class MessagePump(metaclass=MessagePumpMeta):
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)

View File

@@ -9,7 +9,6 @@ from .geometry import Region
from .message import Message
if TYPE_CHECKING:
from .message_pump import MessagePump
from .widget import Widget
@@ -25,12 +24,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 +61,9 @@ class UpdateScroll(Message, verbose=True):
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 +73,9 @@ class InvokeLater(Message, verbose=True, bubble=False):
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):

View File

@@ -210,9 +210,7 @@ class Reactive(Generic[ReactiveType]):
_rich_traceback_omit = True
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))
)
obj.post_message(events.Callback(callback=partial(Reactive._compute, obj)))
def invoke_watcher(
watch_function: Callable, old_value: object, value: object
@@ -235,10 +233,8 @@ class Reactive(Generic[ReactiveType]):
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(
events.Callback(
sender=obj, callback=partial(await_watcher, watch_result)
)
obj.post_message(
events.Callback(callback=partial(await_watcher, watch_result))
)
watch_function = getattr(obj, f"watch_{name}", None)

View File

@@ -330,20 +330,20 @@ class Screen(Widget):
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(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(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(self))
widget.post_message(events.Focus())
self.log.debug(widget, "was focused")
async def _on_idle(self, event: events.Idle) -> None:
@@ -381,7 +381,7 @@ class Screen(Widget):
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(events.InvokeCallbacks())
self.update_timer.pause()
@@ -439,9 +439,9 @@ class Screen(Widget):
if widget._size_updated(
region.size, virtual_size, container_size, layout=False
):
widget.post_message_no_wait(
widget.post_message(
ResizeEvent(
self, region.size, virtual_size, container_size
region.size, virtual_size, container_size
)
)
@@ -451,7 +451,7 @@ class Screen(Widget):
Show = events.Show
for widget in hidden:
widget.post_message_no_wait(Hide(self))
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 @@ class Screen(Widget):
) in layers:
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)
widget.post_message(
ResizeEvent(region.size, virtual_size, container_size)
)
for widget in shown:
widget.post_message_no_wait(Show(self))
widget.post_message(Show())
except Exception as error:
self.app._handle_exception(error)
@@ -480,7 +480,7 @@ class Screen(Widget):
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(events.Ready())
self.app._dom_ready = True
async def _on_update(self, message: messages.Update) -> None:
@@ -516,7 +516,7 @@ class Screen(Widget):
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,11 +524,10 @@ class Screen(Widget):
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(
self,
event.x - region.x,
event.y - region.y,
event.delta_x,
@@ -543,18 +542,18 @@ class Screen(Widget):
)
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:
@@ -574,11 +573,9 @@ class Screen(Widget):
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:
@@ -588,8 +585,8 @@ class Screen(Widget):
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)

View File

@@ -10,7 +10,6 @@ from rich.segment import Segment, Segments
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
@@ -47,7 +46,6 @@ class ScrollTo(ScrollMessage, verbose=True):
def __init__(
self,
sender: MessageTarget,
x: float | None = None,
y: float | None = None,
animate: bool = True,
@@ -55,7 +53,7 @@ class ScrollTo(ScrollMessage, verbose=True):
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
@@ -301,12 +299,10 @@ class ScrollBar(Widget):
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(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(ScrollUp() if self.vertical else ScrollLeft())
def action_grab(self) -> None:
self.capture_mouse()
@@ -359,7 +355,7 @@ class ScrollBar(Widget):
* (virtual_size / self.window_size)
)
)
await self.post_message(ScrollTo(self, x=x, y=y))
self.post_message(ScrollTo(x=x, y=y))
event.stop()
async def _on_click(self, event: events.Click) -> None:

View File

@@ -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 @@ class Timer:
self,
event_target: MessageTarget,
interval: float,
sender: MessageTarget,
*,
name: str | None = None,
callback: TimerCallback | None = None,
@@ -59,7 +57,6 @@ class Timer:
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
@@ -92,14 +89,8 @@ class Timer:
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()
@@ -170,10 +161,9 @@ class Timer:
app._handle_exception(error)
else:
event = events.Timer(
self.sender,
timer=self,
time=next_timer,
count=count,
callback=self._callback,
)
await self.target._post_priority_message(event)
await self.target.post_message(event)

View File

@@ -42,7 +42,7 @@ from ._arrange import DockArrangeResult, arrange
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
@@ -2491,9 +2491,9 @@ class Widget(DOMNode):
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 @@ class Widget(DOMNode):
"""
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,13 @@ class Widget(DOMNode):
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)
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:
"""Called when there are no more events on the queue.
@@ -2608,13 +2610,13 @@ class Widget(DOMNode):
else:
if self._scroll_required:
self._scroll_required = False
screen.post_message_no_wait(messages.UpdateScroll(self))
screen.post_message(messages.UpdateScroll())
if self._repaint_required:
self._repaint_required = False
screen.post_message_no_wait(messages.Update(self, self))
screen.post_message(messages.Update(self))
if self._layout_required:
self._layout_required = False
screen.post_message_no_wait(messages.Layout(self))
screen.post_message(messages.Layout())
def focus(self, scroll_visible: bool = True) -> None:
"""Give focus to this widget.
@@ -2729,12 +2731,12 @@ class Widget(DOMNode):
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(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(events.DescendantBlur())
def _on_descendant_blur(self, event: events.DescendantBlur) -> None:
if self._has_focus_within:

View File

@@ -165,9 +165,14 @@ class Button(Static, can_focus=True):
button: The button that was pressed.
"""
def __init__(self, button: Button) -> None:
self.button = button
super().__init__()
@property
def button(self) -> Button:
return cast(Button, self.sender)
def control(self) -> Button:
"""Alias for the button."""
return self.button
def __init__(
self,
@@ -235,7 +240,7 @@ class Button(Static, can_focus=True):
# 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 +252,7 @@ class Button(Static, can_focus=True):
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(

View File

@@ -1,5 +1,7 @@
"""Provides a check box widget."""
from __future__ import annotations
from ._toggle_button import ToggleButton
@@ -14,3 +16,14 @@ class Checkbox(ToggleButton):
# 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
@property
def control(self) -> Checkbox:
"""An alias for self.checkbox"""
return self.checkbox

View File

@@ -319,25 +319,31 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
def __init__(
self,
sender: DataTable,
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
"""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
@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.
@@ -348,25 +354,31 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
def __init__(
self,
sender: DataTable,
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
"""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
@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.
@@ -376,18 +388,26 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
widget in the DOM.
"""
def __init__(self, sender: DataTable, 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
"""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
@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.
@@ -397,18 +417,26 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
widget in the DOM.
"""
def __init__(self, sender: DataTable, 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
"""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
@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.
@@ -419,19 +447,25 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
"""
def __init__(
self, sender: DataTable, cursor_column: int, column_key: ColumnKey
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
"""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
@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.
@@ -442,67 +476,85 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
"""
def __init__(
self, sender: DataTable, cursor_column: int, column_key: ColumnKey
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
"""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
@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,
sender: DataTable,
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
"""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
@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,
sender: DataTable,
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
"""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
@property
def control(self) -> DataTable:
"""Alias for the data table."""
return self.data_table
def __init__(
self,
*,
@@ -896,7 +948,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
return
else:
cell_key = self.coordinate_to_cell_key(coordinate)
self.post_message_no_wait(
self.post_message(
DataTable.CellHighlighted(
self, cell_value, coordinate=coordinate, cell_key=cell_key
)
@@ -927,16 +979,14 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
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(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_no_wait(
self.post_message(
DataTable.ColumnHighlighted(self, column_index, column_key)
)
@@ -1837,13 +1887,13 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
message = DataTable.HeaderSelected(
self, 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(
self, 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)
@@ -1900,7 +1950,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
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,
self.get_cell_at(cursor_coordinate),
@@ -1911,10 +1961,8 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
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(DataTable.RowSelected(self, 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)
)
self.post_message(DataTable.ColumnSelected(self, column_index, column_key))

View File

@@ -77,9 +77,9 @@ class DirectoryTree(Tree[DirEntry]):
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)
super().__init__()
def __init__(
self,
@@ -176,7 +176,7 @@ class DirectoryTree(Tree[DirEntry]):
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 @@ class DirectoryTree(Tree[DirEntry]):
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))

View File

@@ -146,10 +146,15 @@ class Input(Widget, can_focus=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
@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`.
@@ -162,10 +167,15 @@ class Input(Widget, can_focus=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
@property
def control(self) -> Input:
"""Alias for self.input."""
return self.input
def __init__(
self,
@@ -243,7 +253,7 @@ class Input(Widget, can_focus=True):
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 +489,4 @@ class Input(Widget, can_focus=True):
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))

View File

@@ -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,10 +43,12 @@ 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
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")

View File

@@ -48,8 +48,9 @@ class ListView(Vertical, can_focus=True, can_focus_children=False):
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 ListView(Vertical, can_focus=True, can_focus_children=False):
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__(
@@ -143,7 +145,7 @@ class ListView(Vertical, can_focus=True, can_focus_children=False):
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.
@@ -176,7 +178,7 @@ class ListView(Vertical, can_focus=True, can_focus_children=False):
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."""
@@ -194,8 +196,8 @@ class ListView(Vertical, can_focus=True, can_focus_children=False):
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(self.Selected(self, event.item))
def _scroll_highlighted_region(self) -> None:
"""Used to keep the highlighted index within vision"""

View File

@@ -100,7 +100,7 @@ class MarkdownBlock(Static):
async def action_link(self, href: str) -> None:
"""Called on link click."""
await self.post_message(Markdown.LinkClicked(href, sender=self))
self.post_message(Markdown.LinkClicked(href))
class MarkdownHeader(MarkdownBlock):
@@ -524,26 +524,24 @@ class Markdown(Widget):
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 @@ class Markdown(Widget):
)
)
await self.post_message(
Markdown.TableOfContentsUpdated(table_of_contents, sender=self)
)
self.post_message(Markdown.TableOfContentsUpdated(table_of_contents))
with self.app.batch_update():
await self.query("MarkdownBlock").remove()
await self.mount_all(output)
@@ -760,8 +756,8 @@ class MarkdownTableOfContents(Widget, can_focus_children=True):
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)
await self._post_message(
Markdown.TableOfContentsSelected(node_data["block_id"])
)

View File

@@ -1,5 +1,7 @@
"""Provides a radio button widget."""
from __future__ import annotations
from ._toggle_button import ToggleButton
@@ -21,3 +23,14 @@ class RadioButton(ToggleButton):
# 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
@property
def control(self) -> RadioButton:
"""Alias for self.radio_button"""
return self.radio_button

View File

@@ -37,15 +37,14 @@ class RadioSet(Container):
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.radio_set = radio_set
"""A reference to the `RadioSet` that was changed."""
self.pressed = pressed
"""The `RadioButton` that was pressed to make the change."""
@@ -54,7 +53,7 @@ class RadioSet(Container):
# 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__(
@@ -114,16 +113,14 @@ class RadioSet(Container):
event: The event.
"""
# If the button is changing to be the pressed button...
if event.input.value:
if event.radio_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.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"):
if button != event.input:
if button != event.radio_button:
button.value = False
break
else:
@@ -134,7 +131,7 @@ class RadioSet(Container):
event.stop()
if not self._buttons.filter(".-on"):
with self.prevent(RadioButton.Changed):
event.input.value = True
event.radio_button.value = True
@property
def pressed_button(self) -> RadioButton | None:

View File

@@ -87,10 +87,15 @@ class Switch(Widget, can_focus=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
@property
def control(self) -> Switch:
"""Alias for self.switch."""
return self.switch
def __init__(
self,
@@ -124,7 +129,7 @@ class Switch(Widget, can_focus=True):
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")

View File

@@ -218,15 +218,15 @@ class ToggleButton(Static, can_focus=True):
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."""
@@ -239,4 +239,4 @@ class ToggleButton(Static, can_focus=True):
`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))

View File

@@ -14,7 +14,6 @@ from .._cache import LRUCache
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
@@ -436,11 +435,9 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=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 +449,9 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=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 +463,9 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=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 +477,9 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=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,
@@ -779,7 +770,7 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
node._selected = True
self._cursor_node = node
if previous_node != node:
self.post_message_no_wait(self.NodeHighlighted(self, node))
self.post_message(self.NodeHighlighted(node))
def watch_guide_depth(self, guide_depth: int) -> None:
self._invalidate()
@@ -1027,10 +1018,10 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
return
if node.is_expanded:
node.collapse()
self.post_message_no_wait(self.NodeCollapsed(self, node))
self.post_message(self.NodeCollapsed(node))
else:
node.expand()
self.post_message_no_wait(self.NodeExpanded(self, node))
self.post_message(self.NodeExpanded(node))
async def _on_click(self, event: events.Click) -> None:
meta = event.style.meta
@@ -1117,4 +1108,4 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
node = line.path[-1]
if self.auto_expand:
self._toggle_node(node)
self.post_message_no_wait(self.NodeSelected(self, node))
self.post_message(self.NodeSelected(node))

View File

@@ -10,7 +10,6 @@ from textual.app import App
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,
@@ -556,9 +555,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 +575,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 +589,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)
@@ -610,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")
@@ -619,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
@@ -639,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 == [
@@ -648,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
@@ -669,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,
@@ -694,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,
@@ -708,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
@@ -729,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,
@@ -743,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

View File

@@ -19,7 +19,7 @@ class ValidWidget(Widget):
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

View File

@@ -11,7 +11,7 @@ async def test_paste_app():
app = PasteApp()
async with app.run_test() as pilot:
await app.post_message(events.Paste(sender=app, text="Hello"))
app.post_message(events.Paste(text="Hello"))
await pilot.pause(0)
assert len(paste_events) == 1

View File

@@ -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):

View File

@@ -15,7 +15,7 @@ class CheckboxApp(App[None]):
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.checkbox.id, event.checkbox.value))
async def test_checkbox_initial_state() -> None:

View File

@@ -15,7 +15,7 @@ class RadioButtonApp(App[None]):
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.radio_button.id, event.radio_button.value))
async def test_radio_button_initial_state() -> None:

View File

@@ -19,9 +19,9 @@ class RadioSetApp(App[None]):
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)],
)
)