From 606033acd90b6102984c83a1057d2352a4b23407 Mon Sep 17 00:00:00 2001 From: Jonathan Slenders Date: Sat, 11 Nov 2023 16:40:56 +0000 Subject: [PATCH] Fix inputhook implementation to be compatible with asyncio.run(). `asyncio.get_event_loop()` got deprecated. So we can't install an event loop with inputhook upfront and then use in in prompt_toolkit. Instead, we can take the inputhook as an argument in `Application.run()` and `PromptSession.run()`, and install it in the event loop that we create ourselves using `asyncio.run()`. --- .github/workflows/test.yaml | 5 +-- src/prompt_toolkit/application/application.py | 31 ++++++++--------- src/prompt_toolkit/application/dummy.py | 2 ++ src/prompt_toolkit/eventloop/__init__.py | 2 ++ src/prompt_toolkit/eventloop/inputhook.py | 33 +++++++++++-------- src/prompt_toolkit/layout/controls.py | 2 +- src/prompt_toolkit/shortcuts/prompt.py | 11 ++++++- 7 files changed, 52 insertions(+), 34 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 82ed05d43..2761c4061 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -36,8 +36,9 @@ jobs: - name: Tests run: | coverage run -m pytest - - name: Mypy - # Check wheather the imports were sorted correctly. + - if: "matrix.python-version != '3.7'" + name: Mypy + # Check whether the imports were sorted correctly. # When this fails, please run ./tools/sort-imports.sh run: | mypy --strict src/prompt_toolkit --platform win32 diff --git a/src/prompt_toolkit/application/application.py b/src/prompt_toolkit/application/application.py index c6e67ab9f..4981815c7 100644 --- a/src/prompt_toolkit/application/application.py +++ b/src/prompt_toolkit/application/application.py @@ -40,7 +40,9 @@ from prompt_toolkit.data_structures import Size from prompt_toolkit.enums import EditingMode from prompt_toolkit.eventloop import ( + InputHook, get_traceback_from_context, + new_eventloop_with_inputhook, run_in_executor_with_context, ) from prompt_toolkit.eventloop.utils import call_soon_threadsafe @@ -898,13 +900,12 @@ def run( set_exception_handler: bool = True, handle_sigint: bool = True, in_thread: bool = False, + inputhook: InputHook | None = None, ) -> _AppResult: """ A blocking 'run' call that waits until the UI is finished. - This will start the current asyncio event loop. If no loop is set for - the current thread, then it will create a new loop. If a new loop was - created, this won't close the new loop (if `in_thread=False`). + This will run the application in a fresh asyncio event loop. :param pre_run: Optional callable, which is called right after the "reset" of the application. @@ -937,6 +938,7 @@ def run_in_thread() -> None: set_exception_handler=set_exception_handler, # Signal handling only works in the main thread. handle_sigint=False, + inputhook=inputhook, ) except BaseException as e: exception = e @@ -954,23 +956,18 @@ def run_in_thread() -> None: set_exception_handler=set_exception_handler, handle_sigint=handle_sigint, ) - try: - # See whether a loop was installed already. If so, use that. That's - # required for the input hooks to work, they are installed using - # `set_event_loop`. - if sys.version_info < (3, 10): - loop = asyncio.get_event_loop() - else: - try: - loop = asyncio.get_running_loop() - except RuntimeError: - loop = asyncio.new_event_loop() - except RuntimeError: + if inputhook is None: # No loop installed. Run like usual. return asyncio.run(coro) else: - # Use existing loop. - return loop.run_until_complete(coro) + # Create new event loop with given input hook and run the app. + # In Python 3.12, we can use asyncio.run(loop_factory=...) + # For now, use `run_until_complete()`. + loop = new_eventloop_with_inputhook(inputhook) + result = loop.run_until_complete(coro) + loop.run_until_complete(loop.shutdown_asyncgens()) + loop.close() + return result def _handle_exception( self, loop: AbstractEventLoop, context: dict[str, Any] diff --git a/src/prompt_toolkit/application/dummy.py b/src/prompt_toolkit/application/dummy.py index 4107d0578..43819e1e7 100644 --- a/src/prompt_toolkit/application/dummy.py +++ b/src/prompt_toolkit/application/dummy.py @@ -2,6 +2,7 @@ from typing import Callable +from prompt_toolkit.eventloop import InputHook from prompt_toolkit.formatted_text import AnyFormattedText from prompt_toolkit.input import DummyInput from prompt_toolkit.output import DummyOutput @@ -28,6 +29,7 @@ def run( set_exception_handler: bool = True, handle_sigint: bool = True, in_thread: bool = False, + inputhook: InputHook | None = None, ) -> None: raise NotImplementedError("A DummyApplication is not supposed to run.") diff --git a/src/prompt_toolkit/eventloop/__init__.py b/src/prompt_toolkit/eventloop/__init__.py index 89eb71c31..5df623bff 100644 --- a/src/prompt_toolkit/eventloop/__init__.py +++ b/src/prompt_toolkit/eventloop/__init__.py @@ -2,6 +2,7 @@ from .async_generator import aclosing, generator_to_async_generator from .inputhook import ( + InputHook, InputHookContext, InputHookSelector, new_eventloop_with_inputhook, @@ -22,6 +23,7 @@ "call_soon_threadsafe", "get_traceback_from_context", # Inputhooks. + "InputHook", "new_eventloop_with_inputhook", "set_eventloop_with_inputhook", "InputHookSelector", diff --git a/src/prompt_toolkit/eventloop/inputhook.py b/src/prompt_toolkit/eventloop/inputhook.py index 8d6f6a4ff..5731573f5 100644 --- a/src/prompt_toolkit/eventloop/inputhook.py +++ b/src/prompt_toolkit/eventloop/inputhook.py @@ -39,14 +39,32 @@ "set_eventloop_with_inputhook", "InputHookSelector", "InputHookContext", + "InputHook", ] if TYPE_CHECKING: from _typeshed import FileDescriptorLike + from typing_extensions import TypeAlias _EventMask = int +class InputHookContext: + """ + Given as a parameter to the inputhook. + """ + + def __init__(self, fileno: int, input_is_ready: Callable[[], bool]) -> None: + self._fileno = fileno + self.input_is_ready = input_is_ready + + def fileno(self) -> int: + return self._fileno + + +InputHook: TypeAlias = Callable[[InputHookContext], None] + + def new_eventloop_with_inputhook( inputhook: Callable[[InputHookContext], None] ) -> AbstractEventLoop: @@ -64,6 +82,8 @@ def set_eventloop_with_inputhook( """ Create a new event loop with the given inputhook, and activate it. """ + # Deprecated! + loop = new_eventloop_with_inputhook(inputhook) asyncio.set_event_loop(loop) return loop @@ -168,16 +188,3 @@ def close(self) -> None: def get_map(self) -> Mapping[FileDescriptorLike, SelectorKey]: return self.selector.get_map() - - -class InputHookContext: - """ - Given as a parameter to the inputhook. - """ - - def __init__(self, fileno: int, input_is_ready: Callable[[], bool]) -> None: - self._fileno = fileno - self.input_is_ready = input_is_ready - - def fileno(self) -> int: - return self._fileno diff --git a/src/prompt_toolkit/layout/controls.py b/src/prompt_toolkit/layout/controls.py index 647df3d63..c13960bc4 100644 --- a/src/prompt_toolkit/layout/controls.py +++ b/src/prompt_toolkit/layout/controls.py @@ -449,7 +449,7 @@ def mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone: # Handler found. Call it. # (Handler can return NotImplemented, so return # that result.) - handler = item[2] # type: ignore + handler = item[2] return handler(mouse_event) else: break diff --git a/src/prompt_toolkit/shortcuts/prompt.py b/src/prompt_toolkit/shortcuts/prompt.py index ed56adc94..7274b5f03 100644 --- a/src/prompt_toolkit/shortcuts/prompt.py +++ b/src/prompt_toolkit/shortcuts/prompt.py @@ -45,6 +45,7 @@ ) from prompt_toolkit.document import Document from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER, EditingMode +from prompt_toolkit.eventloop import InputHook from prompt_toolkit.filters import ( Condition, FilterOrBool, @@ -892,6 +893,7 @@ def prompt( set_exception_handler: bool = True, handle_sigint: bool = True, in_thread: bool = False, + inputhook: InputHook | None = None, ) -> _T: """ Display the prompt. @@ -1025,6 +1027,7 @@ class itself. For these, passing in ``None`` will keep the current set_exception_handler=set_exception_handler, in_thread=in_thread, handle_sigint=handle_sigint, + inputhook=inputhook, ) @contextmanager @@ -1393,11 +1396,14 @@ def prompt( enable_open_in_editor: FilterOrBool | None = None, tempfile_suffix: str | Callable[[], str] | None = None, tempfile: str | Callable[[], str] | None = None, - in_thread: bool = False, # Following arguments are specific to the current `prompt()` call. default: str = "", accept_default: bool = False, pre_run: Callable[[], None] | None = None, + set_exception_handler: bool = True, + handle_sigint: bool = True, + in_thread: bool = False, + inputhook: InputHook | None = None, ) -> str: """ The global `prompt` function. This will create a new `PromptSession` @@ -1448,7 +1454,10 @@ def prompt( default=default, accept_default=accept_default, pre_run=pre_run, + set_exception_handler=set_exception_handler, + handle_sigint=handle_sigint, in_thread=in_thread, + inputhook=inputhook, )