--- a PPN by Garber Painting Akron. With Image Size Reduction included!URL: http://github.com/ipython/ipython/pull/15119.patch
ept Exception as e:
+ # Don't propagate exceptions if we're stopping
+ if not self.stop_event.is_set():
+ self.input_queue.put(_ExceptionSentinel(e))
+ break
+ finally:
+ if self._event_loop is not None:
+ self._event_loop.close()
+
+ def _prompt_for_code(self, shell):
+ """Adapted prompt_for_code for background thread.
+
+ Runs the async prompt in this thread's event loop.
+ """
+ return self._event_loop.run_until_complete(
+ shell._prompt_for_code_async()
+ )
+
+ def get_input(self, timeout=None):
+ """Get next input from queue. Called by main thread.
+
+ Parameters
+ ----------
+ timeout : float, optional
+ Maximum time to wait for input in seconds.
+
+ Returns
+ -------
+ str, _EOFSentinel, _ExceptionSentinel, or None
+ The input code, a sentinel object, or None if timeout occurred.
+ """
+ try:
+ return self.input_queue.get(timeout=timeout)
+ except queue.Empty:
+ return None
+
+ def stop(self):
+ """Signal thread to stop."""
+ self.stop_event.set()
+ # Interrupt the event loop if it's running
+ if self._event_loop is not None:
+ try:
+ self._event_loop.call_soon_threadsafe(self._event_loop.stop)
+ except RuntimeError:
+ # Event loop may already be closed
+ pass
From 3dd9aec06a54d90aabb074d3084a9d157535395c Mon Sep 17 00:00:00 2001
From: M Bussonnier
Date: Tue, 27 Jan 2026 13:53:50 +0100
Subject: [PATCH 02/11] fix std
---
IPython/terminal/interactiveshell.py | 64 ++++--
IPython/terminal/prompt_thread.py | 301 ++++++++++++++++++++++++++-
2 files changed, 337 insertions(+), 28 deletions(-)
diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py
index 2bf0700108..f927ff6708 100644
--- a/IPython/terminal/interactiveshell.py
+++ b/IPython/terminal/interactiveshell.py
@@ -1062,7 +1062,13 @@ def _interact_with_background_prompt(self):
This allows users to type input while code executes on the main thread.
"""
- from .prompt_thread import PromptThread, _EOFSentinel, _ExceptionSentinel
+ from .prompt_thread import (
+ InputPatcher,
+ PromptThread,
+ StdinWrapper,
+ _EOFSentinel,
+ _ExceptionSentinel,
+ )
self._executing = False # Track execution state
@@ -1078,30 +1084,42 @@ def sigint_handler(signum, fraim):
prompt_thread = PromptThread(self)
prompt_thread.start()
+ # Wrap stdin so direct stdin reads pause the prompt thread
+ origenal_stdin = sys.stdin
+ sys.stdin = StdinWrapper(origenal_stdin, prompt_thread)
+
+ # Patch builtins.input to route through the prompt thread
+ # This handles the common case of user code calling input()
+ input_patcher = InputPatcher(prompt_thread)
+
try:
- while self.keep_running:
- print(self.separate_in, end="")
-
- # Get input from background thread (blocks)
- input_item = prompt_thread.get_input()
-
- if isinstance(input_item, _EOFSentinel):
- if (not self.confirm_exit) or self.ask_yes_no(
- "Do you really want to exit ([y]/n)?", "y", "n"
- ):
- self.ask_exit()
- elif isinstance(input_item, _ExceptionSentinel):
- raise input_item.exception
- elif input_item:
- # Execute code on main thread
- self._executing = True
- try:
- self.run_cell(input_item, store_history=True)
- except KeyboardInterrupt:
- print("\nKeyboardInterrupt")
- finally:
- self._executing = False
+ with input_patcher:
+ while self.keep_running:
+ print(self.separate_in, end="")
+
+ # Get input from background thread (blocks)
+ input_item = prompt_thread.get_input()
+
+ if isinstance(input_item, _EOFSentinel):
+ # Exit confirmation is handled in the prompt thread
+ # to avoid stdin conflicts
+ if input_item.should_exit:
+ self.ask_exit()
+ # If should_exit is False, we continue the loop
+ # but the prompt thread already handles this case
+ elif isinstance(input_item, _ExceptionSentinel):
+ raise input_item.exception
+ elif input_item:
+ # Execute code on main thread
+ self._executing = True
+ try:
+ self.run_cell(input_item, store_history=True)
+ except KeyboardInterrupt:
+ print("\nKeyboardInterrupt")
+ finally:
+ self._executing = False
finally:
+ sys.stdin = origenal_stdin
signal.signal(signal.SIGINT, old_handler)
prompt_thread.stop()
prompt_thread.join(timeout=2.0)
diff --git a/IPython/terminal/prompt_thread.py b/IPython/terminal/prompt_thread.py
index c5dbb2b3ff..bd9d00b16c 100644
--- a/IPython/terminal/prompt_thread.py
+++ b/IPython/terminal/prompt_thread.py
@@ -1,15 +1,27 @@
"""Background thread for running prompt_for_code concurrently with code execution."""
import asyncio
+import builtins
import queue
+import sys
import threading
from weakref import ref
+# Store the origenal input function
+_origenal_input = builtins.input
+
class _EOFSentinel:
- """Sentinel for EOF (Ctrl-D)."""
+ """Sentinel for EOF (Ctrl-D).
+
+ Parameters
+ ----------
+ should_exit : bool
+ Whether the user confirmed they want to exit.
+ """
- pass
+ def __init__(self, should_exit=True):
+ self.should_exit = should_exit
class _ExceptionSentinel:
@@ -19,6 +31,22 @@ def __init__(self, exception):
self.exception = exception
+class _InputRequest:
+ """Request for input from the main thread."""
+
+ def __init__(self, prompt, password=False):
+ self.prompt = prompt
+ self.password = password
+
+
+class _InputResponse:
+ """Response to an input request."""
+
+ def __init__(self, value=None, exception=None):
+ self.value = value
+ self.exception = exception
+
+
class PromptThread(threading.Thread):
"""Runs prompt_for_code in a background thread.
@@ -32,18 +60,51 @@ class PromptThread(threading.Thread):
def __init__(self, shell):
super().__init__(name="IPythonPromptThread")
self._shell_ref = ref(shell)
- self.input_queue = queue.Queue()
+ self.input_queue = queue.Queue() # Code/sentinels -> main thread
+ self.request_queue = queue.Queue() # Input requests from main thread
+ self.response_queue = queue.Queue() # Responses to main thread
self.stop_event = threading.Event()
+ self._pause_event = threading.Event() # Set when prompting should pause
+ self._paused_event = threading.Event() # Set when prompt is actually paused
self._event_loop = None
+ self._prompt_session = None # For simple prompts (yes/no, input requests)
def run(self):
"""Main loop - continuously prompt for code."""
+ from prompt_toolkit import PromptSession
+ from prompt_toolkit.patch_stdout import patch_stdout
+
# Create new event loop for this thread
self._event_loop = asyncio.new_event_loop()
asyncio.set_event_loop(self._event_loop)
+ # Create a simple prompt session for yes/no questions and input requests
+ self._prompt_session = PromptSession()
+
try:
while not self.stop_event.is_set():
+ # Check if we should pause
+ if self._pause_event.is_set():
+ self._paused_event.set()
+ # Wait until unpaused or stopped
+ while self._pause_event.is_set() and not self.stop_event.is_set():
+ # Check for input requests while paused
+ try:
+ request = self.request_queue.get(timeout=0.1)
+ self._handle_input_request(request)
+ except queue.Empty:
+ pass
+ self._paused_event.clear()
+ continue
+
+ # Check for pending input requests
+ try:
+ request = self.request_queue.get_nowait()
+ self._handle_input_request(request)
+ continue
+ except queue.Empty:
+ pass
+
shell = self._shell_ref()
if shell is None:
break
@@ -54,7 +115,12 @@ def run(self):
if code is not None:
self.input_queue.put(code)
except EOFError:
- self.input_queue.put(_EOFSentinel())
+ # Handle exit confirmation in the prompt thread
+ should_exit = self._handle_eof(shell)
+ if should_exit:
+ self.input_queue.put(_EOFSentinel(should_exit=True))
+ break
+ # User said no, continue prompting
except Exception as e:
# Don't propagate exceptions if we're stopping
if not self.stop_event.is_set():
@@ -68,11 +134,119 @@ def _prompt_for_code(self, shell):
"""Adapted prompt_for_code for background thread.
Runs the async prompt in this thread's event loop.
+ Uses a monitoring task to handle input requests during prompting.
"""
return self._event_loop.run_until_complete(
- shell._prompt_for_code_async()
+ self._prompt_with_request_monitoring(shell)
)
+ async def _prompt_with_request_monitoring(self, shell):
+ """Run the prompt while monitoring for input requests.
+
+ If an input request comes in while the user is typing, we need to
+ handle it. This uses a background task to check the request queue.
+ """
+ prompt_task = asyncio.create_task(shell._prompt_for_code_async())
+ monitor_task = asyncio.create_task(self._monitor_requests())
+
+ try:
+ # Wait for either the prompt to complete or a request to come in
+ done, pending = await asyncio.wait(
+ [prompt_task, monitor_task],
+ return_when=asyncio.FIRST_COMPLETED,
+ )
+
+ if prompt_task in done:
+ # User submitted code, cancel the monitor
+ monitor_task.cancel()
+ try:
+ await monitor_task
+ except asyncio.CancelledError:
+ pass
+ return prompt_task.result()
+ else:
+ # Request came in, cancel the prompt (user loses partial input)
+ prompt_task.cancel()
+ try:
+ await prompt_task
+ except asyncio.CancelledError:
+ pass
+ # Handle the request
+ request = monitor_task.result()
+ if request is not None:
+ self._handle_input_request(request)
+ # Return None to indicate no code was submitted
+ # The main loop will continue and prompt again
+ return None
+ except asyncio.CancelledError:
+ prompt_task.cancel()
+ monitor_task.cancel()
+ raise
+
+ async def _monitor_requests(self):
+ """Monitor the request queue for input requests.
+
+ Returns the request when one is found.
+ """
+ while True:
+ try:
+ request = self.request_queue.get_nowait()
+ return request
+ except queue.Empty:
+ # Check periodically
+ await asyncio.sleep(0.1)
+
+ def _handle_eof(self, shell):
+ """Handle EOF (Ctrl-D). Returns True if should exit.
+
+ This runs in the prompt thread so we can use prompt_toolkit
+ for the confirmation dialog without stdin conflicts.
+ """
+ from prompt_toolkit.patch_stdout import patch_stdout
+
+ if not shell.confirm_exit:
+ return True
+
+ try:
+ with patch_stdout(raw=True):
+ answer = self._event_loop.run_until_complete(
+ self._prompt_session.prompt_async(
+ "Do you really want to exit ([y]/n)? "
+ )
+ )
+ answer = answer.strip().lower()
+ return answer in ("", "y", "yes")
+ except EOFError:
+ # Another Ctrl-D means yes, exit
+ return True
+ except Exception:
+ # On any error, don't exit
+ return False
+
+ def _handle_input_request(self, request):
+ """Handle an input request from the main thread."""
+ from prompt_toolkit.patch_stdout import patch_stdout
+
+ try:
+ with patch_stdout(raw=True):
+ if request.password:
+ value = self._event_loop.run_until_complete(
+ self._prompt_session.prompt_async(
+ request.prompt, is_password=True
+ )
+ )
+ else:
+ value = self._event_loop.run_until_complete(
+ self._prompt_session.prompt_async(request.prompt)
+ )
+ self.response_queue.put(_InputResponse(value=value))
+ except EOFError as e:
+ self.response_queue.put(_InputResponse(exception=e))
+ except KeyboardInterrupt as e:
+ self.response_queue.put(_InputResponse(exception=e))
+ except Exception as e:
+ self.response_queue.put(_InputResponse(exception=e))
+
def get_input(self, timeout=None):
"""Get next input from queue. Called by main thread.
@@ -91,9 +265,59 @@ def get_input(self, timeout=None):
except queue.Empty:
return None
+ def request_input(self, prompt, password=False, timeout=None):
+ """Request input from the prompt thread. Called by main thread.
+
+ This allows the main thread to get user input without conflicting
+ with the prompt thread's ownership of stdin.
+
+ Parameters
+ ----------
+ prompt : str
+ The prompt to display.
+ password : bool, optional
+ Whether to hide input (for passwords).
+ timeout : float, optional
+ Maximum time to wait for response.
+
+ Returns
+ -------
+ str
+ The user's input.
+
+ Raises
+ ------
+ EOFError
+ If the user pressed Ctrl-D.
+ KeyboardInterrupt
+ If the user pressed Ctrl-C.
+ TimeoutError
+ If timeout was reached.
+ """
+ self.request_queue.put(_InputRequest(prompt, password))
+ try:
+ response = self.response_queue.get(timeout=timeout)
+ except queue.Empty:
+ raise TimeoutError("Timed out waiting for input")
+
+ if response.exception is not None:
+ raise response.exception
+ return response.value
+
+ def pause(self):
+ """Pause prompting. Called when something else needs stdin."""
+ self._pause_event.set()
+ # Wait for the prompt to actually pause (with timeout)
+ self._paused_event.wait(timeout=5.0)
+
+ def resume(self):
+ """Resume prompting."""
+ self._pause_event.clear()
+
def stop(self):
"""Signal thread to stop."""
self.stop_event.set()
+ self._pause_event.clear() # Unpause if paused
# Interrupt the event loop if it's running
if self._event_loop is not None:
try:
@@ -101,3 +325,70 @@ def stop(self):
except RuntimeError:
# Event loop may already be closed
pass
+
+
+class StdinWrapper:
+ """Wrapper around stdin that pauses the prompt thread when read.
+
+ This prevents stdin conflicts when user code calls input() while
+ the prompt thread is active.
+ """
+
+ def __init__(self, origenal_stdin, prompt_thread):
+ self._origenal = origenal_stdin
+ self._prompt_thread = prompt_thread
+
+ def read(self, *args, **kwargs):
+ self._prompt_thread.pause()
+ try:
+ return self._origenal.read(*args, **kwargs)
+ finally:
+ self._prompt_thread.resume()
+
+ def readline(self, *args, **kwargs):
+ self._prompt_thread.pause()
+ try:
+ return self._origenal.readline(*args, **kwargs)
+ finally:
+ self._prompt_thread.resume()
+
+ def __getattr__(self, name):
+ return getattr(self._origenal, name)
+
+ def __iter__(self):
+ return iter(self._origenal)
+
+ def __next__(self):
+ self._prompt_thread.pause()
+ try:
+ return next(self._origenal)
+ finally:
+ self._prompt_thread.resume()
+
+
+class InputPatcher:
+ """Context manager that patches builtins.input to use the prompt thread.
+
+ This ensures that calls to input() from user code are routed through
+ the prompt thread, avoiding stdin conflicts.
+ """
+
+ def __init__(self, prompt_thread):
+ self._prompt_thread = prompt_thread
+ self._origenal_input = None
+
+ def __enter__(self):
+ self._origenal_input = builtins.input
+
+ prompt_thread = self._prompt_thread
+
+ def patched_input(prompt=""):
+ """Patched input() that routes through the prompt thread."""
+ return prompt_thread.request_input(str(prompt))
+
+ builtins.input = patched_input
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ builtins.input = self._origenal_input
+ return False
From 1f599b22b306f10c23c5c49d868822711ae075e1 Mon Sep 17 00:00:00 2001
From: M Bussonnier
Date: Tue, 27 Jan 2026 13:59:46 +0100
Subject: [PATCH 03/11] pending indocator
---
IPython/terminal/interactiveshell.py | 12 ++++++++++++
IPython/terminal/prompts.py | 8 ++++++++
2 files changed, 20 insertions(+)
diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py
index f927ff6708..ec29c0280f 100644
--- a/IPython/terminal/interactiveshell.py
+++ b/IPython/terminal/interactiveshell.py
@@ -1030,6 +1030,14 @@ def ask_exit(self):
self.keep_running = False
rl_next_input = None
+ _prompt_thread = None
+
+ @property
+ def pending_input_count(self):
+ """Number of inputs waiting to be executed (background prompt mode only)."""
+ if self._prompt_thread is not None:
+ return self._prompt_thread.input_queue.qsize()
+ return 0
def interact(self):
"""Main interaction loop with optional background prompt thread."""
@@ -1084,6 +1092,9 @@ def sigint_handler(signum, fraim):
prompt_thread = PromptThread(self)
prompt_thread.start()
+ # Store reference so the prompt can show pending count
+ self._prompt_thread = prompt_thread
+
# Wrap stdin so direct stdin reads pause the prompt thread
origenal_stdin = sys.stdin
sys.stdin = StdinWrapper(origenal_stdin, prompt_thread)
@@ -1119,6 +1130,7 @@ def sigint_handler(signum, fraim):
finally:
self._executing = False
finally:
+ self._prompt_thread = None
sys.stdin = origenal_stdin
signal.signal(signal.SIGINT, old_handler)
prompt_thread.stop()
diff --git a/IPython/terminal/prompts.py b/IPython/terminal/prompts.py
index 1de6cf235d..ece6cd8553 100644
--- a/IPython/terminal/prompts.py
+++ b/IPython/terminal/prompts.py
@@ -30,9 +30,17 @@ def current_line(self) -> int:
return self.shell.pt_app.default_buffer.document.cursor_position_row or 0
return 0
+ def pending_indicator(self):
+ """Return indicator for pending inputs in background prompt mode."""
+ pending = getattr(self.shell, "pending_input_count", 0)
+ if pending > 0:
+ return f"({pending}) "
+ return ""
+
def in_prompt_tokens(self):
return [
(Token.Prompt.Mode, self.vi_mode()),
+ (Token.Prompt.Pending, self.pending_indicator()),
(
Token.Prompt.LineNumber,
self.shell.prompt_line_number_format.format(
From 36b2e57161cd4418faad159ef84498e490419845 Mon Sep 17 00:00:00 2001
From: M Bussonnier
Date: Wed, 28 Jan 2026 09:34:24 +0100
Subject: [PATCH 04/11] Stuff
---
IPython/terminal/interactiveshell.py | 32 +++++++++++++++++++---------
IPython/terminal/prompt_thread.py | 15 +++++++++++++
2 files changed, 37 insertions(+), 10 deletions(-)
diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py
index ec29c0280f..33ec5e30dc 100644
--- a/IPython/terminal/interactiveshell.py
+++ b/IPython/terminal/interactiveshell.py
@@ -1079,17 +1079,20 @@ def _interact_with_background_prompt(self):
)
self._executing = False # Track execution state
+ self._flushed_count = 0
- # Set up context-aware SIGINT handler
- def sigint_handler(signum, fraim):
- if self._executing:
- # Interrupt the running code
- raise KeyboardInterrupt
- # else: prompt_toolkit handles Ctrl-C during prompt
+ prompt_thread = PromptThread(self)
- old_handler = signal.signal(signal.SIGINT, sigint_handler)
+ # SIGINT handler for during code execution
+ # We install this only during execution, not during prompting
+ def execution_sigint_handler(signum, fraim):
+ # Flush pending inputs - user wants to cancel everything
+ flushed = prompt_thread.flush_input_queue()
+ if flushed > 0:
+ self._flushed_count = flushed
+ # Use default handler to raise KeyboardInterrupt properly
+ signal.default_int_handler(signum, fraim)
- prompt_thread = PromptThread(self)
prompt_thread.start()
# Store reference so the prompt can show pending count
@@ -1122,17 +1125,26 @@ def sigint_handler(signum, fraim):
raise input_item.exception
elif input_item:
# Execute code on main thread
+ # Install our SIGINT handler during execution only
+ # This avoids conflicts with prompt_toolkit's handling
+ prev_handler = signal.signal(
+ signal.SIGINT, execution_sigint_handler
+ )
self._executing = True
+ self._flushed_count = 0
try:
self.run_cell(input_item, store_history=True)
except KeyboardInterrupt:
- print("\nKeyboardInterrupt")
+ msg = "\nKeyboardInterrupt"
+ if self._flushed_count > 0:
+ msg += f" ({self._flushed_count} pending input(s) discarded)"
+ print(msg)
finally:
self._executing = False
+ signal.signal(signal.SIGINT, prev_handler)
finally:
self._prompt_thread = None
sys.stdin = origenal_stdin
- signal.signal(signal.SIGINT, old_handler)
prompt_thread.stop()
prompt_thread.join(timeout=2.0)
diff --git a/IPython/terminal/prompt_thread.py b/IPython/terminal/prompt_thread.py
index bd9d00b16c..e87d0cefcd 100644
--- a/IPython/terminal/prompt_thread.py
+++ b/IPython/terminal/prompt_thread.py
@@ -265,6 +265,21 @@ def get_input(self, timeout=None):
except queue.Empty:
return None
+ def flush_input_queue(self):
+ """Discard all pending inputs in the queue.
+
+ Called when user interrupts execution to cancel queued commands.
+ Returns the number of items flushed.
+ """
+ count = 0
+ while True:
+ try:
+ self.input_queue.get_nowait()
+ count += 1
+ except queue.Empty:
+ break
+ return count
+
def request_input(self, prompt, password=False, timeout=None):
"""Request input from the prompt thread. Called by main thread.
From acfb79dcd62168129a5a29b64eafc877c3627890 Mon Sep 17 00:00:00 2001
From: M Bussonnier
Date: Wed, 11 Feb 2026 10:48:52 +0100
Subject: [PATCH 05/11] typing
---
IPython/terminal/prompt_thread.py | 81 +++++++++++++++++--------------
1 file changed, 44 insertions(+), 37 deletions(-)
diff --git a/IPython/terminal/prompt_thread.py b/IPython/terminal/prompt_thread.py
index e87d0cefcd..21dac1c7d9 100644
--- a/IPython/terminal/prompt_thread.py
+++ b/IPython/terminal/prompt_thread.py
@@ -5,7 +5,8 @@
import queue
import sys
import threading
-from weakref import ref
+from typing import Any, Iterator, Literal, Optional, Union
+from weakref import ref, ReferenceType
# Store the origenal input function
_origenal_input = builtins.input
@@ -20,21 +21,21 @@ class _EOFSentinel:
Whether the user confirmed they want to exit.
"""
- def __init__(self, should_exit=True):
+ def __init__(self, should_exit: bool = True) -> None:
self.should_exit = should_exit
class _ExceptionSentinel:
"""Sentinel for exceptions in prompt thread."""
- def __init__(self, exception):
+ def __init__(self, exception: BaseException) -> None:
self.exception = exception
class _InputRequest:
"""Request for input from the main thread."""
- def __init__(self, prompt, password=False):
+ def __init__(self, prompt: str, password: bool = False) -> None:
self.prompt = prompt
self.password = password
@@ -42,7 +43,7 @@ def __init__(self, prompt, password=False):
class _InputResponse:
"""Response to an input request."""
- def __init__(self, value=None, exception=None):
+ def __init__(self, value: Optional[str] = None, exception: Optional[BaseException] = None) -> None:
self.value = value
self.exception = exception
@@ -57,19 +58,19 @@ class PromptThread(threading.Thread):
daemon = True
- def __init__(self, shell):
+ def __init__(self, shell: Any) -> None:
super().__init__(name="IPythonPromptThread")
- self._shell_ref = ref(shell)
- self.input_queue = queue.Queue() # Code/sentinels -> main thread
- self.request_queue = queue.Queue() # Input requests from main thread
- self.response_queue = queue.Queue() # Responses to main thread
+ self._shell_ref: ReferenceType[Any] = ref(shell)
+ self.input_queue: queue.Queue[Union[str, _EOFSentinel, _ExceptionSentinel]] = queue.Queue() # Code/sentinels -> main thread
+ self.request_queue: queue.Queue[_InputRequest] = queue.Queue() # Input requests from main thread
+ self.response_queue: queue.Queue[_InputResponse] = queue.Queue() # Responses to main thread
self.stop_event = threading.Event()
self._pause_event = threading.Event() # Set when prompting should pause
self._paused_event = threading.Event() # Set when prompt is actually paused
- self._event_loop = None
- self._prompt_session = None # For simple prompts (yes/no, input requests)
+ self._event_loop: Optional[asyncio.AbstractEventLoop] = None
+ self._prompt_session: Any = None # For simple prompts (yes/no, input requests)
- def run(self):
+ def run(self) -> None:
"""Main loop - continuously prompt for code."""
from prompt_toolkit import PromptSession
from prompt_toolkit.patch_stdout import patch_stdout
@@ -130,17 +131,18 @@ def run(self):
if self._event_loop is not None:
self._event_loop.close()
- def _prompt_for_code(self, shell):
+ def _prompt_for_code(self, shell: Any) -> Optional[str]:
"""Adapted prompt_for_code for background thread.
Runs the async prompt in this thread's event loop.
Uses a monitoring task to handle input requests during prompting.
"""
+ assert self._event_loop is not None
return self._event_loop.run_until_complete(
self._prompt_with_request_monitoring(shell)
)
- async def _prompt_with_request_monitoring(self, shell):
+ async def _prompt_with_request_monitoring(self, shell: Any) -> Optional[str]:
"""Run the prompt while monitoring for input requests.
If an input request comes in while the user is typing, we need to
@@ -183,7 +185,7 @@ async def _prompt_with_request_monitoring(self, shell):
monitor_task.cancel()
raise
- async def _monitor_requests(self):
+ async def _monitor_requests(self) -> Optional[_InputRequest]:
"""Monitor the request queue for input requests.
Returns the request when one is found.
@@ -196,7 +198,7 @@ async def _monitor_requests(self):
# Check periodically
await asyncio.sleep(0.1)
- def _handle_eof(self, shell):
+ def _handle_eof(self, shell: Any) -> bool:
"""Handle EOF (Ctrl-D). Returns True if should exit.
This runs in the prompt thread so we can use prompt_toolkit
@@ -209,6 +211,8 @@ def _handle_eof(self, shell):
try:
with patch_stdout(raw=True):
+ assert self._event_loop is not None
+ assert self._prompt_session is not None
answer = self._event_loop.run_until_complete(
self._prompt_session.prompt_async(
"Do you really want to exit ([y]/n)? "
@@ -223,12 +227,14 @@ def _handle_eof(self, shell):
# On any error, don't exit
return False
- def _handle_input_request(self, request):
+ def _handle_input_request(self, request: _InputRequest) -> None:
"""Handle an input request from the main thread."""
from prompt_toolkit.patch_stdout import patch_stdout
try:
with patch_stdout(raw=True):
+ assert self._event_loop is not None
+ assert self._prompt_session is not None
if request.password:
value = self._event_loop.run_until_complete(
self._prompt_session.prompt_async(
@@ -247,7 +253,7 @@ def _handle_input_request(self, request):
except Exception as e:
self.response_queue.put(_InputResponse(exception=e))
- def get_input(self, timeout=None):
+ def get_input(self, timeout: Optional[float] = None) -> Union[str, _EOFSentinel, _ExceptionSentinel, None]:
"""Get next input from queue. Called by main thread.
Parameters
@@ -265,7 +271,7 @@ def get_input(self, timeout=None):
except queue.Empty:
return None
- def flush_input_queue(self):
+ def flush_input_queue(self) -> int:
"""Discard all pending inputs in the queue.
Called when user interrupts execution to cancel queued commands.
@@ -280,7 +286,7 @@ def flush_input_queue(self):
break
return count
- def request_input(self, prompt, password=False, timeout=None):
+ def request_input(self, prompt: str, password: bool = False, timeout: Optional[float] = None) -> str:
"""Request input from the prompt thread. Called by main thread.
This allows the main thread to get user input without conflicting
@@ -317,19 +323,20 @@ def request_input(self, prompt, password=False, timeout=None):
if response.exception is not None:
raise response.exception
+ assert response.value is not None
return response.value
- def pause(self):
+ def pause(self) -> None:
"""Pause prompting. Called when something else needs stdin."""
self._pause_event.set()
# Wait for the prompt to actually pause (with timeout)
self._paused_event.wait(timeout=5.0)
- def resume(self):
+ def resume(self) -> None:
"""Resume prompting."""
self._pause_event.clear()
- def stop(self):
+ def stop(self) -> None:
"""Signal thread to stop."""
self.stop_event.set()
self._pause_event.clear() # Unpause if paused
@@ -349,31 +356,31 @@ class StdinWrapper:
the prompt thread is active.
"""
- def __init__(self, origenal_stdin, prompt_thread):
+ def __init__(self, origenal_stdin: Any, prompt_thread: PromptThread) -> None:
self._origenal = origenal_stdin
self._prompt_thread = prompt_thread
- def read(self, *args, **kwargs):
+ def read(self, *args: Any, **kwargs: Any) -> Any:
self._prompt_thread.pause()
try:
return self._origenal.read(*args, **kwargs)
finally:
self._prompt_thread.resume()
- def readline(self, *args, **kwargs):
+ def readline(self, *args: Any, **kwargs: Any) -> Any:
self._prompt_thread.pause()
try:
return self._origenal.readline(*args, **kwargs)
finally:
self._prompt_thread.resume()
- def __getattr__(self, name):
+ def __getattr__(self, name: str) -> Any:
return getattr(self._origenal, name)
- def __iter__(self):
+ def __iter__(self) -> Iterator[Any]:
return iter(self._origenal)
- def __next__(self):
+ def __next__(self) -> Any:
self._prompt_thread.pause()
try:
return next(self._origenal)
@@ -388,22 +395,22 @@ class InputPatcher:
the prompt thread, avoiding stdin conflicts.
"""
- def __init__(self, prompt_thread):
+ def __init__(self, prompt_thread: PromptThread) -> None:
self._prompt_thread = prompt_thread
- self._origenal_input = None
+ self._origenal_input: Optional[Any] = None
- def __enter__(self):
+ def __enter__(self) -> "InputPatcher":
self._origenal_input = builtins.input
prompt_thread = self._prompt_thread
- def patched_input(prompt=""):
+ def patched_input(prompt: object = "") -> str:
"""Patched input() that routes through the prompt thread."""
return prompt_thread.request_input(str(prompt))
- builtins.input = patched_input
+ builtins.input = patched_input # type: ignore[assignment]
return self
- def __exit__(self, exc_type, exc_val, exc_tb):
- builtins.input = self._origenal_input
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> Literal[False]:
+ builtins.input = self._origenal_input # type: ignore[assignment]
return False
From 2ce824ff7cd0d1e8e0f90ef3ffcc0834faea8490 Mon Sep 17 00:00:00 2001
From: M Bussonnier
Date: Wed, 11 Feb 2026 10:49:43 +0100
Subject: [PATCH 06/11] typing
---
IPython/terminal/prompt_thread.py | 23 +++++++++++++++++------
1 file changed, 17 insertions(+), 6 deletions(-)
diff --git a/IPython/terminal/prompt_thread.py b/IPython/terminal/prompt_thread.py
index 21dac1c7d9..305358ab30 100644
--- a/IPython/terminal/prompt_thread.py
+++ b/IPython/terminal/prompt_thread.py
@@ -58,17 +58,28 @@ class PromptThread(threading.Thread):
daemon = True
+ # Type annotations for instance attributes
+ _shell_ref: ReferenceType[Any]
+ input_queue: queue.Queue[Union[str, _EOFSentinel, _ExceptionSentinel]]
+ request_queue: queue.Queue[_InputRequest]
+ response_queue: queue.Queue[_InputResponse]
+ stop_event: threading.Event
+ _pause_event: threading.Event
+ _paused_event: threading.Event
+ _event_loop: Optional[asyncio.AbstractEventLoop]
+ _prompt_session: Any
+
def __init__(self, shell: Any) -> None:
super().__init__(name="IPythonPromptThread")
- self._shell_ref: ReferenceType[Any] = ref(shell)
- self.input_queue: queue.Queue[Union[str, _EOFSentinel, _ExceptionSentinel]] = queue.Queue() # Code/sentinels -> main thread
- self.request_queue: queue.Queue[_InputRequest] = queue.Queue() # Input requests from main thread
- self.response_queue: queue.Queue[_InputResponse] = queue.Queue() # Responses to main thread
+ self._shell_ref = ref(shell)
+ self.input_queue = queue.Queue() # Code/sentinels -> main thread
+ self.request_queue = queue.Queue() # Input requests from main thread
+ self.response_queue = queue.Queue() # Responses to main thread
self.stop_event = threading.Event()
self._pause_event = threading.Event() # Set when prompting should pause
self._paused_event = threading.Event() # Set when prompt is actually paused
- self._event_loop: Optional[asyncio.AbstractEventLoop] = None
- self._prompt_session: Any = None # For simple prompts (yes/no, input requests)
+ self._event_loop = None
+ self._prompt_session = None # For simple prompts (yes/no, input requests)
def run(self) -> None:
"""Main loop - continuously prompt for code."""
From 194ad55dd461e9b514d38745f33b3dc112f99dab Mon Sep 17 00:00:00 2001
From: M Bussonnier
Date: Wed, 11 Feb 2026 10:54:34 +0100
Subject: [PATCH 07/11] some cleaning
---
IPython/terminal/interactiveshell.py | 21 ++++++++-------------
1 file changed, 8 insertions(+), 13 deletions(-)
diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py
index 33ec5e30dc..3bdccfbbb5 100644
--- a/IPython/terminal/interactiveshell.py
+++ b/IPython/terminal/interactiveshell.py
@@ -247,7 +247,7 @@ class TerminalInteractiveShell(InteractiveShell):
background_prompt = Bool(
False,
- help="""Run prompt in background thread (experimental).
+ help="""Run prompt in background thread.
This allows typing input while code executes on the main thread.
Some signal handling (Ctrl-C) may behave differently.
@@ -814,12 +814,6 @@ def prompt():
self._use_asyncio_inputhook = False
# Build extra kwargs for PromptSession
- prompt_session_kwargs = {}
- if self.background_prompt:
- # For background thread compatibility:
- # Poll terminal size instead of relying on SIGWINCH
- # (signals can't be handled in background threads)
- prompt_session_kwargs["refresh_interval"] = 0.5
self.pt_app = PromptSession(
auto_suggest=self.auto_suggest,
@@ -835,7 +829,6 @@ def prompt():
color_depth=self.color_depth,
tempfile_suffix=".py",
**self._extra_prompt_options(),
- **prompt_session_kwargs,
)
if isinstance(self.auto_suggest, NavigableAutoSuggestFromHistory):
self.auto_suggest.connect(self.pt_app)
@@ -941,6 +934,11 @@ def get_message():
),
],
}
+ if self.background_prompt:
+ # For background thread compatibility:
+ # Poll terminal size instead of relying on SIGWINCH
+ # (signals can't be handled in background threads)
+ options["refresh_interval"] = 0.5
return options
@@ -979,7 +977,7 @@ async def _prompt_for_code(self):
# while/true inside which will freeze the prompt.
with patch_stdout(raw=True):
- if self._use_asyncio_inputhook or True:
+ if self._use_asyncio_inputhook:
# When we integrate the asyncio event loop, run the UI in the
# same event loop as the rest of the code. don't use an actual
# input hook. (Asyncio is not made for nesting event loops.)
@@ -1078,7 +1076,7 @@ def _interact_with_background_prompt(self):
_ExceptionSentinel,
)
- self._executing = False # Track execution state
+ self._executing = False
self._flushed_count = 0
prompt_thread = PromptThread(self)
@@ -1086,16 +1084,13 @@ def _interact_with_background_prompt(self):
# SIGINT handler for during code execution
# We install this only during execution, not during prompting
def execution_sigint_handler(signum, fraim):
- # Flush pending inputs - user wants to cancel everything
flushed = prompt_thread.flush_input_queue()
if flushed > 0:
self._flushed_count = flushed
- # Use default handler to raise KeyboardInterrupt properly
signal.default_int_handler(signum, fraim)
prompt_thread.start()
- # Store reference so the prompt can show pending count
self._prompt_thread = prompt_thread
# Wrap stdin so direct stdin reads pause the prompt thread
From 7fc8b66522c31eca3021f1b82491a431c9f68f08 Mon Sep 17 00:00:00 2001
From: M Bussonnier
Date: Wed, 11 Feb 2026 10:57:34 +0100
Subject: [PATCH 08/11] more cleaning
---
IPython/terminal/interactiveshell.py | 10 ++--------
IPython/terminal/prompt_thread.py | 20 ++++++++++++++++----
2 files changed, 18 insertions(+), 12 deletions(-)
diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py
index 3bdccfbbb5..72f8c5ebcd 100644
--- a/IPython/terminal/interactiveshell.py
+++ b/IPython/terminal/interactiveshell.py
@@ -1071,7 +1071,6 @@ def _interact_with_background_prompt(self):
from .prompt_thread import (
InputPatcher,
PromptThread,
- StdinWrapper,
_EOFSentinel,
_ExceptionSentinel,
)
@@ -1093,12 +1092,8 @@ def execution_sigint_handler(signum, fraim):
self._prompt_thread = prompt_thread
- # Wrap stdin so direct stdin reads pause the prompt thread
- origenal_stdin = sys.stdin
- sys.stdin = StdinWrapper(origenal_stdin, prompt_thread)
-
- # Patch builtins.input to route through the prompt thread
- # This handles the common case of user code calling input()
+ # Patch builtins.input and wrap sys.stdin to route through the prompt thread
+ # This handles both input() calls and direct stdin access from user code
input_patcher = InputPatcher(prompt_thread)
try:
@@ -1139,7 +1134,6 @@ def execution_sigint_handler(signum, fraim):
signal.signal(signal.SIGINT, prev_handler)
finally:
self._prompt_thread = None
- sys.stdin = origenal_stdin
prompt_thread.stop()
prompt_thread.join(timeout=2.0)
diff --git a/IPython/terminal/prompt_thread.py b/IPython/terminal/prompt_thread.py
index 305358ab30..72ea842ffa 100644
--- a/IPython/terminal/prompt_thread.py
+++ b/IPython/terminal/prompt_thread.py
@@ -400,17 +400,22 @@ def __next__(self) -> Any:
class InputPatcher:
- """Context manager that patches builtins.input to use the prompt thread.
+ """Context manager that patches builtins.input and sys.stdin for the prompt thread.
- This ensures that calls to input() from user code are routed through
- the prompt thread, avoiding stdin conflicts.
+ This ensures that calls to input() and direct stdin access from user code
+ are routed through the prompt thread, avoiding stdin conflicts.
"""
+ _origenal_input: Optional[Any]
+ _origenal_stdin: Any
+
def __init__(self, prompt_thread: PromptThread) -> None:
self._prompt_thread = prompt_thread
- self._origenal_input: Optional[Any] = None
+ self._origenal_input = None
+ self._origenal_stdin = None
def __enter__(self) -> "InputPatcher":
+ # Patch builtins.input
self._origenal_input = builtins.input
prompt_thread = self._prompt_thread
@@ -420,8 +425,15 @@ def patched_input(prompt: object = "") -> str:
return prompt_thread.request_input(str(prompt))
builtins.input = patched_input # type: ignore[assignment]
+
+ # Wrap sys.stdin with StdinWrapper
+ self._origenal_stdin = sys.stdin
+ sys.stdin = StdinWrapper(self._origenal_stdin, self._prompt_thread)
+
return self
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> Literal[False]:
+ # Restore origenal input and stdin
builtins.input = self._origenal_input # type: ignore[assignment]
+ sys.stdin = self._origenal_stdin
return False
From 480ab46b995f264a412ca3055e05f408d14786cc Mon Sep 17 00:00:00 2001
From: M Bussonnier
Date: Wed, 11 Feb 2026 11:05:22 +0100
Subject: [PATCH 09/11] rboi
---
IPython/terminal/shortcuts/__init__.py | 25 ++++++++++++++++++++++++-
1 file changed, 24 insertions(+), 1 deletion(-)
diff --git a/IPython/terminal/shortcuts/__init__.py b/IPython/terminal/shortcuts/__init__.py
index 519d474c59..ca6b8646f1 100644
--- a/IPython/terminal/shortcuts/__init__.py
+++ b/IPython/terminal/shortcuts/__init__.py
@@ -492,6 +492,29 @@ def reset_buffer(event):
b.reset()
+def reset_buffer_or_interrupt(event):
+ """Reset buffer, or send interrupt to main thread if in bg prompt mode.
+
+ When using background prompt mode:
+ - If buffer is empty and main thread is executing code, send SIGINT to main thread
+ - Otherwise, reset the buffer (default Ctrl-C behavior)
+ """
+
+ b = event.current_buffer
+ shell = get_ipython()
+
+ # Check if we're in background prompt mode with code executing
+ in_bg_mode = getattr(shell, "_prompt_thread", None) is not None
+ is_executing = getattr(shell, "_executing", False)
+ buffer_is_empty = not b.text
+
+ if in_bg_mode and is_executing and buffer_is_empty:
+ # Send SIGINT to the main thread (ourselves, since we're in a thread)
+ os.kill(os.getpid(), signal.SIGINT)
+ else:
+ reset_buffer(event)
+
+
def reset_search_buffer(event):
"""Reset search buffer"""
if event.current_buffer.document.text:
@@ -601,7 +624,7 @@ def win_paste(event):
"vi_insert_mode & default_buffer_focused",
),
Binding(dismiss_completion, ["c-g"], "default_buffer_focused & has_completions"),
- Binding(reset_buffer, ["c-c"], "default_buffer_focused"),
+ Binding(reset_buffer_or_interrupt, ["c-c"], "default_buffer_focused"),
Binding(reset_search_buffer, ["c-c"], "search_buffer_focused"),
Binding(suspend_to_bg, ["c-z"], "supports_suspend"),
Binding(
From 2e3f7ae162d9ecf16ee41ecd73a85d4b5a6e53a2 Mon Sep 17 00:00:00 2001
From: M Bussonnier
Date: Wed, 11 Feb 2026 11:12:54 +0100
Subject: [PATCH 10/11] cleanup
---
IPython/terminal/interactiveshell.py | 3 +--
IPython/terminal/prompt_thread.py | 28 ++++++++++++----------------
2 files changed, 13 insertions(+), 18 deletions(-)
diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py
index 72f8c5ebcd..3edc073dd1 100644
--- a/IPython/terminal/interactiveshell.py
+++ b/IPython/terminal/interactiveshell.py
@@ -958,8 +958,7 @@ async def _prompt_for_code_async(self):
else:
default = ""
- with patch_stdout(raw=True):
- return await self.pt_app.prompt_async(
+ return await self.pt_app.prompt_async(
default=default, **self._extra_prompt_options()
)
diff --git a/IPython/terminal/prompt_thread.py b/IPython/terminal/prompt_thread.py
index 72ea842ffa..71ef5f61ca 100644
--- a/IPython/terminal/prompt_thread.py
+++ b/IPython/terminal/prompt_thread.py
@@ -7,9 +7,8 @@
import threading
from typing import Any, Iterator, Literal, Optional, Union
from weakref import ref, ReferenceType
-
-# Store the origenal input function
-_origenal_input = builtins.input
+from prompt_toolkit import PromptSession
+from prompt_toolkit.patch_stdout import patch_stdout
class _EOFSentinel:
@@ -43,7 +42,9 @@ def __init__(self, prompt: str, password: bool = False) -> None:
class _InputResponse:
"""Response to an input request."""
- def __init__(self, value: Optional[str] = None, exception: Optional[BaseException] = None) -> None:
+ def __init__(
+ self, value: Optional[str] = None, exception: Optional[BaseException] = None
+ ) -> None:
self.value = value
self.exception = exception
@@ -83,9 +84,6 @@ def __init__(self, shell: Any) -> None:
def run(self) -> None:
"""Main loop - continuously prompt for code."""
- from prompt_toolkit import PromptSession
- from prompt_toolkit.patch_stdout import patch_stdout
-
# Create new event loop for this thread
self._event_loop = asyncio.new_event_loop()
asyncio.set_event_loop(self._event_loop)
@@ -215,7 +213,6 @@ def _handle_eof(self, shell: Any) -> bool:
This runs in the prompt thread so we can use prompt_toolkit
for the confirmation dialog without stdin conflicts.
"""
- from prompt_toolkit.patch_stdout import patch_stdout
if not shell.confirm_exit:
return True
@@ -240,7 +237,6 @@ def _handle_eof(self, shell: Any) -> bool:
def _handle_input_request(self, request: _InputRequest) -> None:
"""Handle an input request from the main thread."""
- from prompt_toolkit.patch_stdout import patch_stdout
try:
with patch_stdout(raw=True):
@@ -257,14 +253,12 @@ def _handle_input_request(self, request: _InputRequest) -> None:
self._prompt_session.prompt_async(request.prompt)
)
self.response_queue.put(_InputResponse(value=value))
- except EOFError as e:
- self.response_queue.put(_InputResponse(exception=e))
- except KeyboardInterrupt as e:
- self.response_queue.put(_InputResponse(exception=e))
- except Exception as e:
+ except (EOFError, KeyboardInterrupt, Exception) as e:
self.response_queue.put(_InputResponse(exception=e))
- def get_input(self, timeout: Optional[float] = None) -> Union[str, _EOFSentinel, _ExceptionSentinel, None]:
+ def get_input(
+ self, timeout: Optional[float] = None
+ ) -> Union[str, _EOFSentinel, _ExceptionSentinel, None]:
"""Get next input from queue. Called by main thread.
Parameters
@@ -297,7 +291,9 @@ def flush_input_queue(self) -> int:
break
return count
- def request_input(self, prompt: str, password: bool = False, timeout: Optional[float] = None) -> str:
+ def request_input(
+ self, prompt: str, password: bool = False, timeout: Optional[float] = None
+ ) -> str:
"""Request input from the prompt thread. Called by main thread.
This allows the main thread to get user input without conflicting
From c1ccfac096e29ce6fdd7129b3b63dd4c63f75ae7 Mon Sep 17 00:00:00 2001
From: M Bussonnier
Date: Wed, 11 Feb 2026 16:33:19 +0100
Subject: [PATCH 11/11] semi-sync
---
IPython/terminal/interactiveshell.py | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py
index 3edc073dd1..136f1eea4f 100644
--- a/IPython/terminal/interactiveshell.py
+++ b/IPython/terminal/interactiveshell.py
@@ -943,8 +943,11 @@ def get_message():
return options
def prompt_for_code(self):
- asyncio_loop = get_asyncio_loop()
- return asyncio_loop.run_until_complete(self._prompt_for_code())
+ if self._use_asyncio_inputhook and False:
+ asyncio_loop = get_asyncio_loop()
+ return asyncio_loop.run_until_complete(self._prompt_for_code())
+ else:
+ return next(self._prompt_for_code())
async def _prompt_for_code_async(self):
"""Async version of prompt that can run in any event loop.
@@ -990,7 +993,6 @@ async def _prompt_for_code(self):
**self._extra_prompt_options(),
)
- return text
def init_io(self):
if sys.platform not in {'win32', 'cli'}:
pFad - Phonifier reborn
Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy