From 6e6e5d0927bfa4e1cf7b940da2887fa8f673b9f6 Mon Sep 17 00:00:00 2001 From: Geoffrey Thomas Date: Sun, 27 Jul 2025 02:10:06 -0400 Subject: [PATCH 1/2] Fix Ctrl-C handling in REPL Python's readline module is unable to handle signals when it's called not on the main thread, and it's blocking so we can't call it from the same thread as Trio. Get rid of the additional thread and handle input ourselves. --- src/trio/_repl.py | 106 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 102 insertions(+), 4 deletions(-) diff --git a/src/trio/_repl.py b/src/trio/_repl.py index 8be5af8fb8..c00ad031a9 100644 --- a/src/trio/_repl.py +++ b/src/trio/_repl.py @@ -2,7 +2,9 @@ import ast import contextlib +import ctypes import inspect +import os import sys import types import warnings @@ -19,15 +21,35 @@ class TrioInteractiveConsole(InteractiveConsole): def __init__(self, repl_locals: dict[str, object] | None = None) -> None: super().__init__(locals=repl_locals) + self.code_to_run = None self.compile.compiler.flags |= ast.PyCF_ALLOW_TOP_LEVEL_AWAIT + readline = sys.modules.get("readline") + if readline is not None: + self.readline = readline + if hasattr(readline, "__file__"): + self.rl = ctypes.CDLL(readline.__file__) + else: + self.rl = ctypes.pythonapi + if hasattr(self.rl, "rl_catch_signals"): + ctypes.c_int.in_dll(self.rl, "rl_catch_signals").value = 0 + self.rlcallbacktype = ctypes.CFUNCTYPE(None, ctypes.c_char_p) + self.rl.rl_callback_handler_install.argtypes = [ctypes.c_char_p, self.rlcallbacktype] + else: + self.rl = None + self.linebuffer = "" + def runcode(self, code: types.CodeType) -> None: + self.code_to_run = code + + async def actually_run_code(self) -> None: # https://github.com/python/typeshed/issues/13768 - func = types.FunctionType(code, self.locals) # type: ignore[arg-type] + func = types.FunctionType(self.code_to_run, self.locals) # type: ignore[arg-type] + self.code_to_run = None if inspect.iscoroutinefunction(func): - result = trio.from_thread.run(outcome.acapture, func) + result = await outcome.acapture(func) else: - result = trio.from_thread.run_sync(outcome.capture, func) + result = outcome.capture(func) if isinstance(result, outcome.Error): # If it is SystemExit, quit the repl. Otherwise, print the traceback. # If there is a SystemExit inside a BaseExceptionGroup, it probably isn't @@ -50,6 +72,78 @@ def runcode(self, code: types.CodeType) -> None: # This means that overriding self.write also does nothing to tbs. sys.excepthook(sys.last_type, sys.last_value, sys.last_traceback) + async def ainteract(self, banner): + try: + sys.ps1 + except AttributeError: + sys.ps1 = ">>> " + try: + sys.ps2 + except AttributeError: + sys.ps2 = "... " + + self.write("%s\n" % str(banner)) + more = 0 + + while True: + try: + if more: + prompt = sys.ps2 + else: + prompt = sys.ps1 + try: + line = await self.async_input(prompt) + except EOFError: + self.write("\n") + break + else: + more = self.push(line) + if more == 0: + await self.actually_run_code() + except KeyboardInterrupt: + self.write("\nKeyboardInterrupt\n") + self.resetbuffer() + more = 0 + + async def async_input(self, prompt=""): + if self.rl: + line = b"" + + @self.rlcallbacktype + def callback(text): + nonlocal line + line = text + + try: + self.rl.rl_callback_handler_install(prompt.encode(), callback) + while line == b"": + await trio.lowlevel.wait_readable(0) + self.rl.rl_callback_read_char() + except KeyboardInterrupt: + self.rl.rl_free_line_state() + raise + finally: + self.rl.rl_callback_handler_remove() + if line is None: + raise EOFError + self.readline.add_history(line.decode()) + return line.decode() + else: + line = "" + print(prompt, file=sys.stderr, end="") + sys.stderr.flush() + while True: + await trio.lowlevel.wait_readable(0) + new = os.read(0, 1024).decode() + if new == "": + raise EOFError + self.linebuffer += new + line, nl, buffer = self.linebuffer.partition("\n") + if nl: + self.linebuffer = buffer + return line + return line + async def run_repl(console: TrioInteractiveConsole) -> None: banner = ( @@ -60,7 +154,7 @@ async def run_repl(console: TrioInteractiveConsole) -> None: f'{getattr(sys, "ps1", ">>> ")}import trio' ) try: - await trio.to_thread.run_sync(console.interact, banner) + await console.ainteract(banner) finally: warnings.filterwarnings( "ignore", @@ -86,3 +180,7 @@ def main(original_locals: dict[str, object]) -> None: console = TrioInteractiveConsole(repl_locals) trio.run(run_repl, console) + + +if __name__ == "__main__": + main(locals()) From 0e25830698b6801b3098fc9ca883cb10e8955307 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 27 Jul 2025 06:13:07 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/trio/_repl.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/trio/_repl.py b/src/trio/_repl.py index c00ad031a9..bbd6f7dc52 100644 --- a/src/trio/_repl.py +++ b/src/trio/_repl.py @@ -34,7 +34,10 @@ def __init__(self, repl_locals: dict[str, object] | None = None) -> None: if hasattr(self.rl, "rl_catch_signals"): ctypes.c_int.in_dll(self.rl, "rl_catch_signals").value = 0 self.rlcallbacktype = ctypes.CFUNCTYPE(None, ctypes.c_char_p) - self.rl.rl_callback_handler_install.argtypes = [ctypes.c_char_p, self.rlcallbacktype] + self.rl.rl_callback_handler_install.argtypes = [ + ctypes.c_char_p, + self.rlcallbacktype, + ] else: self.rl = None self.linebuffer = ""