From 7d99d168ff907955bda8c1da609ebc9093f1a188 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 23 Feb 2023 13:49:07 +0000 Subject: [PATCH 01/25] prevent implementation --- CHANGELOG.md | 1 + src/textual/events.py | 6 +++++- src/textual/message_pump.py | 35 +++++++++++++++++++++++++++++++++-- src/textual/reactive.py | 4 +++- tests/test_message_pump.py | 33 +++++++++++++++++++++++++++++++++ 5 files changed, 75 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 042796b7b..7e9eb0a49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added horizontal rule to Markdown https://github.com/Textualize/textual/pull/1832 - Added `Widget.disabled` https://github.com/Textualize/textual/pull/1785 - Added `DOMNode.notify_style_update` to replace `messages.StylesUpdated` message https://github.com/Textualize/textual/pull/1861 +- Added `MessagePump.prevent` context manager to temporarily suppress a given message type ### Changed diff --git a/src/textual/events.py b/src/textual/events.py index 9914c03d5..34f302fe2 100644 --- a/src/textual/events.py +++ b/src/textual/events.py @@ -29,9 +29,13 @@ class Event(Message): @rich.repr.auto class Callback(Event, bubble=False, verbose=True): def __init__( - self, sender: MessageTarget, callback: Callable[[], Awaitable[None]] + self, + sender: MessageTarget, + callback: Callable[[], Awaitable[None]], + prevent: set[type[Message]] | None = None, ) -> None: self.callback = callback + self.prevent = frozenset(prevent) if prevent else None super().__init__(sender) def __rich_repr__(self) -> rich.repr.Result: diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index d17bd8249..4391103cc 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -10,8 +10,9 @@ from __future__ import annotations import asyncio import inspect from asyncio import CancelledError, Queue, QueueEmpty, Task +from contextlib import contextmanager from functools import partial -from typing import TYPE_CHECKING, Any, Awaitable, Callable, Iterable +from typing import TYPE_CHECKING, Any, Awaitable, Callable, Generator, Iterable from weakref import WeakSet from . import Logger, events, log, messages @@ -19,6 +20,7 @@ from ._asyncio import create_task from ._callback import invoke from ._context import NoActiveAppError, active_app, active_message_pump from ._time import time +from ._types import CallbackType from .case import camel_to_snake from .errors import DuplicateKeyHandlers from .events import Event @@ -77,6 +79,7 @@ class MessagePump(metaclass=MessagePumpMeta): self._max_idle: float | None = None self._mounted_event = asyncio.Event() self._next_callbacks: list[CallbackType] = [] + self._prevent_events: list[set[type[Message]]] = [] @property def task(self) -> Task: @@ -149,6 +152,9 @@ class MessagePump(metaclass=MessagePumpMeta): self._parent = None def check_message_enabled(self, message: Message) -> bool: + message_type = type(message) + if self._prevent_events and message_type in self._prevent_events[-1]: + return False return type(message) not in self._disabled_messages def disable_messages(self, *messages: type[Message]) -> None: @@ -527,6 +533,27 @@ class MessagePump(metaclass=MessagePumpMeta): if self._message_queue.empty(): self.post_message_no_wait(messages.Prompt(sender=self)) + @contextmanager + def prevent(self, *message_types: type[Message]) -> Generator[None, None, None]: + """A context manager to *temporarily* prevent the given message types from being posted. + + Example: + + input = self.query_one(Input) + with self.prevent(Input.Changed): + input.value = "foo" + + + """ + if self._prevent_events: + self._prevent_events.append(self._prevent_events[-1].union(message_types)) + else: + self._prevent_events.append(set(message_types)) + try: + yield + finally: + self._prevent_events.pop() + async def post_message(self, message: Message) -> bool: """Post a message or an event to this message pump. @@ -594,7 +621,11 @@ class MessagePump(metaclass=MessagePumpMeta): return self.post_message_no_wait(message) async def on_callback(self, event: events.Callback) -> None: - await invoke(event.callback) + if event.prevent: + with self.prevent(*event.prevent): + await invoke(event.callback) + else: + await invoke(event.callback) # TODO: Does dispatch_key belong on message pump? async def dispatch_key(self, event: events.Key) -> bool: diff --git a/src/textual/reactive.py b/src/textual/reactive.py index 0553d076c..140cb0ff1 100644 --- a/src/textual/reactive.py +++ b/src/textual/reactive.py @@ -235,7 +235,9 @@ class Reactive(Generic[ReactiveType]): # 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) + sender=obj, + callback=partial(await_watcher, watch_result), + prevent=obj._prevent_events[0] if obj._prevent_events else None, ) ) diff --git a/tests/test_message_pump.py b/tests/test_message_pump.py index ce665f7c9..9667b4cbc 100644 --- a/tests/test_message_pump.py +++ b/tests/test_message_pump.py @@ -1,8 +1,10 @@ import pytest +from textual.app import App, ComposeResult from textual.errors import DuplicateKeyHandlers from textual.events import Key from textual.widget import Widget +from textual.widgets import Input class ValidWidget(Widget): @@ -54,3 +56,34 @@ async def test_dispatch_key_raises_when_conflicting_handler_aliases(): with pytest.raises(DuplicateKeyHandlers): await widget.dispatch_key(Key(widget, key="tab", character="\t")) assert widget.called_by == widget.key_tab + + +class PreventTestApp(App): + def __init__(self) -> None: + self.input_changed_events = [] + super().__init__() + + def compose(self) -> ComposeResult: + yield Input() + + def on_input_changed(self, event: Input.Changed) -> None: + self.input_changed_events.append(event) + + +async def test_prevent() -> None: + app = PreventTestApp() + + async with app.run_test() as pilot: + assert not app.input_changed_events + input = app.query_one(Input) + input.value = "foo" + await pilot.pause() + assert len(app.input_changed_events) == 1 + assert app.input_changed_events[0].value == "foo" + + with input.prevent(Input.Changed): + input.value = "bar" + + await pilot.pause() + assert len(app.input_changed_events) == 1 + assert app.input_changed_events[0].value == "foo" From f723f3771f5fe71735a91e002e555c5e2c1bb460 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 23 Feb 2023 13:51:34 +0000 Subject: [PATCH 02/25] changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e9eb0a49..8242d5dac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added horizontal rule to Markdown https://github.com/Textualize/textual/pull/1832 - Added `Widget.disabled` https://github.com/Textualize/textual/pull/1785 - Added `DOMNode.notify_style_update` to replace `messages.StylesUpdated` message https://github.com/Textualize/textual/pull/1861 -- Added `MessagePump.prevent` context manager to temporarily suppress a given message type +- Added `MessagePump.prevent` context manager to temporarily suppress a given message type https://github.com/Textualize/textual/pull/1866 ### Changed From 4cf7492a280a59231f0d7995a22b318b3f521ff9 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 23 Feb 2023 13:53:36 +0000 Subject: [PATCH 03/25] docstrings --- src/textual/message_pump.py | 8 ++++++++ src/textual/widget.py | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 4391103cc..061743722 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -152,6 +152,14 @@ class MessagePump(metaclass=MessagePumpMeta): self._parent = None def check_message_enabled(self, message: Message) -> bool: + """Check if a given message is enabled (allowed to be sent). + + Args: + message: A message object + + Returns: + `True` if the message will be sent, or `False` if it is disabled. + """ message_type = type(message) if self._prevent_events and message_type in self._prevent_events[-1]: return False diff --git a/src/textual/widget.py b/src/textual/widget.py index 22faeff55..68a5c56f7 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -2489,6 +2489,14 @@ class Widget(DOMNode): self.app.capture_mouse(None) def check_message_enabled(self, message: Message) -> bool: + """Check if a given message is enabled (allowed to be sent). + + Args: + message: A message object + + Returns: + `True` if the message will be sent, or `False` if it is disabled. + """ # Do the normal checking and get out if that fails. if not super().check_message_enabled(message): return False From 2ee61f95dbf57b4bbe7c7af29826ffd22be6632e Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 23 Feb 2023 14:37:47 +0000 Subject: [PATCH 04/25] Docs --- docs/guide/events.md | 28 +++++++++++++++++++++++++++- src/textual/message_pump.py | 4 ++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/docs/guide/events.md b/docs/guide/events.md index 7c3df74c4..3a6da3dae 100644 --- a/docs/guide/events.md +++ b/docs/guide/events.md @@ -108,7 +108,7 @@ The message class is defined within the widget class itself. This is not strictl - 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 events +## 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. @@ -118,6 +118,32 @@ There are other ways of sending (posting) messages, which you may need to use le - [post_message_no_wait][textual.message_pump.MessagePump.post_message_no_wait] The non-async version of `post_message`. +## Preventing messages + +You can *temporarily* prevent a Widget or App from posting messages or events of a particular type by calling [prevent][textual.message_pump.MessagePump.prevent], which returns a context manager. This is typically used when updating data in a child widget and you don't want to receive notifications that something has changed. + +The following example will play the terminal bell as you type, by handling the [Input.Changed][textual.widgets.Input.Changed] event. There's also a button to clear the input by setting `input.value` to empty string. We don't want to play the bell when the button is clicked so we wrap setting the attribute with a call to [prevent][textual.message_pump.MessagePump.prevent]. + +!!! tip + + In reality, playing the terminal bell as you type would be very irritating -- we don't recommend it! + +=== "prevent.py" + + ```python title="prevent.py" + --8<-- "docs/examples/events/prevent.py" + ``` + + 1. Clear the input without sending an Input.Changed event. + 2. Plays the terminal sound when typing. + +=== "Output" + + ```{.textual path="docs/examples/events/prevent.py"} + ``` + + + ## Message handlers Most of the logic in a Textual app will be written in message handlers. Let's explore handlers in more detail. diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 061743722..bea707a02 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -546,11 +546,11 @@ class MessagePump(metaclass=MessagePumpMeta): """A context manager to *temporarily* prevent the given message types from being posted. Example: - + ```python input = self.query_one(Input) with self.prevent(Input.Changed): input.value = "foo" - + ``` """ if self._prevent_events: From da9e28d4d67a76cc72b9574a651ff049e920213b Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 23 Feb 2023 14:41:44 +0000 Subject: [PATCH 05/25] Add example --- docs/examples/events/prevent.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 docs/examples/events/prevent.py diff --git a/docs/examples/events/prevent.py b/docs/examples/events/prevent.py new file mode 100644 index 000000000..66ffb034c --- /dev/null +++ b/docs/examples/events/prevent.py @@ -0,0 +1,26 @@ +from textual.app import App, ComposeResult +from textual.containers import Horizontal +from textual.widgets import Button, Input + + +class PreventApp(App): + """Demonstrates `prevent` context manager.""" + + def compose(self) -> ComposeResult: + yield Input() + yield Button("Clear", id="clear") + + def on_button_pressed(self) -> None: + """Clear the text input.""" + input = self.query_one(Input) + with input.prevent(Input.Changed): # (1) + input.value = "" + + def on_input_changed(self) -> None: + """Called as the user types.""" + self.bell() # (2) + + +if __name__ == "__main__": + app = PreventApp() + app.run() From 0f1251d0bf4b8c33a43741681a9fc7875b4d0efe Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 23 Feb 2023 15:22:40 +0000 Subject: [PATCH 06/25] pass prevent to messages --- src/textual/message.py | 2 ++ src/textual/message_pump.py | 43 +++++++++++++++++++++++++++---------- src/textual/reactive.py | 4 +++- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/textual/message.py b/src/textual/message.py index 7ed72789b..8f6816619 100644 --- a/src/textual/message.py +++ b/src/textual/message.py @@ -31,6 +31,7 @@ class Message: "_no_default_action", "_stop_propagation", "_handler_name", + "_prevent", ] sender: MessageTarget @@ -50,6 +51,7 @@ class Message: self._handler_name = ( f"on_{self.namespace}_{name}" if self.namespace else f"on_{name}" ) + self._prevent: set[type[Message]] = set() super().__init__() def __rich_repr__(self) -> rich.repr.Result: diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index bea707a02..b56f5bc03 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -79,7 +79,7 @@ class MessagePump(metaclass=MessagePumpMeta): self._max_idle: float | None = None self._mounted_event = asyncio.Event() self._next_callbacks: list[CallbackType] = [] - self._prevent_events: list[set[type[Message]]] = [] + self._prevent_message_types_stack: list[set[type[Message]]] = [] @property def task(self) -> Task: @@ -161,7 +161,10 @@ class MessagePump(metaclass=MessagePumpMeta): `True` if the message will be sent, or `False` if it is disabled. """ message_type = type(message) - if self._prevent_events and message_type in self._prevent_events[-1]: + if ( + self._prevent_message_types_stack + and message_type in self._prevent_message_types_stack[-1] + ): return False return type(message) not in self._disabled_messages @@ -472,11 +475,12 @@ class MessagePump(metaclass=MessagePumpMeta): if message.no_dispatch: return - # Allow apps to treat events and messages separately - if isinstance(message, Event): - await self.on_event(message) - else: - await self._on_message(message) + with self.prevent(*message._prevent): + # Allow apps to treat events and messages separately + if isinstance(message, Event): + await self.on_event(message) + else: + await self._on_message(message) await self._flush_next_callbacks() def _get_dispatch_methods( @@ -553,14 +557,27 @@ class MessagePump(metaclass=MessagePumpMeta): ``` """ - if self._prevent_events: - self._prevent_events.append(self._prevent_events[-1].union(message_types)) + if self._prevent_message_types_stack: + self._prevent_message_types_stack.append( + self._prevent_message_types_stack[-1].union(message_types) + ) else: - self._prevent_events.append(set(message_types)) + self._prevent_message_types_stack.append(set(message_types)) try: yield finally: - self._prevent_events.pop() + self._prevent_message_types_stack.pop() + + def is_prevented(self, message_type: type[Message]) -> bool: + """Check if a message type is currently prevented from posting with [prevent][textual.message_pump.MessagePump.prevent]. + + Args: + message_type: A message type. + + Returns: + `True` if the message type is currently prevented, otherwise `False` + """ + return message_type in self._prevent_message_types_stack async def post_message(self, message: Message) -> bool: """Post a message or an event to this message pump. @@ -577,6 +594,8 @@ class MessagePump(metaclass=MessagePumpMeta): return False if not self.check_message_enabled(message): return True + if self._prevent_message_types_stack: + message._prevent.update(self._prevent_message_types_stack[-1]) await self._message_queue.put(message) return True @@ -599,6 +618,8 @@ class MessagePump(metaclass=MessagePumpMeta): return False if not self.check_message_enabled(message): return False + if self._prevent_message_types_stack: + message._prevent.update(self._prevent_message_types_stack[-1]) await self._message_queue.put(message) return True diff --git a/src/textual/reactive.py b/src/textual/reactive.py index 140cb0ff1..d074b1d92 100644 --- a/src/textual/reactive.py +++ b/src/textual/reactive.py @@ -237,7 +237,9 @@ class Reactive(Generic[ReactiveType]): events.Callback( sender=obj, callback=partial(await_watcher, watch_result), - prevent=obj._prevent_events[0] if obj._prevent_events else None, + prevent=obj._prevent_message_types_stack[0] + if obj._prevent_message_types_stack + else None, ) ) From fd9ce05305e19045af1fe161201bf7f3d237fb01 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 23 Feb 2023 15:26:41 +0000 Subject: [PATCH 07/25] prevent messages stack --- src/textual/message_pump.py | 41 +++++++++---------------------------- 1 file changed, 10 insertions(+), 31 deletions(-) diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index b56f5bc03..c2bcb701c 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -161,10 +161,7 @@ class MessagePump(metaclass=MessagePumpMeta): `True` if the message will be sent, or `False` if it is disabled. """ message_type = type(message) - if ( - self._prevent_message_types_stack - and message_type in self._prevent_message_types_stack[-1] - ): + if self._prevent_events and message_type in self._prevent_events[-1]: return False return type(message) not in self._disabled_messages @@ -475,12 +472,11 @@ class MessagePump(metaclass=MessagePumpMeta): if message.no_dispatch: return - with self.prevent(*message._prevent): - # Allow apps to treat events and messages separately - if isinstance(message, Event): - await self.on_event(message) - else: - await self._on_message(message) + # Allow apps to treat events and messages separately + if isinstance(message, Event): + await self.on_event(message) + else: + await self._on_message(message) await self._flush_next_callbacks() def _get_dispatch_methods( @@ -557,27 +553,14 @@ class MessagePump(metaclass=MessagePumpMeta): ``` """ - if self._prevent_message_types_stack: - self._prevent_message_types_stack.append( - self._prevent_message_types_stack[-1].union(message_types) - ) + if self._prevent_events: + self._prevent_events.append(self._prevent_events[-1].union(message_types)) else: - self._prevent_message_types_stack.append(set(message_types)) + self._prevent_events.append(set(message_types)) try: yield finally: - self._prevent_message_types_stack.pop() - - def is_prevented(self, message_type: type[Message]) -> bool: - """Check if a message type is currently prevented from posting with [prevent][textual.message_pump.MessagePump.prevent]. - - Args: - message_type: A message type. - - Returns: - `True` if the message type is currently prevented, otherwise `False` - """ - return message_type in self._prevent_message_types_stack + self._prevent_events.pop() async def post_message(self, message: Message) -> bool: """Post a message or an event to this message pump. @@ -594,8 +577,6 @@ class MessagePump(metaclass=MessagePumpMeta): return False if not self.check_message_enabled(message): return True - if self._prevent_message_types_stack: - message._prevent.update(self._prevent_message_types_stack[-1]) await self._message_queue.put(message) return True @@ -618,8 +599,6 @@ class MessagePump(metaclass=MessagePumpMeta): return False if not self.check_message_enabled(message): return False - if self._prevent_message_types_stack: - message._prevent.update(self._prevent_message_types_stack[-1]) await self._message_queue.put(message) return True From c2ea074f4af0876c3452fd2370afc8ea9eb9822c Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 23 Feb 2023 15:28:11 +0000 Subject: [PATCH 08/25] fix name --- src/textual/message_pump.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index c2bcb701c..4eafa15fb 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -161,7 +161,10 @@ class MessagePump(metaclass=MessagePumpMeta): `True` if the message will be sent, or `False` if it is disabled. """ message_type = type(message) - if self._prevent_events and message_type in self._prevent_events[-1]: + if ( + self._prevent_message_types_stack + and message_type in self._prevent_message_types_stack[-1] + ): return False return type(message) not in self._disabled_messages @@ -553,14 +556,16 @@ class MessagePump(metaclass=MessagePumpMeta): ``` """ - if self._prevent_events: - self._prevent_events.append(self._prevent_events[-1].union(message_types)) + if self._prevent_message_types_stack: + self._prevent_message_types_stack.append( + self._prevent_message_types_stack[-1].union(message_types) + ) else: - self._prevent_events.append(set(message_types)) + self._prevent_message_types_stack.append(set(message_types)) try: yield finally: - self._prevent_events.pop() + self._prevent_message_types_stack.pop() async def post_message(self, message: Message) -> bool: """Post a message or an event to this message pump. From ba30e0dd66a9df4e5ba3eb287da6b0b9c89e46b3 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 23 Feb 2023 15:34:02 +0000 Subject: [PATCH 09/25] set prevent --- src/textual/message_pump.py | 4 ++++ src/textual/reactive.py | 8 +++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 4eafa15fb..89183175a 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -582,6 +582,8 @@ class MessagePump(metaclass=MessagePumpMeta): return False if not self.check_message_enabled(message): return True + if self._prevent_message_types_stack: + message._prevent.update(self._prevent_message_types_stack[-1]) await self._message_queue.put(message) return True @@ -620,6 +622,8 @@ class MessagePump(metaclass=MessagePumpMeta): return False if not self.check_message_enabled(message): return False + if self._prevent_message_types_stack: + message._prevent.update(self._prevent_message_types_stack[-1]) self._message_queue.put_nowait(message) return True diff --git a/src/textual/reactive.py b/src/textual/reactive.py index d074b1d92..a64bdc594 100644 --- a/src/textual/reactive.py +++ b/src/textual/reactive.py @@ -237,9 +237,11 @@ class Reactive(Generic[ReactiveType]): events.Callback( sender=obj, callback=partial(await_watcher, watch_result), - prevent=obj._prevent_message_types_stack[0] - if obj._prevent_message_types_stack - else None, + prevent=( + obj._prevent_message_types_stack[0] + if obj._prevent_message_types_stack + else None + ), ) ) From fd13b33cce47640397c9b9f4ba949858e52aac0e Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 23 Feb 2023 16:05:11 +0000 Subject: [PATCH 10/25] prevent message stack --- src/textual/message_pump.py | 16 +++++----------- src/textual/reactive.py | 11 ++++------- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 89183175a..2d78cca64 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -79,7 +79,7 @@ class MessagePump(metaclass=MessagePumpMeta): self._max_idle: float | None = None self._mounted_event = asyncio.Event() self._next_callbacks: list[CallbackType] = [] - self._prevent_message_types_stack: list[set[type[Message]]] = [] + self._prevent_message_types_stack: list[set[type[Message]]] = [set()] @property def task(self) -> Task: @@ -161,10 +161,7 @@ class MessagePump(metaclass=MessagePumpMeta): `True` if the message will be sent, or `False` if it is disabled. """ message_type = type(message) - if ( - self._prevent_message_types_stack - and message_type in self._prevent_message_types_stack[-1] - ): + if message_type in self._prevent_message_types_stack[-1]: return False return type(message) not in self._disabled_messages @@ -556,12 +553,9 @@ class MessagePump(metaclass=MessagePumpMeta): ``` """ - if self._prevent_message_types_stack: - self._prevent_message_types_stack.append( - self._prevent_message_types_stack[-1].union(message_types) - ) - else: - self._prevent_message_types_stack.append(set(message_types)) + self._prevent_message_types_stack.append( + self._prevent_message_types_stack[-1].union(message_types) + ) try: yield finally: diff --git a/src/textual/reactive.py b/src/textual/reactive.py index a64bdc594..81085db77 100644 --- a/src/textual/reactive.py +++ b/src/textual/reactive.py @@ -237,11 +237,7 @@ class Reactive(Generic[ReactiveType]): events.Callback( sender=obj, callback=partial(await_watcher, watch_result), - prevent=( - obj._prevent_message_types_stack[0] - if obj._prevent_message_types_stack - else None - ), + prevent=obj._prevent_message_types_stack[-1], ) ) @@ -259,8 +255,9 @@ class Reactive(Generic[ReactiveType]): for reactable, callback in watchers if reactable.is_attached and not reactable._closing ] - for _, callback in watchers: - invoke_watcher(callback, old_value, value) + for reactable, callback in watchers: + with reactable.prevent(*obj._prevent_message_types_stack[-1]): + invoke_watcher(callback, old_value, value) @classmethod def _compute(cls, obj: Reactable) -> None: From 2f104a5db41c4f2d0e783bede7806abfe89d83eb Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 23 Feb 2023 17:41:13 +0000 Subject: [PATCH 11/25] pass prevented messages types in post --- src/textual/dom.py | 29 ++++++++++++++++- src/textual/message_pump.py | 62 +++++++++++++++++-------------------- src/textual/widget.py | 3 ++ 3 files changed, 60 insertions(+), 34 deletions(-) diff --git a/src/textual/dom.py b/src/textual/dom.py index 9980c9471..22eaa3d00 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -1,11 +1,13 @@ from __future__ import annotations import re -from functools import lru_cache +from contextlib import contextmanager +from functools import lru_cache, reduce from inspect import getfile from typing import ( TYPE_CHECKING, ClassVar, + Generator, Iterable, Sequence, Type, @@ -39,6 +41,7 @@ from .walk import walk_breadth_first, walk_depth_first if TYPE_CHECKING: from .app import App + from .messages import Message from .css.query import DOMQuery from .screen import Screen from .widget import Widget @@ -202,6 +205,30 @@ class DOMNode(MessagePump): cls._merged_bindings = cls._merge_bindings() cls._css_type_names = frozenset(css_type_names) + def is_prevented(self, message_type: type[Message]) -> bool: + """Check if a message type has been prevented via the [prevent][textual.message_pump.MessagePump.prevent] context manager. + + Args: + message_type: A message type. + + Returns: + `True` if the message has been prevented from sending, or `False` if it will be sent as normal. + """ + return any( + message_type in node._prevent_message_types_stack[-1] + for node in self.ancestors_with_self + ) + + @property + def prevented_messages(self) -> set[type[Message]]: + """A set of all the prevented message types.""" + return set().union( + *[ + node._prevent_message_types_stack[-1] + for node in self.ancestors_with_self + ] + ) + def get_component_styles(self, name: str) -> RenderStyles: """Get a "component" styles object (must be defined in COMPONENT_CLASSES classvar). diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 2d78cca64..49d2336d9 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -81,6 +81,26 @@ class MessagePump(metaclass=MessagePumpMeta): self._next_callbacks: list[CallbackType] = [] self._prevent_message_types_stack: list[set[type[Message]]] = [set()] + @contextmanager + def prevent(self, *message_types: type[Message]) -> Generator[None, None, None]: + """A context manager to *temporarily* prevent the given message types from being posted. + + Example: + ```python + input = self.query_one(Input) + with self.prevent(Input.Changed): + input.value = "foo" + ``` + + """ + self._prevent_message_types_stack.append( + self._prevent_message_types_stack[-1].union(message_types) + ) + try: + yield + finally: + self._prevent_message_types_stack.pop() + @property def task(self) -> Task: assert self._task is not None @@ -160,9 +180,6 @@ class MessagePump(metaclass=MessagePumpMeta): Returns: `True` if the message will be sent, or `False` if it is disabled. """ - message_type = type(message) - if message_type in self._prevent_message_types_stack[-1]: - return False return type(message) not in self._disabled_messages def disable_messages(self, *messages: type[Message]) -> None: @@ -472,12 +489,13 @@ class MessagePump(metaclass=MessagePumpMeta): if message.no_dispatch: return - # Allow apps to treat events and messages separately - if isinstance(message, Event): - await self.on_event(message) - else: - await self._on_message(message) - await self._flush_next_callbacks() + with self.prevent(*message._prevent): + # Allow apps to treat events and messages separately + if isinstance(message, Event): + await self.on_event(message) + else: + await self._on_message(message) + await self._flush_next_callbacks() def _get_dispatch_methods( self, method_name: str, message: Message @@ -541,26 +559,6 @@ class MessagePump(metaclass=MessagePumpMeta): if self._message_queue.empty(): self.post_message_no_wait(messages.Prompt(sender=self)) - @contextmanager - def prevent(self, *message_types: type[Message]) -> Generator[None, None, None]: - """A context manager to *temporarily* prevent the given message types from being posted. - - Example: - ```python - input = self.query_one(Input) - with self.prevent(Input.Changed): - input.value = "foo" - ``` - - """ - self._prevent_message_types_stack.append( - self._prevent_message_types_stack[-1].union(message_types) - ) - try: - yield - finally: - self._prevent_message_types_stack.pop() - async def post_message(self, message: Message) -> bool: """Post a message or an event to this message pump. @@ -576,8 +574,7 @@ class MessagePump(metaclass=MessagePumpMeta): return False if not self.check_message_enabled(message): return True - if self._prevent_message_types_stack: - message._prevent.update(self._prevent_message_types_stack[-1]) + message._prevent.update(self.prevented_messages) await self._message_queue.put(message) return True @@ -616,8 +613,7 @@ class MessagePump(metaclass=MessagePumpMeta): return False if not self.check_message_enabled(message): return False - if self._prevent_message_types_stack: - message._prevent.update(self._prevent_message_types_stack[-1]) + message._prevent.update(self.prevented_messages) self._message_queue.put_nowait(message) return True diff --git a/src/textual/widget.py b/src/textual/widget.py index 68a5c56f7..ad9b87cc5 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -2500,6 +2500,9 @@ class Widget(DOMNode): # Do the normal checking and get out if that fails. if not super().check_message_enabled(message): return False + message_type = type(message) + if self.is_prevented(message_type): + return False # Otherwise, if this is a mouse event, the widget receiving the # event must not be disabled at this moment. return ( From 9579b9fd24154198f4314a8d9a1ac9e1caea545e Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 23 Feb 2023 21:15:05 +0000 Subject: [PATCH 12/25] prevent API refinement --- src/textual/dom.py | 9 ++++----- src/textual/message_pump.py | 8 ++++++-- src/textual/widget.py | 2 +- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/textual/dom.py b/src/textual/dom.py index 22eaa3d00..aa59f6f6b 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -205,7 +205,7 @@ class DOMNode(MessagePump): cls._merged_bindings = cls._merge_bindings() cls._css_type_names = frozenset(css_type_names) - def is_prevented(self, message_type: type[Message]) -> bool: + def _is_prevented(self, message_type: type[Message]) -> bool: """Check if a message type has been prevented via the [prevent][textual.message_pump.MessagePump.prevent] context manager. Args: @@ -219,14 +219,13 @@ class DOMNode(MessagePump): for node in self.ancestors_with_self ) - @property - def prevented_messages(self) -> set[type[Message]]: + def _get_prevented_messages(self) -> set[type[Message]]: """A set of all the prevented message types.""" return set().union( - *[ + *( node._prevent_message_types_stack[-1] for node in self.ancestors_with_self - ] + ) ) def get_component_styles(self, name: str) -> RenderStyles: diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 49d2336d9..9a866d82d 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -81,6 +81,10 @@ class MessagePump(metaclass=MessagePumpMeta): self._next_callbacks: list[CallbackType] = [] self._prevent_message_types_stack: list[set[type[Message]]] = [set()] + def _get_prevented_messages(self) -> set[type[Message]]: + """A set of all the prevented message types.""" + return set() + @contextmanager def prevent(self, *message_types: type[Message]) -> Generator[None, None, None]: """A context manager to *temporarily* prevent the given message types from being posted. @@ -574,7 +578,7 @@ class MessagePump(metaclass=MessagePumpMeta): return False if not self.check_message_enabled(message): return True - message._prevent.update(self.prevented_messages) + message._prevent.update(self._get_prevented_messages()) await self._message_queue.put(message) return True @@ -613,7 +617,7 @@ class MessagePump(metaclass=MessagePumpMeta): return False if not self.check_message_enabled(message): return False - message._prevent.update(self.prevented_messages) + message._prevent.update(self._get_prevented_messages()) self._message_queue.put_nowait(message) return True diff --git a/src/textual/widget.py b/src/textual/widget.py index ad9b87cc5..47e1b431d 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -2501,7 +2501,7 @@ class Widget(DOMNode): if not super().check_message_enabled(message): return False message_type = type(message) - if self.is_prevented(message_type): + if self._is_prevented(message_type): return False # Otherwise, if this is a mouse event, the widget receiving the # event must not be disabled at this moment. From 0894881950571affe7d2fec0245a5b908dbc90f3 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 23 Feb 2023 21:18:30 +0000 Subject: [PATCH 13/25] simplify --- src/textual/message_pump.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 9a866d82d..0db654bc7 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -97,13 +97,12 @@ class MessagePump(metaclass=MessagePumpMeta): ``` """ - self._prevent_message_types_stack.append( - self._prevent_message_types_stack[-1].union(message_types) - ) + prevent_stack = self._prevent_message_types_stack + prevent_stack.append(prevent_stack[-1].union(message_types)) try: yield finally: - self._prevent_message_types_stack.pop() + prevent_stack.pop() @property def task(self) -> Task: From 3231b7a8b83eeb9e3aac6da6f9a8a62a93f35f4f Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 23 Feb 2023 21:22:24 +0000 Subject: [PATCH 14/25] remove prevent from callback --- src/textual/events.py | 2 -- src/textual/message_pump.py | 6 +----- src/textual/reactive.py | 4 +--- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/textual/events.py b/src/textual/events.py index 34f302fe2..d4c1625b2 100644 --- a/src/textual/events.py +++ b/src/textual/events.py @@ -32,10 +32,8 @@ class Callback(Event, bubble=False, verbose=True): self, sender: MessageTarget, callback: Callable[[], Awaitable[None]], - prevent: set[type[Message]] | None = None, ) -> None: self.callback = callback - self.prevent = frozenset(prevent) if prevent else None super().__init__(sender) def __rich_repr__(self) -> rich.repr.Result: diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 0db654bc7..31922a8d5 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -631,11 +631,7 @@ class MessagePump(metaclass=MessagePumpMeta): return self.post_message_no_wait(message) async def on_callback(self, event: events.Callback) -> None: - if event.prevent: - with self.prevent(*event.prevent): - await invoke(event.callback) - else: - await invoke(event.callback) + await invoke(event.callback) # TODO: Does dispatch_key belong on message pump? async def dispatch_key(self, event: events.Key) -> bool: diff --git a/src/textual/reactive.py b/src/textual/reactive.py index 81085db77..ae5cf9d6d 100644 --- a/src/textual/reactive.py +++ b/src/textual/reactive.py @@ -235,9 +235,7 @@ class Reactive(Generic[ReactiveType]): # 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), - prevent=obj._prevent_message_types_stack[-1], + sender=obj, callback=partial(await_watcher, watch_result) ) ) From b2ab5b68869b2acd2c14a85bcc8b18813bf520e9 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 23 Feb 2023 21:36:33 +0000 Subject: [PATCH 15/25] docs --- docs/guide/events.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guide/events.md b/docs/guide/events.md index 3a6da3dae..299bbd9d9 100644 --- a/docs/guide/events.md +++ b/docs/guide/events.md @@ -120,9 +120,9 @@ There are other ways of sending (posting) messages, which you may need to use le ## Preventing messages -You can *temporarily* prevent a Widget or App from posting messages or events of a particular type by calling [prevent][textual.message_pump.MessagePump.prevent], which returns a context manager. This is typically used when updating data in a child widget and you don't want to receive notifications that something has changed. +You can *temporarily* disable posting of messages of a particular type by calling [prevent][textual.message_pump.MessagePump.prevent], which returns a context manager (used with Python's `with` keyword). This is typically used when updating data in a child widget and you don't want to receive notifications that something has changed. -The following example will play the terminal bell as you type, by handling the [Input.Changed][textual.widgets.Input.Changed] event. There's also a button to clear the input by setting `input.value` to empty string. We don't want to play the bell when the button is clicked so we wrap setting the attribute with a call to [prevent][textual.message_pump.MessagePump.prevent]. +The following example will play the terminal bell as you type. It does this by handling [Input.Changed][textual.widgets.Input.Changed] and calling [bell()][textual.app.App.bell]. There is a Clear button which sets the input's value to an empty string. This would normally also result in a `Input.Changed` event being sent (and the bell playing). Since don't want the button to make a sound, the assignment to `value` is wrapped in with a [prevent][textual.message_pump.MessagePump.prevent] context manager. !!! tip From d1d6f03b2d4fee3a9dff3adbe43b99660a6a635f Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 23 Feb 2023 21:39:45 +0000 Subject: [PATCH 16/25] microoptmization --- src/textual/message_pump.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 31922a8d5..faf92ac38 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -97,12 +97,15 @@ class MessagePump(metaclass=MessagePumpMeta): ``` """ - prevent_stack = self._prevent_message_types_stack - prevent_stack.append(prevent_stack[-1].union(message_types)) - try: + if message_types: + prevent_stack = self._prevent_message_types_stack + prevent_stack.append(prevent_stack[-1].union(message_types)) + try: + yield + finally: + prevent_stack.pop() + else: yield - finally: - prevent_stack.pop() @property def task(self) -> Task: From 7c470fc5dd11cfb423102aa11044747d6c847032 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 24 Feb 2023 08:44:40 +0000 Subject: [PATCH 17/25] Move prevent stack to contextvar --- src/textual/_context.py | 6 ++++++ src/textual/dom.py | 23 ----------------------- src/textual/message_pump.py | 36 +++++++++++++++++++++++++++++++++--- 3 files changed, 39 insertions(+), 26 deletions(-) diff --git a/src/textual/_context.py b/src/textual/_context.py index 6a5562476..9dc5d8ca3 100644 --- a/src/textual/_context.py +++ b/src/textual/_context.py @@ -1,8 +1,11 @@ +from __future__ import annotations + from contextvars import ContextVar from typing import TYPE_CHECKING if TYPE_CHECKING: from .app import App + from .message import Message from .message_pump import MessagePump @@ -12,3 +15,6 @@ class NoActiveAppError(RuntimeError): active_app: ContextVar["App"] = ContextVar("active_app") active_message_pump: ContextVar["MessagePump"] = ContextVar("active_message_pump") +prevent_message_types_stack: ContextVar[list[set[type[Message]]]] = ContextVar( + "prevent_message_types_stack" +) diff --git a/src/textual/dom.py b/src/textual/dom.py index aa59f6f6b..62997576f 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -205,29 +205,6 @@ class DOMNode(MessagePump): cls._merged_bindings = cls._merge_bindings() cls._css_type_names = frozenset(css_type_names) - def _is_prevented(self, message_type: type[Message]) -> bool: - """Check if a message type has been prevented via the [prevent][textual.message_pump.MessagePump.prevent] context manager. - - Args: - message_type: A message type. - - Returns: - `True` if the message has been prevented from sending, or `False` if it will be sent as normal. - """ - return any( - message_type in node._prevent_message_types_stack[-1] - for node in self.ancestors_with_self - ) - - def _get_prevented_messages(self) -> set[type[Message]]: - """A set of all the prevented message types.""" - return set().union( - *( - node._prevent_message_types_stack[-1] - for node in self.ancestors_with_self - ) - ) - def get_component_styles(self, name: str) -> RenderStyles: """Get a "component" styles object (must be defined in COMPONENT_CLASSES classvar). diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index faf92ac38..013a7e865 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -18,7 +18,12 @@ from weakref import WeakSet from . import Logger, events, log, messages from ._asyncio import create_task from ._callback import invoke -from ._context import NoActiveAppError, active_app, active_message_pump +from ._context import ( + NoActiveAppError, + active_app, + active_message_pump, + prevent_message_types_stack, +) from ._time import time from ._types import CallbackType from .case import camel_to_snake @@ -79,11 +84,36 @@ class MessagePump(metaclass=MessagePumpMeta): self._max_idle: float | None = None self._mounted_event = asyncio.Event() self._next_callbacks: list[CallbackType] = [] - self._prevent_message_types_stack: list[set[type[Message]]] = [set()] + + @property + def _prevent_message_types_stack(self) -> list[set[type[Message]]]: + """A stack that manages prevented messages. + + Returns: + A list of sets of Message Types. + """ + try: + stack = prevent_message_types_stack.get() + except LookupError: + stack = [set()] + prevent_message_types_stack.set(stack) + return stack def _get_prevented_messages(self) -> set[type[Message]]: """A set of all the prevented message types.""" - return set() + return self._prevent_message_types_stack[-1] + + def _is_prevented(self, message_type: type[Message]) -> bool: + """Check if a message type has been prevented via the + [prevent][textual.message_pump.MessagePump.prevent] context manager. + + Args: + message_type: A message type. + + Returns: + `True` if the message has been prevented from sending, or `False` if it will be sent as normal. + """ + return message_type in self._prevent_message_types_stack[-1] @contextmanager def prevent(self, *message_types: type[Message]) -> Generator[None, None, None]: From 099911481141207c9e3ef4c0fefe31933ca5762e Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 24 Feb 2023 12:02:20 +0000 Subject: [PATCH 18/25] Update docs/guide/events.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com> --- docs/guide/events.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/events.md b/docs/guide/events.md index 299bbd9d9..09f9a7043 100644 --- a/docs/guide/events.md +++ b/docs/guide/events.md @@ -122,7 +122,7 @@ There are other ways of sending (posting) messages, which you may need to use le You can *temporarily* disable posting of messages of a particular type by calling [prevent][textual.message_pump.MessagePump.prevent], which returns a context manager (used with Python's `with` keyword). This is typically used when updating data in a child widget and you don't want to receive notifications that something has changed. -The following example will play the terminal bell as you type. It does this by handling [Input.Changed][textual.widgets.Input.Changed] and calling [bell()][textual.app.App.bell]. There is a Clear button which sets the input's value to an empty string. This would normally also result in a `Input.Changed` event being sent (and the bell playing). Since don't want the button to make a sound, the assignment to `value` is wrapped in with a [prevent][textual.message_pump.MessagePump.prevent] context manager. +The following example will play the terminal bell as you type. It does this by handling [Input.Changed][textual.widgets.Input.Changed] and calling [bell()][textual.app.App.bell]. There is a Clear button which sets the input's value to an empty string. This would normally also result in a `Input.Changed` event being sent (and the bell playing). Since we don't want the button to make a sound, the assignment to `value` is wrapped within a [prevent][textual.message_pump.MessagePump.prevent] context manager. !!! tip From 217956bbdff01509322ab28a90b03135c520d206 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 24 Feb 2023 12:02:46 +0000 Subject: [PATCH 19/25] Update docs/examples/events/prevent.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com> --- docs/examples/events/prevent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/events/prevent.py b/docs/examples/events/prevent.py index 66ffb034c..e8fd0de05 100644 --- a/docs/examples/events/prevent.py +++ b/docs/examples/events/prevent.py @@ -13,7 +13,7 @@ class PreventApp(App): def on_button_pressed(self) -> None: """Clear the text input.""" input = self.query_one(Input) - with input.prevent(Input.Changed): # (1) + with input.prevent(Input.Changed): # (1)! input.value = "" def on_input_changed(self) -> None: From 63438f170def9bbdadf42d3a58d8d543b9939cd6 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 24 Feb 2023 12:02:56 +0000 Subject: [PATCH 20/25] Update docs/examples/events/prevent.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com> --- docs/examples/events/prevent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/events/prevent.py b/docs/examples/events/prevent.py index e8fd0de05..39fe437c2 100644 --- a/docs/examples/events/prevent.py +++ b/docs/examples/events/prevent.py @@ -18,7 +18,7 @@ class PreventApp(App): def on_input_changed(self) -> None: """Called as the user types.""" - self.bell() # (2) + self.bell() # (2)! if __name__ == "__main__": From 884db9c39c8039aaef948295a11a64f80f2ab5ce Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 24 Feb 2023 12:03:05 +0000 Subject: [PATCH 21/25] Update src/textual/dom.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com> --- src/textual/dom.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/textual/dom.py b/src/textual/dom.py index 62997576f..51497d221 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -1,8 +1,7 @@ from __future__ import annotations import re -from contextlib import contextmanager -from functools import lru_cache, reduce +from functools import lru_cache from inspect import getfile from typing import ( TYPE_CHECKING, From 15cd3ef5ed9403e910397cb5aa97f0cc555aa64f Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 24 Feb 2023 12:03:16 +0000 Subject: [PATCH 22/25] Update src/textual/dom.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com> --- src/textual/dom.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/textual/dom.py b/src/textual/dom.py index 51497d221..55ff4ffd6 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -6,7 +6,6 @@ from inspect import getfile from typing import ( TYPE_CHECKING, ClassVar, - Generator, Iterable, Sequence, Type, From a5f139b45e17e2918ad609bf8f619134e810e533 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 24 Feb 2023 12:04:08 +0000 Subject: [PATCH 23/25] Update src/textual/message_pump.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com> --- src/textual/message_pump.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 013a7e865..d3704da22 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -87,11 +87,7 @@ class MessagePump(metaclass=MessagePumpMeta): @property def _prevent_message_types_stack(self) -> list[set[type[Message]]]: - """A stack that manages prevented messages. - - Returns: - A list of sets of Message Types. - """ + """The stack that manages prevented messages.""" try: stack = prevent_message_types_stack.get() except LookupError: From 03ffcdab0ff147e9b2270bd88cb8753e434afb7b Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 24 Feb 2023 12:05:24 +0000 Subject: [PATCH 24/25] Update src/textual/message_pump.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com> --- src/textual/message_pump.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index d3704da22..9122175d6 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -207,7 +207,7 @@ class MessagePump(metaclass=MessagePumpMeta): """Check if a given message is enabled (allowed to be sent). Args: - message: A message object + message: A message object. Returns: `True` if the message will be sent, or `False` if it is disabled. From 825f21fe2865b5d7c1b0e38b433c3b19e71541ee Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 24 Feb 2023 12:09:50 +0000 Subject: [PATCH 25/25] comments --- src/textual/message_pump.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 9122175d6..7a2f6d24e 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -606,6 +606,8 @@ class MessagePump(metaclass=MessagePumpMeta): 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 @@ -645,6 +647,8 @@ class MessagePump(metaclass=MessagePumpMeta): return False if not self.check_message_enabled(message): return False + # 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