Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix inputhook implementation to be compatible with asyncio.run(). #1810

Merged
merged 1 commit into from
Nov 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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