Skip to content

Commit

Permalink
Fix inputhook implementation to be compatible with asyncio.run().
Browse files Browse the repository at this point in the history
`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()`.
  • Loading branch information
jonathanslenders committed Nov 13, 2023
1 parent b6a9f05 commit 606033a
Show file tree
Hide file tree
Showing 7 changed files with 52 additions and 34 deletions.
5 changes: 3 additions & 2 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 14 additions & 17 deletions src/prompt_toolkit/application/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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]
Expand Down
2 changes: 2 additions & 0 deletions src/prompt_toolkit/application/dummy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.")

Expand Down
2 changes: 2 additions & 0 deletions src/prompt_toolkit/eventloop/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from .async_generator import aclosing, generator_to_async_generator
from .inputhook import (
InputHook,
InputHookContext,
InputHookSelector,
new_eventloop_with_inputhook,
Expand All @@ -22,6 +23,7 @@
"call_soon_threadsafe",
"get_traceback_from_context",
# Inputhooks.
"InputHook",
"new_eventloop_with_inputhook",
"set_eventloop_with_inputhook",
"InputHookSelector",
Expand Down
33 changes: 20 additions & 13 deletions src/prompt_toolkit/eventloop/inputhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion src/prompt_toolkit/layout/controls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion src/prompt_toolkit/shortcuts/prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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,
)


Expand Down

0 comments on commit 606033a

Please sign in to comment.