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 11, 2023
1 parent b6a9f05 commit 23de0e9
Show file tree
Hide file tree
Showing 7 changed files with 54 additions and 34 deletions.
11 changes: 6 additions & 5 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
python_version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]

steps:
- uses: actions/checkout@v4
- name: Setup Python ${{ matrix.python-version }}
- name: Setup Python ${{ matrix.python_version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
python-version: ${{ matrix.python_version }}
allow-prereleases: true
- name: Install Dependencies
run: |
Expand All @@ -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
27 changes: 13 additions & 14 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,6 +900,7 @@ 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.
Expand Down Expand Up @@ -937,6 +940,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 +958,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 23de0e9

Please sign in to comment.