Skip to content

Commit 2e2175a

Browse files
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()`.
1 parent b6a9f05 commit 2e2175a

File tree

7 files changed

+52
-34
lines changed

7 files changed

+52
-34
lines changed

.github/workflows/test.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ jobs:
3636
- name: Tests
3737
run: |
3838
coverage run -m pytest
39-
- name: Mypy
40-
# Check wheather the imports were sorted correctly.
39+
- if: "matrix.python-version != '3.7'"
40+
name: Mypy
41+
# Check whether the imports were sorted correctly.
4142
# When this fails, please run ./tools/sort-imports.sh
4243
run: |
4344
mypy --strict src/prompt_toolkit --platform win32

src/prompt_toolkit/application/application.py

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@
4040
from prompt_toolkit.data_structures import Size
4141
from prompt_toolkit.enums import EditingMode
4242
from prompt_toolkit.eventloop import (
43+
InputHook,
4344
get_traceback_from_context,
45+
new_eventloop_with_inputhook,
4446
run_in_executor_with_context,
4547
)
4648
from prompt_toolkit.eventloop.utils import call_soon_threadsafe
@@ -898,13 +900,12 @@ def run(
898900
set_exception_handler: bool = True,
899901
handle_sigint: bool = True,
900902
in_thread: bool = False,
903+
inputhook: InputHook | None = None,
901904
) -> _AppResult:
902905
"""
903906
A blocking 'run' call that waits until the UI is finished.
904907
905-
This will start the current asyncio event loop. If no loop is set for
906-
the current thread, then it will create a new loop. If a new loop was
907-
created, this won't close the new loop (if `in_thread=False`).
908+
This will run the application in a fresh asyncio event loop.
908909
909910
:param pre_run: Optional callable, which is called right after the
910911
"reset" of the application.
@@ -937,6 +938,7 @@ def run_in_thread() -> None:
937938
set_exception_handler=set_exception_handler,
938939
# Signal handling only works in the main thread.
939940
handle_sigint=False,
941+
inputhook=inputhook,
940942
)
941943
except BaseException as e:
942944
exception = e
@@ -954,23 +956,18 @@ def run_in_thread() -> None:
954956
set_exception_handler=set_exception_handler,
955957
handle_sigint=handle_sigint,
956958
)
957-
try:
958-
# See whether a loop was installed already. If so, use that. That's
959-
# required for the input hooks to work, they are installed using
960-
# `set_event_loop`.
961-
if sys.version_info < (3, 10):
962-
loop = asyncio.get_event_loop()
963-
else:
964-
try:
965-
loop = asyncio.get_running_loop()
966-
except RuntimeError:
967-
loop = asyncio.new_event_loop()
968-
except RuntimeError:
959+
if inputhook is None:
969960
# No loop installed. Run like usual.
970961
return asyncio.run(coro)
971962
else:
972-
# Use existing loop.
973-
return loop.run_until_complete(coro)
963+
# Create new event loop with given input hook and run the app.
964+
# In Python 3.12, we can use asyncio.run(loop_factory=...)
965+
# For now, use `run_until_complete()`.
966+
loop = new_eventloop_with_inputhook(inputhook)
967+
result = loop.run_until_complete(coro)
968+
loop.run_until_complete(loop.shutdown_asyncgens())
969+
loop.close()
970+
return result
974971

975972
def _handle_exception(
976973
self, loop: AbstractEventLoop, context: dict[str, Any]

src/prompt_toolkit/application/dummy.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from typing import Callable
44

5+
from prompt_toolkit.eventloop import InputHook
56
from prompt_toolkit.formatted_text import AnyFormattedText
67
from prompt_toolkit.input import DummyInput
78
from prompt_toolkit.output import DummyOutput
@@ -28,6 +29,7 @@ def run(
2829
set_exception_handler: bool = True,
2930
handle_sigint: bool = True,
3031
in_thread: bool = False,
32+
inputhook: InputHook | None = None,
3133
) -> None:
3234
raise NotImplementedError("A DummyApplication is not supposed to run.")
3335

src/prompt_toolkit/eventloop/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from .async_generator import aclosing, generator_to_async_generator
44
from .inputhook import (
5+
InputHook,
56
InputHookContext,
67
InputHookSelector,
78
new_eventloop_with_inputhook,
@@ -22,6 +23,7 @@
2223
"call_soon_threadsafe",
2324
"get_traceback_from_context",
2425
# Inputhooks.
26+
"InputHook",
2527
"new_eventloop_with_inputhook",
2628
"set_eventloop_with_inputhook",
2729
"InputHookSelector",

src/prompt_toolkit/eventloop/inputhook.py

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,32 @@
3939
"set_eventloop_with_inputhook",
4040
"InputHookSelector",
4141
"InputHookContext",
42+
"InputHook",
4243
]
4344

4445
if TYPE_CHECKING:
4546
from _typeshed import FileDescriptorLike
47+
from typing_extensions import TypeAlias
4648

4749
_EventMask = int
4850

4951

52+
class InputHookContext:
53+
"""
54+
Given as a parameter to the inputhook.
55+
"""
56+
57+
def __init__(self, fileno: int, input_is_ready: Callable[[], bool]) -> None:
58+
self._fileno = fileno
59+
self.input_is_ready = input_is_ready
60+
61+
def fileno(self) -> int:
62+
return self._fileno
63+
64+
65+
InputHook: TypeAlias = Callable[[InputHookContext], None]
66+
67+
5068
def new_eventloop_with_inputhook(
5169
inputhook: Callable[[InputHookContext], None]
5270
) -> AbstractEventLoop:
@@ -64,6 +82,8 @@ def set_eventloop_with_inputhook(
6482
"""
6583
Create a new event loop with the given inputhook, and activate it.
6684
"""
85+
# Deprecated!
86+
6787
loop = new_eventloop_with_inputhook(inputhook)
6888
asyncio.set_event_loop(loop)
6989
return loop
@@ -168,16 +188,3 @@ def close(self) -> None:
168188

169189
def get_map(self) -> Mapping[FileDescriptorLike, SelectorKey]:
170190
return self.selector.get_map()
171-
172-
173-
class InputHookContext:
174-
"""
175-
Given as a parameter to the inputhook.
176-
"""
177-
178-
def __init__(self, fileno: int, input_is_ready: Callable[[], bool]) -> None:
179-
self._fileno = fileno
180-
self.input_is_ready = input_is_ready
181-
182-
def fileno(self) -> int:
183-
return self._fileno

src/prompt_toolkit/layout/controls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,7 @@ def mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone:
449449
# Handler found. Call it.
450450
# (Handler can return NotImplemented, so return
451451
# that result.)
452-
handler = item[2] # type: ignore
452+
handler = item[2]
453453
return handler(mouse_event)
454454
else:
455455
break

src/prompt_toolkit/shortcuts/prompt.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
)
4646
from prompt_toolkit.document import Document
4747
from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER, EditingMode
48+
from prompt_toolkit.eventloop import InputHook
4849
from prompt_toolkit.filters import (
4950
Condition,
5051
FilterOrBool,
@@ -892,6 +893,7 @@ def prompt(
892893
set_exception_handler: bool = True,
893894
handle_sigint: bool = True,
894895
in_thread: bool = False,
896+
inputhook: InputHook | None = None,
895897
) -> _T:
896898
"""
897899
Display the prompt.
@@ -1025,6 +1027,7 @@ class itself. For these, passing in ``None`` will keep the current
10251027
set_exception_handler=set_exception_handler,
10261028
in_thread=in_thread,
10271029
handle_sigint=handle_sigint,
1030+
inputhook=inputhook,
10281031
)
10291032

10301033
@contextmanager
@@ -1393,11 +1396,14 @@ def prompt(
13931396
enable_open_in_editor: FilterOrBool | None = None,
13941397
tempfile_suffix: str | Callable[[], str] | None = None,
13951398
tempfile: str | Callable[[], str] | None = None,
1396-
in_thread: bool = False,
13971399
# Following arguments are specific to the current `prompt()` call.
13981400
default: str = "",
13991401
accept_default: bool = False,
14001402
pre_run: Callable[[], None] | None = None,
1403+
set_exception_handler: bool = True,
1404+
handle_sigint: bool = True,
1405+
in_thread: bool = False,
1406+
inputhook: InputHook | None = None,
14011407
) -> str:
14021408
"""
14031409
The global `prompt` function. This will create a new `PromptSession`
@@ -1448,7 +1454,10 @@ def prompt(
14481454
default=default,
14491455
accept_default=accept_default,
14501456
pre_run=pre_run,
1457+
set_exception_handler=set_exception_handler,
1458+
handle_sigint=handle_sigint,
14511459
in_thread=in_thread,
1460+
inputhook=inputhook,
14521461
)
14531462

14541463

0 commit comments

Comments
 (0)