From 2f1bdea23490544acaa252a580976b6e61be925f Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 18 Dec 2022 21:56:38 +0000 Subject: [PATCH 01/10] optional actions --- src/textual/demo.css | 2 +- src/textual/screen.py | 3 -- src/textual/widget.py | 85 +++++++++++++++++++++++++------------------ 3 files changed, 50 insertions(+), 40 deletions(-) diff --git a/src/textual/demo.css b/src/textual/demo.css index 620cae5f03..88ef1b7331 100644 --- a/src/textual/demo.css +++ b/src/textual/demo.css @@ -29,7 +29,7 @@ Sidebar { } Sidebar:focus-within { - offset: 0 0 !important; + offset: 0 0 !important; } Sidebar.-hidden { diff --git a/src/textual/screen.py b/src/textual/screen.py index 0de76479e4..3f89e40f4f 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -295,7 +295,6 @@ def set_focus(self, widget: Widget | None, scroll_visible: bool = True) -> 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.emit_no_wait(events.DescendantBlur(self)) self.focused = None self.log.debug("focus was removed") elif widget.can_focus: @@ -303,14 +302,12 @@ def set_focus(self, widget: Widget | None, scroll_visible: bool = True) -> None: if self.focused is not None: # Blur currently focused widget self.focused.post_message_no_wait(events.Blur(self)) - self.focused.emit_no_wait(events.DescendantBlur(self)) # 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.emit_no_wait(events.DescendantFocus(self)) self.log.debug(widget, "was focused") async def _on_idle(self, event: events.Idle) -> None: diff --git a/src/textual/widget.py b/src/textual/widget.py index 1d5f4365bc..cd45834adb 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -2344,17 +2344,22 @@ def _on_enter(self, event: events.Enter) -> None: self.mouse_over = True def _on_focus(self, event: events.Focus) -> None: - for node in self.ancestors_with_self: - if node._has_focus_within: - self.app.update_styles(node) self.has_focus = True self.refresh() + self.emit_no_wait(events.DescendantFocus(self)) def _on_blur(self, event: events.Blur) -> None: - if any(node._has_focus_within for node in self.ancestors_with_self): - self.app.update_styles(self) self.has_focus = False self.refresh() + self.emit_no_wait(events.DescendantBlur(self)) + + def _on_descendant_blur(self, event: events.DescendantBlur) -> None: + if self._has_focus_within: + self.app.update_styles(self) + + def _on_descendant_focus(self, event: events.DescendantBlur) -> None: + if self._has_focus_within: + self.app.update_styles(self) def _on_mouse_scroll_down(self, event) -> None: if self.allow_vertical_scroll: @@ -2398,34 +2403,42 @@ def _on_hide(self, event: events.Hide) -> None: def _on_scroll_to_region(self, message: messages.ScrollToRegion) -> None: self.scroll_to_region(message.region, animate=True) - def action_scroll_home(self) -> None: - if self._allow_scroll: - self.scroll_home() - - def action_scroll_end(self) -> None: - if self._allow_scroll: - self.scroll_end() - - def action_scroll_left(self) -> None: - if self.allow_horizontal_scroll: - self.scroll_left() - - def action_scroll_right(self) -> None: - if self.allow_horizontal_scroll: - self.scroll_right() - - def action_scroll_up(self) -> None: - if self.allow_vertical_scroll: - self.scroll_up() - - def action_scroll_down(self) -> None: - if self.allow_vertical_scroll: - self.scroll_down() - - def action_page_down(self) -> None: - if self.allow_vertical_scroll: - self.scroll_page_down() - - def action_page_up(self) -> None: - if self.allow_vertical_scroll: - self.scroll_page_up() + def action_scroll_home(self) -> bool | None: + if not self._allow_scroll: + return False + self.scroll_home() + + def action_scroll_end(self) -> bool | None: + if not self._allow_scroll: + return False + self.scroll_end() + + def action_scroll_left(self) -> bool | None: + if not self.allow_horizontal_scroll: + return False + self.scroll_left() + + def action_scroll_right(self) -> bool | None: + if not self.allow_horizontal_scroll: + return False + self.scroll_right() + + def action_scroll_up(self) -> bool | None: + if not self.allow_vertical_scroll: + return False + self.scroll_up() + + def action_scroll_down(self) -> bool | None: + if not self.allow_vertical_scroll: + return False + self.scroll_down() + + def action_page_down(self) -> bool | None: + if not self.allow_vertical_scroll: + return False + self.scroll_page_down() + + def action_page_up(self) -> bool | None: + if not self.allow_vertical_scroll: + return False + self.scroll_page_up() From bb499f68e20caa66c085b0eb057ab01fd8b211bb Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 18 Dec 2022 22:03:19 +0000 Subject: [PATCH 02/10] stop actions --- src/textual/app.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/textual/app.py b/src/textual/app.py index a6b7ad27aa..d0a475a828 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -1753,8 +1753,8 @@ async def check_bindings(self, key: str, priority: bool = False) -> bool: ): binding = bindings.keys.get(key) if binding is not None and binding.priority == priority: - await self.action(binding.action, default_namespace=namespace) - return True + if await self.action(binding.action, namespace) in (True, None): + return True return False async def on_event(self, event: events.Event) -> None: @@ -1843,11 +1843,9 @@ async def _dispatch_action( ) if callable(private_method): - await invoke(private_method, *params) - return True + return await invoke(private_method, *params) elif callable(public_method): - await invoke(public_method, *params) - return True + return await invoke(public_method, *params) return False From af049d4a4d9b5e3235273aedd2a14242d54a7b42 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 19 Dec 2022 09:30:22 +0000 Subject: [PATCH 03/10] added test --- src/textual/app.py | 3 +-- tests/test_binding_inheritance.py | 45 ++++++++++++++++++++++++++++--- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/textual/app.py b/src/textual/app.py index d0a475a828..bdd61771bf 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -1386,11 +1386,10 @@ async def invoke_ready_callback() -> None: raise finally: + self._running = True await self._ready() await invoke_ready_callback() - self._running = True - try: await self._process_messages_loop() except asyncio.CancelledError: diff --git a/tests/test_binding_inheritance.py b/tests/test_binding_inheritance.py index 49cc33092b..3d4c7f89c5 100644 --- a/tests/test_binding_inheritance.py +++ b/tests/test_binding_inheritance.py @@ -11,13 +11,12 @@ from __future__ import annotations -import pytest - from textual.app import App, ComposeResult -from textual.widgets import Static -from textual.screen import Screen from textual.binding import Binding from textual.containers import Container +from textual.screen import Screen +from textual.widget import Widget +from textual.widgets import Static ############################################################################## # These are the movement keys within Textual; they kind of have a special @@ -574,6 +573,7 @@ def compose(self) -> ComposeResult: def on_mount(self) -> None: self.query_one(PriorityOverlapWidget).focus() + class PriorityOverlapApp(AppKeyRecorder): """An application with a priority binding.""" @@ -607,3 +607,40 @@ async def test_overlapping_priority_bindings() -> None: "app_e", "screen_f", ] + + +async def test_skip_action() -> None: + """Test that a binding may be skipped by an action returning False""" + + class Handle(Widget, can_focus=True): + BINDINGS = [("t", "test('foo')", "Test")] + + def action_test(self, text: str) -> None: + self.app.exit(text) + + no_handle_invoked = False + + class NoHandle(Widget, can_focus=True): + BINDINGS = [("t", "test('bar')", "Test")] + + def action_test(self, text: str) -> bool: + nonlocal no_handle_invoked + no_handle_invoked = True + return False + + class SkipApp(App): + def compose(self) -> ComposeResult: + yield Handle(NoHandle()) + + def on_mount(self) -> None: + self.query_one(NoHandle).focus() + + async with SkipApp().run_test() as pilot: + # Check the NoHandle widget has focus + assert pilot.app.query_one(NoHandle).has_focus + # Press the "t" key + await pilot.press("t") + # Check the action on the no handle widget was called + assert no_handle_invoked + # Check the return value, confirming that the action on Handle was called + assert pilot.app.return_value == "foo" From ee59c5882e18fb9df693864f402b598ad87efd9e Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 19 Dec 2022 10:01:06 +0000 Subject: [PATCH 04/10] Added SkipAction exception --- src/textual/actions.py | 4 +++ src/textual/app.py | 50 +++++++++++++++++++------------ src/textual/widget.py | 33 ++++++++++---------- tests/test_binding_inheritance.py | 3 +- 4 files changed, 54 insertions(+), 36 deletions(-) diff --git a/src/textual/actions.py b/src/textual/actions.py index 8d3dbdaa2f..ce5b796db2 100644 --- a/src/textual/actions.py +++ b/src/textual/actions.py @@ -4,6 +4,10 @@ import re +class SkipAction(Exception): + """Raise in an action to skip the action (and allow any parent bindings to run).""" + + class ActionError(Exception): pass diff --git a/src/textual/app.py b/src/textual/app.py index bdd61771bf..83f246f7e7 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -38,7 +38,7 @@ from rich.segment import Segment, Segments from rich.traceback import Traceback -from . import Logger, LogGroup, LogVerbosity, actions, events, log, messages +from . import actions, Logger, LogGroup, LogVerbosity, events, log, messages from ._animator import DEFAULT_EASING, Animatable, Animator, EasingFunction from ._ansi_sequences import SYNC_END, SYNC_START from ._callback import invoke @@ -47,6 +47,7 @@ from ._filter import LineFilter, Monochrome from ._path import _make_path_object_relative from ._typing import Final, TypeAlias +from .actions import SkipAction from .await_remove import AwaitRemove from .binding import Binding, Bindings from .css.query import NoMatches @@ -1752,7 +1753,7 @@ async def check_bindings(self, key: str, priority: bool = False) -> bool: ): binding = bindings.keys.get(key) if binding is not None and binding.priority == priority: - if await self.action(binding.action, namespace) in (True, None): + if await self.action(binding.action, namespace): return True return False @@ -1822,30 +1823,41 @@ async def action( async def _dispatch_action( self, namespace: object, action_name: str, params: Any ) -> bool: + """Dispatch an action to an action method. + + Args: + namespace (object): Namespace (object) of action. + action_name (str): Name of the action. + params (Any): Action parameters. + + Returns: + bool: True if handled, otherwise False. + """ + _rich_traceback_guard = True + log( "", namespace=namespace, action_name=action_name, params=params, - ) - _rich_traceback_guard = True - - public_method_name = f"action_{action_name}" - private_method_name = f"_{public_method_name}" - - private_method = getattr(namespace, private_method_name, None) - public_method = getattr(namespace, public_method_name, None) + ) - if private_method is None and public_method is None: + try: + private_method = getattr(namespace, f"_action_{action_name}", None) + if callable(private_method): + await invoke(private_method, *params) + return True + public_method = getattr(namespace, f"action_{action_name}", None) + if callable(public_method): + await invoke(public_method, *params) + return True log( - f" {action_name!r} has no target. Couldn't find methods {public_method_name!r} or {private_method_name!r}" + f" {action_name!r} has no target." + f" Could not find methods '_action_{action_name}' or 'action_{action_name}'" ) - - if callable(private_method): - return await invoke(private_method, *params) - elif callable(public_method): - return await invoke(public_method, *params) - + except SkipAction: + # The action method raised this to explicitly not handle the action + log(" {action_name!r} skipped.") return False async def _broker_event( @@ -1856,7 +1868,7 @@ async def _broker_event( Args: event_name (str): _description_ event (events.Event): An event object. - default_namespace (object | None): TODO: _description_ + default_namespace (object | None): The default namespace, where one isn't supplied. Returns: bool: True if an action was processed. diff --git a/src/textual/widget.py b/src/textual/widget.py index cd45834adb..546e371fc3 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -44,6 +44,7 @@ from ._segment_tools import align_lines from ._styles_cache import StylesCache from ._types import Lines +from .actions import SkipAction from .await_remove import AwaitRemove from .binding import Binding from .box_model import BoxModel, get_box_model @@ -2403,42 +2404,42 @@ def _on_hide(self, event: events.Hide) -> None: def _on_scroll_to_region(self, message: messages.ScrollToRegion) -> None: self.scroll_to_region(message.region, animate=True) - def action_scroll_home(self) -> bool | None: + def action_scroll_home(self) -> None: if not self._allow_scroll: - return False + raise SkipAction() self.scroll_home() - def action_scroll_end(self) -> bool | None: + def action_scroll_end(self) -> None: if not self._allow_scroll: - return False + raise SkipAction() self.scroll_end() - def action_scroll_left(self) -> bool | None: + def action_scroll_left(self) -> None: if not self.allow_horizontal_scroll: - return False + raise SkipAction() self.scroll_left() - def action_scroll_right(self) -> bool | None: + def action_scroll_right(self) -> None: if not self.allow_horizontal_scroll: - return False + raise SkipAction() self.scroll_right() - def action_scroll_up(self) -> bool | None: + def action_scroll_up(self) -> None: if not self.allow_vertical_scroll: - return False + raise SkipAction() self.scroll_up() - def action_scroll_down(self) -> bool | None: + def action_scroll_down(self) -> None: if not self.allow_vertical_scroll: - return False + raise SkipAction() self.scroll_down() - def action_page_down(self) -> bool | None: + def action_page_down(self) -> None: if not self.allow_vertical_scroll: - return False + raise SkipAction() self.scroll_page_down() - def action_page_up(self) -> bool | None: + def action_page_up(self) -> None: if not self.allow_vertical_scroll: - return False + raise SkipAction() self.scroll_page_up() diff --git a/tests/test_binding_inheritance.py b/tests/test_binding_inheritance.py index 3d4c7f89c5..8f7029e350 100644 --- a/tests/test_binding_inheritance.py +++ b/tests/test_binding_inheritance.py @@ -11,6 +11,7 @@ from __future__ import annotations +from textual.actions import SkipAction from textual.app import App, ComposeResult from textual.binding import Binding from textual.containers import Container @@ -626,7 +627,7 @@ class NoHandle(Widget, can_focus=True): def action_test(self, text: str) -> bool: nonlocal no_handle_invoked no_handle_invoked = True - return False + raise SkipAction() class SkipApp(App): def compose(self) -> ComposeResult: From 053b7f38e1665cb4b82eed5c5693120c99ea054c Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 19 Dec 2022 10:27:29 +0000 Subject: [PATCH 05/10] snapshot tweak --- .../__snapshots__/test_snapshots.ambr | 212 ++++++++++-------- tests/snapshot_tests/test_snapshots.py | 10 +- 2 files changed, 126 insertions(+), 96 deletions(-) diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 1a38659fbd..d7b86f8891 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -4758,7 +4758,7 @@ # --- # name: test_demo ''' - + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + + + + + + + + + + + + + + + + + - Textual Demo + Textual Demo - - - - Textual Demo - - - TOP▄▄ - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - WidgetsTextual Demo - - Welcome! Textual is a framework for      - creating sophisticated applications with - Rich contentthe terminal.                            - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  Start  - CSS▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - - - - - -  CTRL+B  Sidebar  CTRL+T  Toggle Dark mode  CTRL+S  Screenshot  F1  Notes  CTRL… + + + + Textual Demo + ▅▅ + + TOP + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▃▃ + + Widgets + Textual Demo + + Welcome! Textual is a framework for creating sophisticated + Rich contentapplications with the terminal.                            + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  Start  + CSS▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + + + + + + + + + +                           Widgets                            +  CTRL+B  Sidebar  CTRL+T  Toggle Dark mode  CTRL+S  Screenshot  F1  Notes  CTRL+Q  Quit  diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index 88fccaaf59..d4c9980cf0 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -101,7 +101,9 @@ def test_header_render(snap_compare): def test_list_view(snap_compare): - assert snap_compare(WIDGET_EXAMPLES_DIR / "list_view.py", press=["tab", "down", "down", "up"]) + assert snap_compare( + WIDGET_EXAMPLES_DIR / "list_view.py", press=["tab", "down", "down", "up"] + ) def test_textlog_max_lines(snap_compare): @@ -169,4 +171,8 @@ def test_key_display(snap_compare): def test_demo(snap_compare): """Test the demo app (python -m textual)""" - assert snap_compare(Path("../../src/textual/demo.py")) + assert snap_compare( + Path("../../src/textual/demo.py"), + press=["down", "down", "down"], + terminal_size=(100, 30), + ) From 7cddd61b0054c7d8c95c6ce11bf7f9f02e350589 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 19 Dec 2022 10:31:30 +0000 Subject: [PATCH 06/10] Reformat --- src/textual/app.py | 4 ++-- src/textual/widgets/_static.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/textual/app.py b/src/textual/app.py index 83f246f7e7..e090734b82 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -1834,13 +1834,13 @@ async def _dispatch_action( bool: True if handled, otherwise False. """ _rich_traceback_guard = True - + log( "", namespace=namespace, action_name=action_name, params=params, - ) + ) try: private_method = getattr(namespace, f"_action_{action_name}", None) diff --git a/src/textual/widgets/_static.py b/src/textual/widgets/_static.py index 0d5004baec..1e572aee44 100644 --- a/src/textual/widgets/_static.py +++ b/src/textual/widgets/_static.py @@ -24,7 +24,7 @@ def _check_renderable(renderable: object): ) -class Static(Widget): +class Static(Widget, inherit_bindings=False): """A widget to display simple static content, or use as a base class for more complex widgets. Args: From 623f7f635e01a339887e40927d5d9968d09bedc0 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 19 Dec 2022 11:07:39 +0000 Subject: [PATCH 07/10] changelog --- CHANGELOG.md | 6 ++++++ src/textual/demo.css | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5d964375d..16ba4cd683 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ 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.8.0] - Unreleased + +### Added + +- Added `textual.actions.SkipAction` exception which can be raised from an action to allow parents to process bindings. + ## [0.7.0] - 2022-12-17 ### Added diff --git a/src/textual/demo.css b/src/textual/demo.css index 88ef1b7331..5213465451 100644 --- a/src/textual/demo.css +++ b/src/textual/demo.css @@ -29,7 +29,7 @@ Sidebar { } Sidebar:focus-within { - offset: 0 0 !important; + offset: 0 0 !important; } Sidebar.-hidden { From 621e96dcdd1f0fa817cb2ca33f8ee87e8d0d7cae Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 19 Dec 2022 11:20:46 +0000 Subject: [PATCH 08/10] pause --- tests/test_binding_inheritance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_binding_inheritance.py b/tests/test_binding_inheritance.py index 8f7029e350..f87cf5f66a 100644 --- a/tests/test_binding_inheritance.py +++ b/tests/test_binding_inheritance.py @@ -640,7 +640,7 @@ def on_mount(self) -> None: # Check the NoHandle widget has focus assert pilot.app.query_one(NoHandle).has_focus # Press the "t" key - await pilot.press("t") + await pilot.press("t", "_") # Check the action on the no handle widget was called assert no_handle_invoked # Check the return value, confirming that the action on Handle was called From 002cbab0d5baac409bbd499cf25bfc7a7459ef74 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 19 Dec 2022 11:26:42 +0000 Subject: [PATCH 09/10] add pause to action --- docs/guide/actions.md | 4 ++-- tests/snapshot_tests/test_snapshots.py | 2 +- tests/test_binding_inheritance.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/guide/actions.md b/docs/guide/actions.md index f48a47a1dc..0639860699 100644 --- a/docs/guide/actions.md +++ b/docs/guide/actions.md @@ -75,7 +75,7 @@ When you click any of the links, Textual runs the `"set_background"` action to c ## Bindings -Textual will also run actions bound to keys. The following example adds key [bindings](./input.md#bindings) for the ++r++, ++g++, and ++b++ keys which call the `"set_background"` action. +Textual will run actions bound to keys. The following example adds key [bindings](./input.md#bindings) for the ++r++, ++g++, and ++b++ keys which call the `"set_background"` action. === "actions04.py" @@ -92,7 +92,7 @@ If you run this example, you can change the background by pressing keys in addit ## Namespaces -Textual will look for action methods on the widget or app where they are used. If we were to create a [custom widget](./widgets.md#custom-widgets) it can have its own set of actions. +Textual will look for action methods in the class where they are defined (App, Screen, or Widget). If we were to create a [custom widget](./widgets.md#custom-widgets) it can have its own set of actions. The following example defines a custom widget with its own `set_background` action. diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index d4c9980cf0..b6cfa1fac8 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -173,6 +173,6 @@ def test_demo(snap_compare): """Test the demo app (python -m textual)""" assert snap_compare( Path("../../src/textual/demo.py"), - press=["down", "down", "down"], + press=["down", "down", "down", "_"], terminal_size=(100, 30), ) diff --git a/tests/test_binding_inheritance.py b/tests/test_binding_inheritance.py index f87cf5f66a..8f7029e350 100644 --- a/tests/test_binding_inheritance.py +++ b/tests/test_binding_inheritance.py @@ -640,7 +640,7 @@ def on_mount(self) -> None: # Check the NoHandle widget has focus assert pilot.app.query_one(NoHandle).has_focus # Press the "t" key - await pilot.press("t", "_") + await pilot.press("t") # Check the action on the no handle widget was called assert no_handle_invoked # Check the return value, confirming that the action on Handle was called From 16994c54d176e087ee3a1d541832c465fb719848 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 20 Dec 2022 10:42:21 +0000 Subject: [PATCH 10/10] Update tests/test_binding_inheritance.py Co-authored-by: darrenburns --- tests/test_binding_inheritance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_binding_inheritance.py b/tests/test_binding_inheritance.py index 8f7029e350..a1f83042a7 100644 --- a/tests/test_binding_inheritance.py +++ b/tests/test_binding_inheritance.py @@ -611,7 +611,7 @@ async def test_overlapping_priority_bindings() -> None: async def test_skip_action() -> None: - """Test that a binding may be skipped by an action returning False""" + """Test that a binding may be skipped by an action raising SkipAction""" class Handle(Widget, can_focus=True): BINDINGS = [("t", "test('foo')", "Test")]