From ee59c5882e18fb9df693864f402b598ad87efd9e Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 19 Dec 2022 10:01:06 +0000 Subject: [PATCH] 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 8d3dbdaa2..ce5b796db 100644 --- a/src/textual/actions.py +++ b/src/textual/actions.py @@ -4,6 +4,10 @@ import ast 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 bdd61771b..83f246f7e 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -38,7 +38,7 @@ from rich.protocol import is_renderable 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 ._event_broker import NoHandler, extract_handler_actions 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 @@ class App(Generic[ReturnType], DOMNode): ): 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 @@ class App(Generic[ReturnType], DOMNode): 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 @@ class App(Generic[ReturnType], DOMNode): 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 cd45834ad..546e371fc 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -44,6 +44,7 @@ from ._layout import Layout 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 @@ class Widget(DOMNode): 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 3d4c7f89c..8f7029e35 100644 --- a/tests/test_binding_inheritance.py +++ b/tests/test_binding_inheritance.py @@ -11,6 +11,7 @@ background relating to this. 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 @@ async def test_skip_action() -> None: 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: