diff --git a/Lib/asyncio/__main__.py b/Lib/asyncio/__main__.py index 44667efc522556..0cef7b891988ce 100644 --- a/Lib/asyncio/__main__.py +++ b/Lib/asyncio/__main__.py @@ -102,7 +102,13 @@ def run(self): import tokenize with tokenize.open(startup_path) as f: startup_code = compile(f.read(), startup_path, "exec") - exec(startup_code, console.locals) + try: + exec(startup_code, console.locals) + # TODO: Revisit in GH-143023 + except SystemExit: + raise + except BaseException: + console.showtraceback() ps1 = getattr(sys, "ps1", ">>> ") if CAN_USE_PYREPL: diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index 6cdb1ca65c6aed..ddc79a122fd523 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -5,6 +5,7 @@ import subprocess import sys import unittest +from contextlib import contextmanager from functools import partial from textwrap import dedent from test import support @@ -67,6 +68,19 @@ def spawn_repl(*args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, custom=F spawn_asyncio_repl = partial(spawn_repl, "-m", "asyncio", custom=True) +@contextmanager +def new_pythonstartup_env(*, code: str, histfile: str = ".pythonhist"): + """Create environment variables for a PYTHONSTARTUP script in a temporary directory.""" + with os_helper.temp_dir() as tmpdir: + filename = os.path.join(tmpdir, "pythonstartup.py") + with open(filename, "w") as f: + f.write(code) + yield { + "PYTHONSTARTUP": filename, + "PYTHON_HISTORY": os.path.join(tmpdir, histfile) + } + + def run_on_interactive_mode(source): """Spawn a new Python interpreter, pass the given input source code from the stdin and return the @@ -260,8 +274,6 @@ def make_repl(env): """) % script self.assertIn(expected, output) - - def test_runsource_show_syntax_error_location(self): user_input = dedent("""def f(x, x): ... """) @@ -356,6 +368,45 @@ def test_asyncio_repl_is_ok(self): self.assertEqual(exit_code, 0, "".join(output)) + def test_pythonstartup_success(self): + # errors based on https://github.com/python/cpython/issues/137576 + # case 1: error in user input, but PYTHONSTARTUP is fine + startup_code = "print('notice from pythonstartup')" + startup_env = self.enterContext(new_pythonstartup_env(code=startup_code)) + + p = spawn_repl("-q", env=os.environ | startup_env, isolated=False) + p.stdin.write("1/0") + output = kill_python(p) + self.assertStartsWith(output, 'notice from pythonstartup') + expected = dedent("""\ + Traceback (most recent call last): + File "", line 1, in + 1/0 + ~^~ + ZeroDivisionError: division by zero + """) + self.assertIn(expected, output) + + def test_pythonstartup_failure(self): + # case 2: error in PYTHONSTARTUP triggered by user input + startup_code = "def foo():\n 1/0\n" + startup_env = self.enterContext(new_pythonstartup_env(code=startup_code)) + + p = spawn_repl("-q", env=os.environ | startup_env, isolated=False) + p.stdin.write("foo()") + output = kill_python(p) + expected = dedent(f"""\ + Traceback (most recent call last): + File "", line 1, in + foo() + ~~~^^ + File "{startup_env['PYTHONSTARTUP']}", line 2, in foo + 1/0 + ~^~ + ZeroDivisionError: division by zero + """) + self.assertIn(expected, output) + @support.force_not_colorized_test_class class TestInteractiveModeSyntaxErrors(unittest.TestCase): @@ -376,6 +427,7 @@ def f(): self.assertEqual(traceback_lines, expected_lines) +@support.force_not_colorized_test_class class TestAsyncioREPL(unittest.TestCase): def test_multiple_statements_fail_early(self): user_input = "1 / 0; print(f'afterwards: {1+1}')" @@ -426,6 +478,50 @@ def test_quiet_mode(self): self.assertEqual(p.returncode, 0) self.assertEqual(output[:3], ">>>") + def test_pythonstartup_success(self): + startup_code = dedent("print('notice from pythonstartup in asyncio repl')") + startup_env = self.enterContext( + new_pythonstartup_env(code=startup_code, histfile=".asyncio_history")) + + p = spawn_repl( + "-qm", "asyncio", + env=os.environ | startup_env, + isolated=False, + custom=True) + p.stdin.write("1/0") + output = kill_python(p) + self.assertStartsWith(output, 'notice from pythonstartup in asyncio repl') + + expected = dedent("""\ + File "", line 1, in + 1/0 + ~^~ + ZeroDivisionError: division by zero + """) + self.assertIn(expected, output) + + def test_pythonstartup_failure(self): + startup_code = "def foo():\n 1/0\n" + startup_env = self.enterContext( + new_pythonstartup_env(code=startup_code, histfile=".asyncio_history")) + + p = spawn_repl( + "-qm", "asyncio", + env=os.environ | startup_env, + isolated=False, + custom=True) + p.stdin.write("foo()") + output = kill_python(p) + expected = dedent(f"""\ + File "", line 1, in + foo() + ~~~^^ + File "{startup_env['PYTHONSTARTUP']}", line 2, in foo + 1/0 + ~^~ + ZeroDivisionError: division by zero + """) + self.assertIn(expected, output) if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/Library/2025-10-18-12-13-39.gh-issue-140287.49iU-4.rst b/Misc/NEWS.d/next/Library/2025-10-18-12-13-39.gh-issue-140287.49iU-4.rst new file mode 100644 index 00000000000000..a8d89ca599933c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-10-18-12-13-39.gh-issue-140287.49iU-4.rst @@ -0,0 +1,5 @@ +The :mod:`asyncio` REPL now properly handles exceptions in :envvar:`PYTHONSTARTUP` scripts. +Previously, any startup exception could prevent the REPL from starting or even cause +a fatal error. + +Patch by Bartosz Sławecki in :gh:`140287`.