Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit c811c8c

Browse filesBrowse files
committed
Try to run the prompt in background to allow completion while executing
This is an attempt to fix Quansight/deshaw#53 You can enable it with ``` $ ipython --TerminalInteractiveShell.background_prompt=True ``` It is likely full of edge cases and is considered experimental
1 parent f96610c commit c811c8c
Copy full SHA for c811c8c

4 files changed

+605-10Lines changed: 605 additions & 10 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎IPython/terminal/interactiveshell.py‎

Copy file name to clipboardExpand all lines: IPython/terminal/interactiveshell.py
+138-9Lines changed: 138 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""IPython terminal interface using prompt_toolkit"""
22

33
import os
4+
import signal
45
import sys
56
import inspect
67
from warnings import warn
@@ -244,6 +245,15 @@ class TerminalInteractiveShell(InteractiveShell):
244245
""",
245246
).tag(config=True)
246247

248+
background_prompt = Bool(
249+
False,
250+
help="""Run prompt in background thread.
251+
252+
This allows typing input while code executes on the main thread.
253+
Some signal handling (Ctrl-C) may behave differently.
254+
""",
255+
).tag(config=True)
256+
247257
@property
248258
def debugger_cls(self):
249259
return Pdb if self.simple_prompt else TerminalPdb
@@ -802,6 +812,9 @@ def prompt():
802812
editing_mode = getattr(EditingMode, self.editing_mode.upper())
803813

804814
self._use_asyncio_inputhook = False
815+
816+
# Build extra kwargs for PromptSession
817+
805818
self.pt_app = PromptSession(
806819
auto_suggest=self.auto_suggest,
807820
editing_mode=editing_mode,
@@ -921,10 +934,38 @@ def get_message():
921934
),
922935
],
923936
}
937+
if self.background_prompt:
938+
# For background thread compatibility:
939+
# Poll terminal size instead of relying on SIGWINCH
940+
# (signals can't be handled in background threads)
941+
options["refresh_interval"] = 0.5
924942

925943
return options
926944

