From 4a98ebe34ba7dffd46262debe100d278b30d9bfe Mon Sep 17 00:00:00 2001 From: Renato Byrro Date: Sat, 24 Oct 2020 08:31:29 -0300 Subject: [PATCH 1/6] Add 'exit_on_key' argument to Application constructor How it works: Make app exitable with custom key-binding by passing 'exit_on_key': app = Application(exit_on_key='c-q') Also accepts a tuple: app = Application(exit_on_key=('escape', 'q')) Obs1: 'exit_on_key' defaults to None (same behavior as currently) Obs2: a key set in 'key_bindings' argument always takes precedence. Rationale: An app.exit() key-binding is arguably needed in many (if not most) applications. Being able to set as argument to Application is handy. Side benefit: simplify examples in documentation. Many aren't exit'able actually. Currently not the most welcoming for someone starting to learn. --- prompt_toolkit/application/application.py | 27 +++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/prompt_toolkit/application/application.py b/prompt_toolkit/application/application.py index 25a0636646..48d4d42d3f 100644 --- a/prompt_toolkit/application/application.py +++ b/prompt_toolkit/application/application.py @@ -118,6 +118,7 @@ class Application(Generic[_AppResult]): :param key_bindings: :class:`~prompt_toolkit.key_binding.KeyBindingsBase` instance for the key bindings. + :param exit_on_key: (str|tuple) auto-bind key to exit the application :param clipboard: :class:`~prompt_toolkit.clipboard.Clipboard` to use. :param on_abort: What to do when Control-C is pressed. :param on_exit: What to do when Control-D is pressed. @@ -195,6 +196,7 @@ def __init__( include_default_pygments_style: FilterOrBool = True, style_transformation: Optional[StyleTransformation] = None, key_bindings: Optional[KeyBindingsBase] = None, + exit_on_key: Optional[Union[str, tuple]] = None, clipboard: Optional[Clipboard] = None, full_screen: bool = False, color_depth: Union[ @@ -242,6 +244,9 @@ def __init__( # Key bindings. self.key_bindings = key_bindings + self.exit_on_key = (exit_on_key,) if type(exit_on_key) is str \ + else exit_on_key + self._merge_exit_key_binding() self._default_bindings = load_key_bindings() self._page_navigation_bindings = load_page_navigation_bindings() @@ -339,6 +344,20 @@ def __init__( # Trigger initialize callback. self.reset() + def _merge_exit_key_binding(self) -> None: + if self.exit_on_key is None: + return + + kb = KeyBindings() + kb.add(*self.exit_on_key)(handle_exit_on_key) + + if self.key_bindings is None: + self.key_bindings = kb + else: + self.key_bindings = merge_key_bindings([kb, self.key_bindings]) + + return + def _create_merged_style(self, include_default_pygments_style: Filter) -> BaseStyle: """ Create a `Style` object that merges the default UI style, the default @@ -1222,3 +1241,11 @@ def attach_winch_signal_handler( previous_winch_handler._callback, *previous_winch_handler._args, ) + + +def handle_exit_on_key(event: KeyPressEvent) -> None: + """ + Exit application function, to be used with a key binding + """ + event.app.exit() + return From 972cf7ab7bb74989e59400afaa81471e1fc46d55 Mon Sep 17 00:00:00 2001 From: Renato Byrro Date: Sat, 24 Oct 2020 08:47:26 -0300 Subject: [PATCH 2/6] Make exit handler func import'able from __init__ --- prompt_toolkit/application/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/prompt_toolkit/application/__init__.py b/prompt_toolkit/application/__init__.py index 343261cdc3..24ffe4ef2a 100644 --- a/prompt_toolkit/application/__init__.py +++ b/prompt_toolkit/application/__init__.py @@ -1,4 +1,4 @@ -from .application import Application +from .application import Application, handle_exit_on_key from .current import ( AppSession, create_app_session, @@ -13,6 +13,7 @@ __all__ = [ # Application. "Application", + "handle_exit_on_key", # Current. "AppSession", "get_app_session", From e53704da04194fe5b244e654ad78a71ec31e09c3 Mon Sep 17 00:00:00 2001 From: Renato Byrro Date: Sat, 24 Oct 2020 08:48:03 -0300 Subject: [PATCH 3/6] Adapt 'set_dummy_app' function to accept kwargs --- tests/test_key_binding.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/test_key_binding.py b/tests/test_key_binding.py index f1ec5af36d..639230d7ae 100644 --- a/tests/test_key_binding.py +++ b/tests/test_key_binding.py @@ -21,14 +21,17 @@ def func(event): return func -def set_dummy_app(): +def set_dummy_app(**kwargs): """ Return a context manager that makes sure that this dummy application is active. This is important, because we need an `Application` with `is_done=False` flag, otherwise no keys will be processed. """ app = Application( - layout=Layout(Window()), output=DummyOutput(), input=create_pipe_input() + layout=Layout(Window()), + output=DummyOutput(), + input=create_pipe_input(), + **kwargs, ) return set_app(app) From b90542f5d8262b4baf9c896c705f53a6afc08325 Mon Sep 17 00:00:00 2001 From: Renato Byrro Date: Sat, 24 Oct 2020 08:48:28 -0300 Subject: [PATCH 4/6] Tests for 'exit_on_key' feature in Application --- tests/test_application.py | 101 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 tests/test_application.py diff --git a/tests/test_application.py b/tests/test_application.py new file mode 100644 index 0000000000..40f542fa4e --- /dev/null +++ b/tests/test_application.py @@ -0,0 +1,101 @@ +import pytest + +from prompt_toolkit.application import ( + Application, + get_app, + handle_exit_on_key, +) +from prompt_toolkit.application.current import set_app +from prompt_toolkit.key_binding.key_bindings import KeyBindings, _parse_key +from prompt_toolkit.keys import Keys +from test_key_binding import handlers, set_dummy_app + + +def test_exit_on_key_simple(): + """ + Setting 'exit_on_key' should create a Binding in 'key_binding' + """ + with set_dummy_app(exit_on_key='c-c'): + exit_kb = KeyBindings() + exit_kb.add('c-c')(handle_exit_on_key) + + app = get_app() + + assert len(app.key_bindings.bindings) == 1 + + app_binding = app.key_bindings.bindings[0] + exit_binding = exit_kb.bindings[0] + + assert app_binding.keys == exit_binding.keys + assert app_binding.handler == exit_binding.handler + + +def test_without_exit_on_key(): + """ + Ommiting 'exit_on_key' should leave 'key_binding' unmodified + """ + kb = KeyBindings() + + @kb.add('c-d') + def bind(e): + pass + + with set_dummy_app(key_bindings=kb): + app = get_app() + + assert app.exit_on_key is None + assert len(app.key_bindings.bindings) == 1 + + app_binding = app.key_bindings.bindings[0] + + assert app_binding.keys == (_parse_key('c-d'),) + assert app_binding.handler == bind + + +def test_exit_on_key_not_override(handlers): + """ + 'exit_on_key' should NOT override a 'key_bindings' Binding with same keys + """ + kb = KeyBindings() + kb.add('c-c')(handlers.control_c) + + with set_dummy_app(exit_on_key='c-c', key_bindings=kb): + app = get_app() + + assert len(app.key_bindings.bindings) == 2 + + app_last_binding = app.key_bindings.bindings[-1] + key_bindings_arg = kb.bindings[0] + + # The last binding should be the one we passed in the key_bindings arg, + # not the one in exit_on_bind + assert app_last_binding.keys == key_bindings_arg.keys + assert app_last_binding.handler == key_bindings_arg.handler + + +def test_exit_on_key_mixed_bindings(): + """ + 'exit_on_key' should not interfere with other 'key_bindings' Bindings + """ + kb = KeyBindings() + + @kb.add('c-d') + def bind_c_d(event): + pass + + @kb.add('s-tab') + def bind_s_tab(event): + pass + + with set_dummy_app(exit_on_key='c-c', key_bindings=kb): + app = get_app() + + binding_tuples = [ + (binding.keys, binding.handler) + for binding in app.key_bindings.bindings + ] + + assert len(binding_tuples) == 3 + assert ((_parse_key('c-c'),), handle_exit_on_key) in binding_tuples + assert ((_parse_key('c-d'),), bind_c_d) in binding_tuples + assert ((_parse_key('s-tab'),), bind_s_tab) in binding_tuples From d8a0b6546b4187a678dcdcfc517f26bd6e41614c Mon Sep 17 00:00:00 2001 From: Renato Byrro Date: Sat, 24 Oct 2020 14:21:44 -0300 Subject: [PATCH 5/6] Apply black and isort --- prompt_toolkit/application/application.py | 83 +++++++---------------- tests/test_application.py | 33 ++++----- 2 files changed, 38 insertions(+), 78 deletions(-) diff --git a/prompt_toolkit/application/application.py b/prompt_toolkit/application/application.py index 48d4d42d3f..8188816b2c 100644 --- a/prompt_toolkit/application/application.py +++ b/prompt_toolkit/application/application.py @@ -4,68 +4,40 @@ import signal import sys import time -from asyncio import ( - AbstractEventLoop, - CancelledError, - Future, - Task, - ensure_future, - get_event_loop, - new_event_loop, - set_event_loop, - sleep, -) +from asyncio import (AbstractEventLoop, CancelledError, Future, Task, + ensure_future, get_event_loop, new_event_loop, + set_event_loop, sleep) from contextlib import contextmanager from subprocess import Popen from traceback import format_tb -from typing import ( - Any, - Awaitable, - Callable, - Dict, - FrozenSet, - Generator, - Generic, - Hashable, - Iterable, - List, - Optional, - Tuple, - Type, - TypeVar, - Union, - cast, - overload, -) +from typing import (Any, Awaitable, Callable, Dict, FrozenSet, Generator, + Generic, Hashable, Iterable, List, Optional, Tuple, Type, + TypeVar, Union, cast, overload) from prompt_toolkit.buffer import Buffer from prompt_toolkit.cache import SimpleCache from prompt_toolkit.clipboard import Clipboard, InMemoryClipboard from prompt_toolkit.enums import EditingMode -from prompt_toolkit.eventloop import ( - get_traceback_from_context, - run_in_executor_with_context, -) +from prompt_toolkit.eventloop import (get_traceback_from_context, + run_in_executor_with_context) from prompt_toolkit.eventloop.utils import call_soon_threadsafe from prompt_toolkit.filters import Condition, Filter, FilterOrBool, to_filter from prompt_toolkit.formatted_text import AnyFormattedText from prompt_toolkit.input.base import Input from prompt_toolkit.input.typeahead import get_typeahead, store_typeahead -from prompt_toolkit.key_binding.bindings.page_navigation import ( - load_page_navigation_bindings, -) +from prompt_toolkit.key_binding.bindings.page_navigation import \ + load_page_navigation_bindings from prompt_toolkit.key_binding.defaults import load_key_bindings from prompt_toolkit.key_binding.emacs_state import EmacsState -from prompt_toolkit.key_binding.key_bindings import ( - Binding, - ConditionalKeyBindings, - GlobalOnlyKeyBindings, - KeyBindings, - KeyBindingsBase, - KeysTuple, - merge_key_bindings, -) -from prompt_toolkit.key_binding.key_processor import KeyPressEvent, KeyProcessor +from prompt_toolkit.key_binding.key_bindings import (Binding, + ConditionalKeyBindings, + GlobalOnlyKeyBindings, + KeyBindings, + KeyBindingsBase, + KeysTuple, + merge_key_bindings) +from prompt_toolkit.key_binding.key_processor import (KeyPressEvent, + KeyProcessor) from prompt_toolkit.key_binding.vi_state import ViState from prompt_toolkit.keys import Keys from prompt_toolkit.layout.containers import Container, Window @@ -75,16 +47,10 @@ from prompt_toolkit.output import ColorDepth, Output from prompt_toolkit.renderer import Renderer, print_formatted_text from prompt_toolkit.search import SearchState -from prompt_toolkit.styles import ( - BaseStyle, - DummyStyle, - DummyStyleTransformation, - DynamicStyle, - StyleTransformation, - default_pygments_style, - default_ui_style, - merge_styles, -) +from prompt_toolkit.styles import (BaseStyle, DummyStyle, + DummyStyleTransformation, DynamicStyle, + StyleTransformation, default_pygments_style, + default_ui_style, merge_styles) from prompt_toolkit.utils import Event, in_main_thread from .current import get_app_session, set_app @@ -244,8 +210,7 @@ def __init__( # Key bindings. self.key_bindings = key_bindings - self.exit_on_key = (exit_on_key,) if type(exit_on_key) is str \ - else exit_on_key + self.exit_on_key = (exit_on_key,) if type(exit_on_key) is str else exit_on_key self._merge_exit_key_binding() self._default_bindings = load_key_bindings() self._page_navigation_bindings = load_page_navigation_bindings() diff --git a/tests/test_application.py b/tests/test_application.py index 40f542fa4e..f1e48fb49a 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -1,10 +1,6 @@ import pytest -from prompt_toolkit.application import ( - Application, - get_app, - handle_exit_on_key, -) +from prompt_toolkit.application import Application, get_app, handle_exit_on_key from prompt_toolkit.application.current import set_app from prompt_toolkit.key_binding.key_bindings import KeyBindings, _parse_key from prompt_toolkit.keys import Keys @@ -15,9 +11,9 @@ def test_exit_on_key_simple(): """ Setting 'exit_on_key' should create a Binding in 'key_binding' """ - with set_dummy_app(exit_on_key='c-c'): + with set_dummy_app(exit_on_key="c-c"): exit_kb = KeyBindings() - exit_kb.add('c-c')(handle_exit_on_key) + exit_kb.add("c-c")(handle_exit_on_key) app = get_app() @@ -36,7 +32,7 @@ def test_without_exit_on_key(): """ kb = KeyBindings() - @kb.add('c-d') + @kb.add("c-d") def bind(e): pass @@ -48,7 +44,7 @@ def bind(e): app_binding = app.key_bindings.bindings[0] - assert app_binding.keys == (_parse_key('c-d'),) + assert app_binding.keys == (_parse_key("c-d"),) assert app_binding.handler == bind @@ -57,9 +53,9 @@ def test_exit_on_key_not_override(handlers): 'exit_on_key' should NOT override a 'key_bindings' Binding with same keys """ kb = KeyBindings() - kb.add('c-c')(handlers.control_c) + kb.add("c-c")(handlers.control_c) - with set_dummy_app(exit_on_key='c-c', key_bindings=kb): + with set_dummy_app(exit_on_key="c-c", key_bindings=kb): app = get_app() assert len(app.key_bindings.bindings) == 2 @@ -79,23 +75,22 @@ def test_exit_on_key_mixed_bindings(): """ kb = KeyBindings() - @kb.add('c-d') + @kb.add("c-d") def bind_c_d(event): pass - @kb.add('s-tab') + @kb.add("s-tab") def bind_s_tab(event): pass - with set_dummy_app(exit_on_key='c-c', key_bindings=kb): + with set_dummy_app(exit_on_key="c-c", key_bindings=kb): app = get_app() binding_tuples = [ - (binding.keys, binding.handler) - for binding in app.key_bindings.bindings + (binding.keys, binding.handler) for binding in app.key_bindings.bindings ] assert len(binding_tuples) == 3 - assert ((_parse_key('c-c'),), handle_exit_on_key) in binding_tuples - assert ((_parse_key('c-d'),), bind_c_d) in binding_tuples - assert ((_parse_key('s-tab'),), bind_s_tab) in binding_tuples + assert ((_parse_key("c-c"),), handle_exit_on_key) in binding_tuples + assert ((_parse_key("c-d"),), bind_c_d) in binding_tuples + assert ((_parse_key("s-tab"),), bind_s_tab) in binding_tuples From 9cb645ebf5c15b54ea8bec5102fba00aa2d2e63d Mon Sep 17 00:00:00 2001 From: Renato Byrro Date: Sat, 24 Oct 2020 15:01:40 -0300 Subject: [PATCH 6/6] Apply black on py36 --- prompt_toolkit/application/application.py | 80 ++++++++++++++++------- 1 file changed, 57 insertions(+), 23 deletions(-) diff --git a/prompt_toolkit/application/application.py b/prompt_toolkit/application/application.py index 8188816b2c..e64aed490a 100644 --- a/prompt_toolkit/application/application.py +++ b/prompt_toolkit/application/application.py @@ -4,40 +4,68 @@ import signal import sys import time -from asyncio import (AbstractEventLoop, CancelledError, Future, Task, - ensure_future, get_event_loop, new_event_loop, - set_event_loop, sleep) +from asyncio import ( + AbstractEventLoop, + CancelledError, + Future, + Task, + ensure_future, + get_event_loop, + new_event_loop, + set_event_loop, + sleep, +) from contextlib import contextmanager from subprocess import Popen from traceback import format_tb -from typing import (Any, Awaitable, Callable, Dict, FrozenSet, Generator, - Generic, Hashable, Iterable, List, Optional, Tuple, Type, - TypeVar, Union, cast, overload) +from typing import ( + Any, + Awaitable, + Callable, + Dict, + FrozenSet, + Generator, + Generic, + Hashable, + Iterable, + List, + Optional, + Tuple, + Type, + TypeVar, + Union, + cast, + overload, +) from prompt_toolkit.buffer import Buffer from prompt_toolkit.cache import SimpleCache from prompt_toolkit.clipboard import Clipboard, InMemoryClipboard from prompt_toolkit.enums import EditingMode -from prompt_toolkit.eventloop import (get_traceback_from_context, - run_in_executor_with_context) +from prompt_toolkit.eventloop import ( + get_traceback_from_context, + run_in_executor_with_context, +) from prompt_toolkit.eventloop.utils import call_soon_threadsafe from prompt_toolkit.filters import Condition, Filter, FilterOrBool, to_filter from prompt_toolkit.formatted_text import AnyFormattedText from prompt_toolkit.input.base import Input from prompt_toolkit.input.typeahead import get_typeahead, store_typeahead -from prompt_toolkit.key_binding.bindings.page_navigation import \ - load_page_navigation_bindings +from prompt_toolkit.key_binding.bindings.page_navigation import ( + load_page_navigation_bindings, +) from prompt_toolkit.key_binding.defaults import load_key_bindings from prompt_toolkit.key_binding.emacs_state import EmacsState -from prompt_toolkit.key_binding.key_bindings import (Binding, - ConditionalKeyBindings, - GlobalOnlyKeyBindings, - KeyBindings, - KeyBindingsBase, - KeysTuple, - merge_key_bindings) -from prompt_toolkit.key_binding.key_processor import (KeyPressEvent, - KeyProcessor) +from prompt_toolkit.key_binding.key_bindings import ( + Binding, + ConditionalKeyBindings, + GlobalOnlyKeyBindings, + KeyBindings, + KeyBindingsBase, + KeysTuple, + merge_key_bindings, +) +from prompt_toolkit.key_binding.key_processor import KeyPressEvent, KeyProcessor from prompt_toolkit.key_binding.vi_state import ViState from prompt_toolkit.keys import Keys from prompt_toolkit.layout.containers import Container, Window @@ -47,10 +75,16 @@ from prompt_toolkit.output import ColorDepth, Output from prompt_toolkit.renderer import Renderer, print_formatted_text from prompt_toolkit.search import SearchState -from prompt_toolkit.styles import (BaseStyle, DummyStyle, - DummyStyleTransformation, DynamicStyle, - StyleTransformation, default_pygments_style, - default_ui_style, merge_styles) +from prompt_toolkit.styles import ( + BaseStyle, + DummyStyle, + DummyStyleTransformation, + DynamicStyle, + StyleTransformation, + default_pygments_style, + default_ui_style, + merge_styles, +) from prompt_toolkit.utils import Event, in_main_thread from .current import get_app_session, set_app