From e5c7216f376a06d2c931daf999e2980e494e747e Mon Sep 17 00:00:00 2001 From: Milan Oberkirch Date: Tue, 16 Jul 2024 00:24:18 +0200 Subject: [PATCH] gh-121790: Fix interactive console initialization (#121793) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ɓukasz Langa --- Lib/_pyrepl/main.py | 6 ++- Lib/_pyrepl/readline.py | 8 ++-- Lib/_pyrepl/simple_interact.py | 14 ++----- Lib/asyncio/__main__.py | 27 +++++--------- Lib/site.py | 5 +-- Lib/test/test_repl.py | 67 ++++++++++++++++++++++++++++++++-- 6 files changed, 85 insertions(+), 42 deletions(-) diff --git a/Lib/_pyrepl/main.py b/Lib/_pyrepl/main.py index 946bf339220865..8d6e07d36b52ca 100644 --- a/Lib/_pyrepl/main.py +++ b/Lib/_pyrepl/main.py @@ -23,7 +23,7 @@ def interactive_console(mainmodule=None, quiet=False, pythonstartup=False): if not CAN_USE_PYREPL: - if not os.environ.get('PYTHON_BASIC_REPL', None) and FAIL_REASON: + if not os.getenv('PYTHON_BASIC_REPL') and FAIL_REASON: from .trace import trace trace(FAIL_REASON) print(FAIL_REASON, file=sys.stderr) @@ -51,5 +51,7 @@ def interactive_console(mainmodule=None, quiet=False, pythonstartup=False): if not hasattr(sys, "ps2"): sys.ps2 = "... " + from .console import InteractiveColoredConsole from .simple_interact import run_multiline_interactive_console - run_multiline_interactive_console(namespace) + console = InteractiveColoredConsole(namespace, filename="") + run_multiline_interactive_console(console) diff --git a/Lib/_pyrepl/readline.py b/Lib/_pyrepl/readline.py index 28f592d80b1b03..3d94f91753587e 100644 --- a/Lib/_pyrepl/readline.py +++ b/Lib/_pyrepl/readline.py @@ -58,7 +58,7 @@ TYPE_CHECKING = False if TYPE_CHECKING: - from typing import Any + from typing import Any, Mapping MoreLinesCallable = Callable[[str], bool] @@ -559,7 +559,7 @@ def stub(*args: object, **kwds: object) -> None: # ____________________________________________________________ -def _setup(namespace: dict[str, Any]) -> None: +def _setup(namespace: Mapping[str, Any]) -> None: global raw_input if raw_input is not None: return # don't run _setup twice @@ -575,7 +575,9 @@ def _setup(namespace: dict[str, Any]) -> None: _wrapper.f_in = f_in _wrapper.f_out = f_out - # set up namespace in rlcompleter + # set up namespace in rlcompleter, which requires it to be a bona fide dict + if not isinstance(namespace, dict): + namespace = dict(namespace) _wrapper.config.readline_completer = RLCompleter(namespace).complete # this is not really what readline.c does. Better than nothing I guess diff --git a/Lib/_pyrepl/simple_interact.py b/Lib/_pyrepl/simple_interact.py index bc16c1f6a23159..5af0798e670fdc 100644 --- a/Lib/_pyrepl/simple_interact.py +++ b/Lib/_pyrepl/simple_interact.py @@ -27,12 +27,9 @@ import _sitebuiltins import linecache -import builtins import sys import code -from types import ModuleType -from .console import InteractiveColoredConsole from .readline import _get_reader, multiline_input TYPE_CHECKING = False @@ -82,17 +79,12 @@ def _clear_screen(): def run_multiline_interactive_console( - namespace: dict[str, Any], + console: code.InteractiveConsole, + *, future_flags: int = 0, - console: code.InteractiveConsole | None = None, ) -> None: from .readline import _setup - _setup(namespace) - - if console is None: - console = InteractiveColoredConsole( - namespace, filename="" - ) + _setup(console.locals) if future_flags: console.compile.compiler.flags |= future_flags diff --git a/Lib/asyncio/__main__.py b/Lib/asyncio/__main__.py index 95147171c2b79d..8b5a4b8f282a92 100644 --- a/Lib/asyncio/__main__.py +++ b/Lib/asyncio/__main__.py @@ -97,30 +97,16 @@ def run(self): exec(startup_code, console.locals) ps1 = getattr(sys, "ps1", ">>> ") - if can_colorize(): + if can_colorize() and CAN_USE_PYREPL: ps1 = f"{ANSIColors.BOLD_MAGENTA}{ps1}{ANSIColors.RESET}" console.write(f"{ps1}import asyncio\n") - try: - import errno - if os.getenv("PYTHON_BASIC_REPL"): - raise RuntimeError("user environment requested basic REPL") - if not os.isatty(sys.stdin.fileno()): - return_code = errno.ENOTTY - raise OSError(return_code, "tty required", "stdin") - - # This import will fail on operating systems with no termios. + if CAN_USE_PYREPL: from _pyrepl.simple_interact import ( - check, run_multiline_interactive_console, ) - if err := check(): - raise RuntimeError(err) - except Exception as e: - console.interact(banner="", exitmsg="") - else: try: - run_multiline_interactive_console(console=console) + run_multiline_interactive_console(console) except SystemExit: # expected via the `exit` and `quit` commands pass @@ -129,6 +115,8 @@ def run(self): console.showtraceback() console.write("Internal error, ") return_code = 1 + else: + console.interact(banner="", exitmsg="") finally: warnings.filterwarnings( 'ignore', @@ -139,7 +127,10 @@ def run(self): if __name__ == '__main__': - CAN_USE_PYREPL = True + if os.getenv('PYTHON_BASIC_REPL'): + CAN_USE_PYREPL = False + else: + from _pyrepl.main import CAN_USE_PYREPL return_code = 0 loop = asyncio.new_event_loop() diff --git a/Lib/site.py b/Lib/site.py index 444b9341122e9e..cafd3ab70b2cac 100644 --- a/Lib/site.py +++ b/Lib/site.py @@ -517,10 +517,7 @@ def register_readline(): pass if readline.get_current_history_length() == 0: - try: - from _pyrepl.main import CAN_USE_PYREPL - except ImportError: - CAN_USE_PYREPL = False + from _pyrepl.main import CAN_USE_PYREPL # If no history was loaded, default to .python_history, # or PYTHON_HISTORY. # The guard is necessary to avoid doubling history size at diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index 0e73fb6249c787..0b938623856e4f 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -1,15 +1,27 @@ """Test the interactive interpreter.""" import os +import select import subprocess import sys import unittest from textwrap import dedent from test import support -from test.support import cpython_only, has_subprocess_support, SuppressCrashReport -from test.support.script_helper import assert_python_failure, kill_python, assert_python_ok +from test.support import ( + cpython_only, + has_subprocess_support, + os_helper, + SuppressCrashReport, + SHORT_TIMEOUT, +) +from test.support.script_helper import kill_python from test.support.import_helper import import_module +try: + import pty +except ImportError: + pty = None + if not has_subprocess_support: raise unittest.SkipTest("test module requires subprocess") @@ -195,9 +207,56 @@ def bar(x): expected = "(30, None, [\'def foo(x):\\n\', \' return x + 1\\n\', \'\\n\'], \'\')" self.assertIn(expected, output, expected) - def test_asyncio_repl_no_tty_fails(self): - assert assert_python_failure("-m", "asyncio") + def test_asyncio_repl_reaches_python_startup_script(self): + with os_helper.temp_dir() as tmpdir: + script = os.path.join(tmpdir, "pythonstartup.py") + with open(script, "w") as f: + f.write("print('pythonstartup done!')" + os.linesep) + f.write("exit(0)" + os.linesep) + + env = os.environ.copy() + env["PYTHONSTARTUP"] = script + subprocess.check_call( + [sys.executable, "-m", "asyncio"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=env, + timeout=SHORT_TIMEOUT, + ) + + @unittest.skipUnless(pty, "requires pty") + def test_asyncio_repl_is_ok(self): + m, s = pty.openpty() + cmd = [sys.executable, "-m", "asyncio"] + proc = subprocess.Popen( + cmd, + stdin=s, + stdout=s, + stderr=s, + text=True, + close_fds=True, + env=os.environ, + ) + os.close(s) + os.write(m, b"await asyncio.sleep(0)\n") + os.write(m, b"exit()\n") + output = [] + while select.select([m], [], [], SHORT_TIMEOUT)[0]: + try: + data = os.read(m, 1024).decode("utf-8") + if not data: + break + except OSError: + break + output.append(data) + os.close(m) + try: + exit_code = proc.wait(timeout=SHORT_TIMEOUT) + except subprocess.TimeoutExpired: + proc.kill() + exit_code = proc.wait() + self.assertEqual(exit_code, 0) class TestInteractiveModeSyntaxErrors(unittest.TestCase):