927945
def prompt_for_code(self):
946+
if self._use_asyncio_inputhook and False:
947+
asyncio_loop = get_asyncio_loop()
948+
return asyncio_loop.run_until_complete(self._prompt_for_code())
949+
else:
950+
return next(self._prompt_for_code())
951+
952+
async def _prompt_for_code_async(self):
953+
"""Async version of prompt that can run in any event loop.
954+
955+
This is used by the background prompt thread to run the prompt
956+
in a separate event loop from the main thread.
957+
"""
958+
if self.rl_next_input:
959+
default = self.rl_next_input
960+
self.rl_next_input = None
961+
else:
962+
default = ""
963+
964+
return await self.pt_app.prompt_async(
965+
default=default, **self._extra_prompt_options()
966+
)
967+
968+
async def _prompt_for_code(self):
928969
if self.rl_next_input:
929970
default = self.rl_next_input
930971
self.rl_next_input = None
@@ -942,20 +983,16 @@ def prompt_for_code(self):
942983
# When we integrate the asyncio event loop, run the UI in the
943984
# same event loop as the rest of the code. don't use an actual
944985
# input hook. (Asyncio is not made for nesting event loops.)
945-
asyncio_loop = get_asyncio_loop()
946-
text = asyncio_loop.run_until_complete(
947-
self.pt_app.prompt_async(
986+
return await self.pt_app.prompt_async(
948987
default=default, **self._extra_prompt_options()
949988
)
950-
)
951989
else:
952-
text = self.pt_app.prompt(
990+
return self.pt_app.prompt(
953991
default=default,
954992
inputhook=self._inputhook,
955993
**self._extra_prompt_options(),
956994
)
957995

958-
return text
959996

960997
def init_io(self):
961998
if sys.platform not in {'win32', 'cli'}:
@@ -992,23 +1029,115 @@ def ask_exit(self):
9921029
self.keep_running = False
9931030

9941031
rl_next_input = None
1032+
_prompt_thread = None
1033+
1034+
@property
1035+
def pending_input_count(self):
1036+
"""Number of inputs waiting to be executed (background prompt mode only)."""
1037+
if self._prompt_thread is not None:
1038+
return self._prompt_thread.input_queue.qsize()
1039+
return 0
9951040

9961041
def interact(self):
1042+
"""Main interaction loop with optional background prompt thread."""
9971043
self.keep_running = True
1044+
1045+
if self.background_prompt and not self.simple_prompt:
1046+
self._interact_with_background_prompt()
1047+
else:
1048+
self._interact_sync()
1049+
1050+
def _interact_sync(self):
1051+
"""Original synchronous interaction loop."""
9981052
while self.keep_running:
999-
print(self.separate_in, end='')
1053+
print(self.separate_in, end="")
10001054

10011055
try:
10021056
code = self.prompt_for_code()
10031057
except EOFError:
1004-
if (not self.confirm_exit) \
1005-
or self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'):
1058+
if (not self.confirm_exit) or self.ask_yes_no(
1059+
"Do you really want to exit ([y]/n)?", "y", "n"
1060+
):
10061061
self.ask_exit()
10071062

10081063
else:
10091064
if code:
10101065
self.run_cell(code, store_history=True)
10111066

1067+
def _interact_with_background_prompt(self):
1068+
"""Interaction loop using background prompt thread.
1069+
1070+
This allows users to type input while code executes on the main thread.
1071+
"""
1072+
from .prompt_thread import (
1073+
InputPatcher,
1074+
PromptThread,
1075+
_EOFSentinel,
1076+
_ExceptionSentinel,
1077+
)
1078+
1079+
self._executing = False
1080+
self._flushed_count = 0
1081+
1082+
prompt_thread = PromptThread(self)
1083+
1084+
# SIGINT handler for during code execution
1085+
# We install this only during execution, not during prompting
1086+
def execution_sigint_handler(signum, frame):
1087+
flushed = prompt_thread.flush_input_queue()
1088+
if flushed > 0:
1089+
self._flushed_count = flushed
1090+
signal.default_int_handler(signum, frame)
1091+
1092+
prompt_thread.start()
1093+
1094+
self._prompt_thread = prompt_thread
1095+
1096+
# Patch builtins.input and wrap sys.stdin to route through the prompt thread
1097+
# This handles both input() calls and direct stdin access from user code
1098+
input_patcher = InputPatcher(prompt_thread)
1099+
1100+
try:
1101+
with input_patcher:
1102+
while self.keep_running:
1103+
print(self.separate_in, end="")
1104+
1105+
# Get input from background thread (blocks)
1106+
input_item = prompt_thread.get_input()
1107+
1108+
if isinstance(input_item, _EOFSentinel):
1109+
# Exit confirmation is handled in the prompt thread
1110+
# to avoid stdin conflicts
1111+
if input_item.should_exit:
1112+
self.ask_exit()
1113+
# If should_exit is False, we continue the loop
1114+
# but the prompt thread already handles this case
1115+
elif isinstance(input_item, _ExceptionSentinel):
1116+
raise input_item.exception
1117+
elif input_item:
1118+
# Execute code on main thread
1119+
# Install our SIGINT handler during execution only
1120+
# This avoids conflicts with prompt_toolkit's handling
1121+
prev_handler = signal.signal(
1122+
signal.SIGINT, execution_sigint_handler
1123+
)
1124+
self._executing = True
1125+
self._flushed_count = 0
1126+
try:
1127+
self.run_cell(input_item, store_history=True)
1128+
except KeyboardInterrupt:
1129+
msg = "\nKeyboardInterrupt"
1130+
if self._flushed_count > 0:
1131+
msg += f" ({self._flushed_count} pending input(s) discarded)"
1132+
print(msg)
1133+
finally:
1134+
self._executing = False
1135+
signal.signal(signal.SIGINT, prev_handler)
1136+
finally:
1137+
self._prompt_thread = None
1138+
prompt_thread.stop()
1139+
prompt_thread.join(timeout=2.0)
1140+
10121141
def mainloop(self):
10131142
# An extra layer of protection in case someone mashing Ctrl-C breaks
10141143
# out of our internal code.

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.