From faa22f483021f06e887e35199a629cd784be2037 Mon Sep 17 00:00:00 2001 From: bswck Date: Sat, 18 Oct 2025 12:07:27 +0200 Subject: [PATCH 01/28] Handle `PYTHONSTARTUP` script exceptions --- Lib/asyncio/__main__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Lib/asyncio/__main__.py b/Lib/asyncio/__main__.py index 10bfca3cf96b3e..cfb8b44c294b2d 100644 --- a/Lib/asyncio/__main__.py +++ b/Lib/asyncio/__main__.py @@ -101,7 +101,12 @@ 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) + except SystemExit: + raise + except BaseException: + sys.excepthook(*sys.exc_info()) ps1 = getattr(sys, "ps1", ">>> ") if CAN_USE_PYREPL: From 5b3ad2c55ae07bb2ff66322f09eddcc9fac3e91c Mon Sep 17 00:00:00 2001 From: bswck Date: Sat, 18 Oct 2025 12:13:53 +0200 Subject: [PATCH 02/28] Add blurb --- .../next/Library/2025-10-18-12-13-39.gh-issue-140287.49iU-4.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-10-18-12-13-39.gh-issue-140287.49iU-4.rst 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..82bb2e7da41032 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-10-18-12-13-39.gh-issue-140287.49iU-4.rst @@ -0,0 +1,2 @@ +The asyncio REPL now properly handles exceptions in ``PYTHONSTARTUP`` +scripts. Patch by Bartosz Sławecki in :gh:`140287`. From 00edac497a68ed538bf9468a8ffab061fe85156f Mon Sep 17 00:00:00 2001 From: bswck Date: Sat, 18 Oct 2025 12:17:57 +0200 Subject: [PATCH 03/28] Use `console.showtraceback()` instead of `sys.excepthook()` --- Lib/asyncio/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/asyncio/__main__.py b/Lib/asyncio/__main__.py index cfb8b44c294b2d..d819d65371e583 100644 --- a/Lib/asyncio/__main__.py +++ b/Lib/asyncio/__main__.py @@ -106,7 +106,7 @@ def run(self): except SystemExit: raise except BaseException: - sys.excepthook(*sys.exc_info()) + console.showtraceback() ps1 = getattr(sys, "ps1", ">>> ") if CAN_USE_PYREPL: From e396622bee1f66a3bc62cc2c9307cfab714c07e7 Mon Sep 17 00:00:00 2001 From: bswck Date: Sat, 18 Oct 2025 14:06:38 +0200 Subject: [PATCH 04/28] Properly run asyncio REPL in REPL tests --- Lib/test/test_repl.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index 54e69277282c30..e41c6e395f6689 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -5,6 +5,7 @@ import subprocess import sys import unittest +from functools import partial from textwrap import dedent from test import support from test.support import ( @@ -27,7 +28,7 @@ raise unittest.SkipTest("test module requires subprocess") -def spawn_repl(*args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, **kw): +def spawn_repl(*args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, custom=False, **kw): """Run the Python REPL with the given arguments. kw is extra keyword args to pass to subprocess.Popen. Returns a Popen @@ -41,7 +42,11 @@ def spawn_repl(*args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, **kw): # path may be used by PyConfig_Get("module_search_paths") to build the # default module search path. stdin_fname = os.path.join(os.path.dirname(sys.executable), "") - cmd_line = [stdin_fname, '-I', '-i'] + cmd_line = [stdin_fname, '-I'] + if not custom: + # Don't re-run the built-in REPL from interactive mode + # if we're testing a custom REPL (such as the asyncio REPL). + cmd_line.append('-i') cmd_line.extend(args) # Set TERM=vt100, for the rationale see the comments in spawn_python() of @@ -55,6 +60,10 @@ def spawn_repl(*args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, **kw): stdout=stdout, stderr=stderr, **kw) + +spawn_asyncio_repl = partial(spawn_repl, "-m", "asyncio", custom=True) + + def run_on_interactive_mode(source): """Spawn a new Python interpreter, pass the given input source code from the stdin and return the @@ -359,7 +368,7 @@ def f(): class TestAsyncioREPL(unittest.TestCase): def test_multiple_statements_fail_early(self): user_input = "1 / 0; print(f'afterwards: {1+1}')" - p = spawn_repl("-m", "asyncio") + p = spawn_asyncio_repl() p.stdin.write(user_input) output = kill_python(p) self.assertIn("ZeroDivisionError", output) @@ -371,7 +380,7 @@ def test_toplevel_contextvars_sync(self): var = ContextVar("var", default="failed") var.set("ok") """) - p = spawn_repl("-m", "asyncio") + p = spawn_asyncio_repl() p.stdin.write(user_input) user_input2 = dedent(""" print(f"toplevel contextvar test: {var.get()}") @@ -387,7 +396,7 @@ def test_toplevel_contextvars_async(self): from contextvars import ContextVar var = ContextVar('var', default='failed') """) - p = spawn_repl("-m", "asyncio") + p = spawn_asyncio_repl() p.stdin.write(user_input) user_input2 = "async def set_var(): var.set('ok')\n" p.stdin.write(user_input2) From 407907453bbbe058bfea51e6d74fd1ede107fd26 Mon Sep 17 00:00:00 2001 From: bswck Date: Sat, 18 Oct 2025 14:09:23 +0200 Subject: [PATCH 05/28] Move comment to a better place --- Lib/test/test_repl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index e41c6e395f6689..042aa84b35dcf8 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -43,9 +43,9 @@ def spawn_repl(*args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, custom=F # default module search path. stdin_fname = os.path.join(os.path.dirname(sys.executable), "") cmd_line = [stdin_fname, '-I'] + # Don't re-run the built-in REPL from interactive mode + # if we're testing a custom REPL (such as the asyncio REPL). if not custom: - # Don't re-run the built-in REPL from interactive mode - # if we're testing a custom REPL (such as the asyncio REPL). cmd_line.append('-i') cmd_line.extend(args) From 0440d0ebcbca9d4e671c5a621d0ef3ae2b9b63e8 Mon Sep 17 00:00:00 2001 From: bswck Date: Thu, 23 Oct 2025 16:55:20 +0200 Subject: [PATCH 06/28] Add tests --- Lib/test/test_repl.py | 107 ++++++++++++++++++++---------------------- 1 file changed, 51 insertions(+), 56 deletions(-) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index 54e69277282c30..6eb82f4210d195 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -188,65 +188,60 @@ def foo(x): ] self.assertEqual(traceback_lines, expected_lines) - def test_pythonstartup_error_reporting(self): + def test_pythonstartup_success(self): # errors based on https://github.com/python/cpython/issues/137576 - - def make_repl(env): - return subprocess.Popen( - [os.path.join(os.path.dirname(sys.executable), ''), "-i"], - executable=sys.executable, - text=True, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - env=env, - ) - # case 1: error in user input, but PYTHONSTARTUP is fine - with os_helper.temp_dir() as tmpdir: - script = os.path.join(tmpdir, "pythonstartup.py") - with open(script, "w") as f: - f.write("print('from pythonstartup')" + os.linesep) - - env = os.environ.copy() - env['PYTHONSTARTUP'] = script - env["PYTHON_HISTORY"] = os.path.join(tmpdir, ".pythonhist") - p = make_repl(env) - p.stdin.write("1/0") - output = kill_python(p) - expected = dedent(""" - Traceback (most recent call last): - File "", line 1, in - 1/0 - ~^~ - ZeroDivisionError: division by zero - """) - self.assertIn("from pythonstartup", output) - self.assertIn(expected, output) - + for repl_name, repl in ("REPL", spawn_repl), ("asyncio REPL", spawn_asyncio_repl): + with os_helper.temp_dir() as tmpdir: + script = os.path.join(tmpdir, "pythonstartup.py") + with open(script, "w") as f: + f.write("print('from pythonstartup')" + os.linesep) + + env = os.environ.copy() + env['PYTHONSTARTUP'] = script + env["PYTHON_HISTORY"] = os.path.join(tmpdir, ".pythonhist") + p = repl(env=env) + p.stdin.write("1/0") + output = kill_python(p) + + with self.subTest(repl_name): + self.assertIn("Traceback (most recent call last):", output) + expected = dedent(""" + File "", line 1, in + 1/0 + ~^~ + ZeroDivisionError: division by zero + """) + self.assertIn("from pythonstartup", output) + self.assertIn(expected, output) + + def test_pythonstartup_failure(self): # case 2: error in PYTHONSTARTUP triggered by user input - with os_helper.temp_dir() as tmpdir: - script = os.path.join(tmpdir, "pythonstartup.py") - with open(script, "w") as f: - f.write("def foo():\n 1/0\n") - - env = os.environ.copy() - env['PYTHONSTARTUP'] = script - env["PYTHON_HISTORY"] = os.path.join(tmpdir, ".pythonhist") - p = make_repl(env) - p.stdin.write('foo()') - output = kill_python(p) - expected = dedent(""" - Traceback (most recent call last): - File "", line 1, in - foo() - ~~~^^ - File "%s", line 2, in foo - 1/0 - ~^~ - ZeroDivisionError: division by zero - """) % script - self.assertIn(expected, output) + for repl_name, repl in ("REPL", spawn_repl), ("asyncio REPL", spawn_asyncio_repl): + with os_helper.temp_dir() as tmpdir: + script = os.path.join(tmpdir, "pythonstartup.py") + with open(script, "w") as f: + f.write("def foo():\n 1/0\n") + + env = os.environ.copy() + env['PYTHONSTARTUP'] = script + env["PYTHON_HISTORY"] = os.path.join(tmpdir, ".pythonhist") + p = repl(env=env) + p.stdin.write('foo()') + output = kill_python(p) + + with self.subTest(repl_name): + self.assertIn("Traceback (most recent call last):", output) + expected = dedent(""" + File "", line 1, in + foo() + ~~~^^ + File "%s", line 2, in foo + 1/0 + ~^~ + ZeroDivisionError: division by zero + """) % script + self.assertIn(expected, output) From d158dbf272d7ac0ebc210db897409ad614e13053 Mon Sep 17 00:00:00 2001 From: bswck Date: Mon, 27 Oct 2025 02:03:31 +0100 Subject: [PATCH 07/28] Improve test structure --- Lib/test/test_repl.py | 153 +++++++++++++++++++++--------------------- 1 file changed, 76 insertions(+), 77 deletions(-) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index 6dbf28605ce012..6a51eacd67e068 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 @@ -28,7 +29,7 @@ raise unittest.SkipTest("test module requires subprocess") -def spawn_repl(*args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, custom=False, **kw): +def spawn_repl(*args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, custom=False, isolated=True, **kw): """Run the Python REPL with the given arguments. kw is extra keyword args to pass to subprocess.Popen. Returns a Popen @@ -42,7 +43,11 @@ def spawn_repl(*args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, custom=F # path may be used by PyConfig_Get("module_search_paths") to build the # default module search path. stdin_fname = os.path.join(os.path.dirname(sys.executable), "") - cmd_line = [stdin_fname, '-I'] + cmd_line = [stdin_fname] + # Isolated mode implies -E, -P and -s, purifies sys.path and ignores PYTHON* + # variables. + if isolated: + cmd_line.append('-I') # Don't re-run the built-in REPL from interactive mode # if we're testing a custom REPL (such as the asyncio REPL). if not custom: @@ -197,63 +202,6 @@ def foo(x): ] self.assertEqual(traceback_lines, expected_lines) - 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 - for repl_name, repl in ("REPL", spawn_repl), ("asyncio REPL", spawn_asyncio_repl): - with os_helper.temp_dir() as tmpdir: - script = os.path.join(tmpdir, "pythonstartup.py") - with open(script, "w") as f: - f.write("print('from pythonstartup')" + os.linesep) - - env = os.environ.copy() - env['PYTHONSTARTUP'] = script - env["PYTHON_HISTORY"] = os.path.join(tmpdir, ".pythonhist") - p = repl(env=env) - p.stdin.write("1/0") - output = kill_python(p) - - with self.subTest(repl_name): - self.assertIn("Traceback (most recent call last):", output) - expected = dedent(""" - File "", line 1, in - 1/0 - ~^~ - ZeroDivisionError: division by zero - """) - self.assertIn("from pythonstartup", output) - self.assertIn(expected, output) - - def test_pythonstartup_failure(self): - # case 2: error in PYTHONSTARTUP triggered by user input - for repl_name, repl in ("REPL", spawn_repl), ("asyncio REPL", spawn_asyncio_repl): - with os_helper.temp_dir() as tmpdir: - script = os.path.join(tmpdir, "pythonstartup.py") - with open(script, "w") as f: - f.write("def foo():\n 1/0\n") - - env = os.environ.copy() - env['PYTHONSTARTUP'] = script - env["PYTHON_HISTORY"] = os.path.join(tmpdir, ".pythonhist") - p = repl(env=env) - p.stdin.write('foo()') - output = kill_python(p) - - with self.subTest(repl_name): - self.assertIn("Traceback (most recent call last):", output) - expected = dedent(""" - File "", line 1, in - foo() - ~~~^^ - File "%s", line 2, in foo - 1/0 - ~^~ - ZeroDivisionError: division by zero - """) % script - self.assertIn(expected, output) - - - def test_runsource_show_syntax_error_location(self): user_input = dedent("""def f(x, x): ... """) @@ -287,24 +235,6 @@ def bar(x): expected = "(30, None, [\'def foo(x):\\n\', \' return x + 1\\n\', \'\\n\'], \'\')" self.assertIn(expected, output, expected) - 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["PYTHON_HISTORY"] = os.path.join(tmpdir, ".asyncio_history") - 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() @@ -341,6 +271,75 @@ def test_asyncio_repl_is_ok(self): self.assertEqual(exit_code, 0, "".join(output)) +@contextmanager +def pythonstartup_env(*, script: str, histfile: str = ".pythonhist", env=None): + with os_helper.temp_dir() as tmpdir: + filename = os.path.join(tmpdir, "pythonstartup.py") + with open(filename, "w") as f: + f.write(os.linesep.join(script.splitlines())) + if env is None: + env = os.environ.copy() + yield env | {"PYTHONSTARTUP": filename, "PYTHON_HISTORY": os.path.join(tmpdir, histfile)} + + +class TestPythonStartup(unittest.TestCase): + REPLS = [ + ("REPL", spawn_repl, ".pythonhist"), + ("asyncio REPL", spawn_asyncio_repl, ".asyncio_history"), + ] + + 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 + for repl_name, repl_factory, histfile in self.REPLS: + with ( + self.subTest(repl_name), + pythonstartup_env(script="print('from pythonstartup')", histfile=histfile) as env + ): + p = repl_factory(env=env, isolated=False) + p.stdin.write("1/0") + output = kill_python(p) + + for chunk in ( + "from pythonstartup", + "Traceback (most recent call last):", + """\ + File "", line 1, in + 1/0 + ~^~ + ZeroDivisionError: division by zero + """ + ): + self.assertIn(dedent(chunk), output) + + def test_pythonstartup_failure(self): + # case 2: error in PYTHONSTARTUP triggered by user input + for repl_name, repl_factory, histfile in self.REPLS: + with ( + self.subTest(repl_name), + pythonstartup_env(script="def foo():\n 1/0\n", histfile=histfile) as env + ): + p = repl_factory(env=env, isolated=False) + p.stdin.write('foo()') + output = kill_python(p) + + for chunk in ( + "Traceback (most recent call last):", + """\ + File "", line 1, in + foo() + ~~~^^ + """, + f"""\ + File "{env['PYTHONSTARTUP']}", line 2, in foo + 1/0 + ~^~ + ZeroDivisionError: division by zero + """ + ): + self.assertIn(dedent(chunk), output) + + @support.force_not_colorized_test_class class TestInteractiveModeSyntaxErrors(unittest.TestCase): From 9355da7f36b2bc0063c5f0b2fff5527cc4f7d32e Mon Sep 17 00:00:00 2001 From: bswck Date: Mon, 27 Oct 2025 02:06:23 +0100 Subject: [PATCH 08/28] Use `SHORT_TIMEOUT` --- Lib/test/test_repl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index 6a51eacd67e068..ff99359e9d38cb 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -296,7 +296,7 @@ def test_pythonstartup_success(self): self.subTest(repl_name), pythonstartup_env(script="print('from pythonstartup')", histfile=histfile) as env ): - p = repl_factory(env=env, isolated=False) + p = repl_factory(env=env, isolated=False, timeout=SHORT_TIMEOUT) p.stdin.write("1/0") output = kill_python(p) @@ -319,7 +319,7 @@ def test_pythonstartup_failure(self): self.subTest(repl_name), pythonstartup_env(script="def foo():\n 1/0\n", histfile=histfile) as env ): - p = repl_factory(env=env, isolated=False) + p = repl_factory(env=env, isolated=False, timeout=SHORT_TIMEOUT) p.stdin.write('foo()') output = kill_python(p) From 3dc4cac7cbe6618e06d597afd4b625519865d730 Mon Sep 17 00:00:00 2001 From: bswck Date: Mon, 27 Oct 2025 02:10:11 +0100 Subject: [PATCH 09/28] Revert "Use `SHORT_TIMEOUT`" This reverts commit 9355da7f36b2bc0063c5f0b2fff5527cc4f7d32e. --- Lib/test/test_repl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index ff99359e9d38cb..6a51eacd67e068 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -296,7 +296,7 @@ def test_pythonstartup_success(self): self.subTest(repl_name), pythonstartup_env(script="print('from pythonstartup')", histfile=histfile) as env ): - p = repl_factory(env=env, isolated=False, timeout=SHORT_TIMEOUT) + p = repl_factory(env=env, isolated=False) p.stdin.write("1/0") output = kill_python(p) @@ -319,7 +319,7 @@ def test_pythonstartup_failure(self): self.subTest(repl_name), pythonstartup_env(script="def foo():\n 1/0\n", histfile=histfile) as env ): - p = repl_factory(env=env, isolated=False, timeout=SHORT_TIMEOUT) + p = repl_factory(env=env, isolated=False) p.stdin.write('foo()') output = kill_python(p) From c786584d99c54abc65dc508c413178e30f1e4d9a Mon Sep 17 00:00:00 2001 From: bswck Date: Mon, 27 Oct 2025 02:26:50 +0100 Subject: [PATCH 10/28] Force no colorization --- Lib/test/test_repl.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index 6a51eacd67e068..a769e5cb8d307a 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -282,6 +282,7 @@ def pythonstartup_env(*, script: str, histfile: str = ".pythonhist", env=None): yield env | {"PYTHONSTARTUP": filename, "PYTHON_HISTORY": os.path.join(tmpdir, histfile)} +@support.force_not_colorized_test_class class TestPythonStartup(unittest.TestCase): REPLS = [ ("REPL", spawn_repl, ".pythonhist"), From b76db6784b025fd31d263505cbf87401517202c4 Mon Sep 17 00:00:00 2001 From: bswck Date: Tue, 28 Oct 2025 17:59:38 +0100 Subject: [PATCH 11/28] Linecache doesn't matter and shouldn't break tests --- Lib/test/test_repl.py | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index a769e5cb8d307a..ba9f64e08a71a7 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -304,14 +304,10 @@ def test_pythonstartup_success(self): for chunk in ( "from pythonstartup", "Traceback (most recent call last):", - """\ - File "", line 1, in - 1/0 - ~^~ - ZeroDivisionError: division by zero - """ + 'File "", line 1, in ', + "ZeroDivisionError: division by zero", ): - self.assertIn(dedent(chunk), output) + self.assertIn(chunk, output) def test_pythonstartup_failure(self): # case 2: error in PYTHONSTARTUP triggered by user input @@ -326,19 +322,11 @@ def test_pythonstartup_failure(self): for chunk in ( "Traceback (most recent call last):", - """\ - File "", line 1, in - foo() - ~~~^^ - """, - f"""\ - File "{env['PYTHONSTARTUP']}", line 2, in foo - 1/0 - ~^~ - ZeroDivisionError: division by zero - """ + 'File "", line 1, in ', + f'File "{env['PYTHONSTARTUP']}", line 2, in foo', + "ZeroDivisionError: division by zero", ): - self.assertIn(dedent(chunk), output) + self.assertIn(chunk, output) @support.force_not_colorized_test_class From cad1748c94a6838262caf9d62a1421a36e058e08 Mon Sep 17 00:00:00 2001 From: bswck Date: Wed, 29 Oct 2025 01:53:25 +0100 Subject: [PATCH 12/28] Don't rely on line numbering on Windows... --- Lib/test/test_repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index ba9f64e08a71a7..c2f6bd5ca8f163 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -323,7 +323,7 @@ def test_pythonstartup_failure(self): for chunk in ( "Traceback (most recent call last):", 'File "", line 1, in ', - f'File "{env['PYTHONSTARTUP']}", line 2, in foo', + f'File "{env['PYTHONSTARTUP']}", line ', "ZeroDivisionError: division by zero", ): self.assertIn(chunk, output) From d114ed5660041cf5e67e530ba678450f96cee7aa Mon Sep 17 00:00:00 2001 From: bswck Date: Wed, 29 Oct 2025 01:53:51 +0100 Subject: [PATCH 13/28] Different names --- Lib/test/test_repl.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index c2f6bd5ca8f163..e5b3a80badbf86 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -301,13 +301,13 @@ def test_pythonstartup_success(self): p.stdin.write("1/0") output = kill_python(p) - for chunk in ( + for expected in ( "from pythonstartup", "Traceback (most recent call last):", 'File "", line 1, in ', "ZeroDivisionError: division by zero", ): - self.assertIn(chunk, output) + self.assertIn(expected, output) def test_pythonstartup_failure(self): # case 2: error in PYTHONSTARTUP triggered by user input @@ -320,13 +320,13 @@ def test_pythonstartup_failure(self): p.stdin.write('foo()') output = kill_python(p) - for chunk in ( + for expected in ( "Traceback (most recent call last):", 'File "", line 1, in ', f'File "{env['PYTHONSTARTUP']}", line ', "ZeroDivisionError: division by zero", ): - self.assertIn(chunk, output) + self.assertIn(expected, output) @support.force_not_colorized_test_class From e6e10adb7aa75ee424ba6d57d94fec50dbd6e695 Mon Sep 17 00:00:00 2001 From: bswck Date: Wed, 29 Oct 2025 18:20:54 +0100 Subject: [PATCH 14/28] Idiomatize and simplify --- Lib/test/test_repl.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index e5b3a80badbf86..617564f56144b7 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -272,14 +272,12 @@ def test_asyncio_repl_is_ok(self): @contextmanager -def pythonstartup_env(*, script: str, histfile: str = ".pythonhist", env=None): +def new_startup_env(*, code: str, histfile: str = ".pythonhist"): with os_helper.temp_dir() as tmpdir: filename = os.path.join(tmpdir, "pythonstartup.py") with open(filename, "w") as f: - f.write(os.linesep.join(script.splitlines())) - if env is None: - env = os.environ.copy() - yield env | {"PYTHONSTARTUP": filename, "PYTHON_HISTORY": os.path.join(tmpdir, histfile)} + f.write(os.linesep.join(code.splitlines())) + yield {"PYTHONSTARTUP": filename, "PYTHON_HISTORY": os.path.join(tmpdir, histfile)} @support.force_not_colorized_test_class @@ -292,12 +290,13 @@ class TestPythonStartup(unittest.TestCase): 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('from pythonstartup')" for repl_name, repl_factory, histfile in self.REPLS: with ( self.subTest(repl_name), - pythonstartup_env(script="print('from pythonstartup')", histfile=histfile) as env + new_startup_env(code=startup_code, histfile=histfile) as startup_env ): - p = repl_factory(env=env, isolated=False) + p = repl_factory(env=os.environ | startup_env, isolated=False) p.stdin.write("1/0") output = kill_python(p) @@ -311,19 +310,20 @@ def test_pythonstartup_success(self): def test_pythonstartup_failure(self): # case 2: error in PYTHONSTARTUP triggered by user input + startup_code = "def foo():\n 1/0\n" for repl_name, repl_factory, histfile in self.REPLS: with ( self.subTest(repl_name), - pythonstartup_env(script="def foo():\n 1/0\n", histfile=histfile) as env + new_startup_env(code=startup_code, histfile=histfile) as startup_env ): - p = repl_factory(env=env, isolated=False) + p = repl_factory(env=os.environ | startup_env, isolated=False) p.stdin.write('foo()') output = kill_python(p) for expected in ( "Traceback (most recent call last):", 'File "", line 1, in ', - f'File "{env['PYTHONSTARTUP']}", line ', + f'File "{startup_env['PYTHONSTARTUP']}", line ', "ZeroDivisionError: division by zero", ): self.assertIn(expected, output) From 149740ae5ca17d280f54f9c2d347bb256413ef1d Mon Sep 17 00:00:00 2001 From: bswck Date: Thu, 30 Oct 2025 08:54:12 +0100 Subject: [PATCH 15/28] Purifying `sys.path` is implied by `-P` --- Lib/test/test_repl.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index 617564f56144b7..1c9576d8511b83 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -44,8 +44,7 @@ def spawn_repl(*args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, custom=F # default module search path. stdin_fname = os.path.join(os.path.dirname(sys.executable), "") cmd_line = [stdin_fname] - # Isolated mode implies -E, -P and -s, purifies sys.path and ignores PYTHON* - # variables. + # Isolated mode implies -EPs and ignores PYTHON* variables. if isolated: cmd_line.append('-I') # Don't re-run the built-in REPL from interactive mode From a4c307eec48259ca895e391395a7b38ce4479d3e Mon Sep 17 00:00:00 2001 From: bswck Date: Sat, 8 Nov 2025 01:28:10 +0100 Subject: [PATCH 16/28] Separate tests for regular and asyncio REPL --- Lib/test/test_repl.py | 140 +++++++++++++++++++++++++++--------------- 1 file changed, 91 insertions(+), 49 deletions(-) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index 1c9576d8511b83..fdef21ee2f7952 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -234,6 +234,46 @@ def bar(x): expected = "(30, None, [\'def foo(x):\\n\', \' return x + 1\\n\', \'\\n\'], \'\')" self.assertIn(expected, output, expected) + 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')" + with new_startup_env(code=startup_code, histfile=".pythonhist") as startup_env: + # -q to suppress noise + p = spawn_repl("-q", env=os.environ | startup_env, isolated=False) + p.stdin.write("1/0") + output_lines = kill_python(p).splitlines() + traceback_lines = output_lines[2:-1] + expected_lines = [ + 'Traceback (most recent call last):', + ' File "", line 1, in ', + ' 1/0', + ' ~^~', + 'ZeroDivisionError: division by zero', + ] + self.assertEqual(output_lines[0], 'notice from pythonstartup') + self.assertEqual(traceback_lines, expected_lines) + + def test_pythonstartup_failure(self): + # case 2: error in PYTHONSTARTUP triggered by user input + startup_code = "def foo():\n 1/0\n" + with new_startup_env(code=startup_code, histfile=".asyncio_history") as startup_env: + # -q to suppress noise + p = spawn_repl("-q", env=os.environ | startup_env, isolated=False) + p.stdin.write("foo()") + traceback_lines = kill_python(p).splitlines()[1:-1] + expected_lines = [ + 'Traceback (most recent call last):', + ' File "", line 1, in ', + ' foo()', + ' ~~~^^', + f' File "{startup_env['PYTHONSTARTUP']}", line 2, in foo', + ' 1/0', + ' ~^~', + 'ZeroDivisionError: division by zero', + ] + self.assertEqual(traceback_lines, expected_lines) + @unittest.skipUnless(pty, "requires pty") def test_asyncio_repl_is_ok(self): m, s = pty.openpty() @@ -279,55 +319,6 @@ def new_startup_env(*, code: str, histfile: str = ".pythonhist"): yield {"PYTHONSTARTUP": filename, "PYTHON_HISTORY": os.path.join(tmpdir, histfile)} -@support.force_not_colorized_test_class -class TestPythonStartup(unittest.TestCase): - REPLS = [ - ("REPL", spawn_repl, ".pythonhist"), - ("asyncio REPL", spawn_asyncio_repl, ".asyncio_history"), - ] - - 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('from pythonstartup')" - for repl_name, repl_factory, histfile in self.REPLS: - with ( - self.subTest(repl_name), - new_startup_env(code=startup_code, histfile=histfile) as startup_env - ): - p = repl_factory(env=os.environ | startup_env, isolated=False) - p.stdin.write("1/0") - output = kill_python(p) - - for expected in ( - "from pythonstartup", - "Traceback (most recent call last):", - 'File "", line 1, in ', - "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" - for repl_name, repl_factory, histfile in self.REPLS: - with ( - self.subTest(repl_name), - new_startup_env(code=startup_code, histfile=histfile) as startup_env - ): - p = repl_factory(env=os.environ | startup_env, isolated=False) - p.stdin.write('foo()') - output = kill_python(p) - - for expected in ( - "Traceback (most recent call last):", - 'File "", line 1, in ', - f'File "{startup_env['PYTHONSTARTUP']}", line ', - "ZeroDivisionError: division by zero", - ): - self.assertIn(expected, output) - - @support.force_not_colorized_test_class class TestInteractiveModeSyntaxErrors(unittest.TestCase): @@ -347,6 +338,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}')" @@ -391,6 +383,56 @@ def test_toplevel_contextvars_async(self): expected = "toplevel contextvar test: ok" self.assertIn(expected, output, expected) + def test_pythonstartup_success(self): + startup_code = "import sys\nprint('notice from pythonstartup in asyncio repl', file=sys.stderr)" + with new_startup_env(code=startup_code, histfile=".asyncio_history") as startup_env: + p = spawn_asyncio_repl(env=os.environ | startup_env, stderr=subprocess.PIPE, isolated=False) + p.stdin.write("1/0") + kill_python(p) + output_lines = p.stderr.read().splitlines() + p.stderr.close() + self.assertEqual(output_lines[3], 'notice from pythonstartup in asyncio repl') + tb_start_lines = output_lines[4:6] + tb_final_lines = output_lines[13:] + expected_lines = [ + '>>> import asyncio', + 'Traceback (most recent call last):', + ' File "", line 1, in ', + ' 1/0', + ' ~^~', + 'ZeroDivisionError: division by zero', + '', + 'exiting asyncio REPL...', + ] + self.assertEqual(tb_start_lines + tb_final_lines, expected_lines) + self.assertEqual(tb_start_lines + tb_final_lines, expected_lines) + + def test_pythonstartup_failure(self): + startup_code = "def foo():\n 1/0\n" + with new_startup_env(code=startup_code, histfile=".asyncio_history") as startup_env: + p = spawn_asyncio_repl(env=os.environ | startup_env, stderr=subprocess.PIPE, isolated=False) + p.stdin.write("foo()") + kill_python(p) + output = p.stderr.read() + p.stderr.close() + tb_start_lines = output.splitlines()[3:5] + tb_final_lines = output.splitlines()[12:] + + expected_lines = [ + '>>> import asyncio', + 'Traceback (most recent call last):', + ' File "", line 1, in ', + ' foo()', + ' ~~~^^', + f' File "{startup_env['PYTHONSTARTUP']}", line 2, in foo', + ' 1/0', + ' ~^~', + 'ZeroDivisionError: division by zero', + '', + 'exiting asyncio REPL...', + ] + self.assertEqual(tb_start_lines + tb_final_lines, expected_lines) + if __name__ == "__main__": unittest.main() From a774da555644a953924e00d74d690843b3a31dea Mon Sep 17 00:00:00 2001 From: bswck Date: Sat, 8 Nov 2025 01:28:29 +0100 Subject: [PATCH 17/28] Remove duplicate assertion --- Lib/test/test_repl.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index fdef21ee2f7952..352cff7ced8b38 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -405,7 +405,6 @@ def test_pythonstartup_success(self): 'exiting asyncio REPL...', ] self.assertEqual(tb_start_lines + tb_final_lines, expected_lines) - self.assertEqual(tb_start_lines + tb_final_lines, expected_lines) def test_pythonstartup_failure(self): startup_code = "def foo():\n 1/0\n" From b3ed3d4b88acaf0a75df92170959157faafa10b4 Mon Sep 17 00:00:00 2001 From: bswck Date: Sat, 8 Nov 2025 01:30:53 +0100 Subject: [PATCH 18/28] Document `new_startup_env` --- Lib/test/test_repl.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index 352cff7ced8b38..e88491e57c537e 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -68,6 +68,16 @@ 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_startup_env(*, code: str, histfile: str = ".pythonhist"): + """Create temporary environment variables for a PYTHONSTARTUP script.""" + with os_helper.temp_dir() as tmpdir: + filename = os.path.join(tmpdir, "pythonstartup.py") + with open(filename, "w") as f: + f.write(os.linesep.join(code.splitlines())) + 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 @@ -310,15 +320,6 @@ def test_asyncio_repl_is_ok(self): self.assertEqual(exit_code, 0, "".join(output)) -@contextmanager -def new_startup_env(*, code: str, histfile: str = ".pythonhist"): - with os_helper.temp_dir() as tmpdir: - filename = os.path.join(tmpdir, "pythonstartup.py") - with open(filename, "w") as f: - f.write(os.linesep.join(code.splitlines())) - yield {"PYTHONSTARTUP": filename, "PYTHON_HISTORY": os.path.join(tmpdir, histfile)} - - @support.force_not_colorized_test_class class TestInteractiveModeSyntaxErrors(unittest.TestCase): From df6dfd8dd93c35657d0453501c6cdac19f006ef8 Mon Sep 17 00:00:00 2001 From: bswck Date: Sat, 8 Nov 2025 01:31:20 +0100 Subject: [PATCH 19/28] Fix `new_startup_env` docs --- Lib/test/test_repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index e88491e57c537e..4dd6c8b7711231 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -70,7 +70,7 @@ def spawn_repl(*args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, custom=F @contextmanager def new_startup_env(*, code: str, histfile: str = ".pythonhist"): - """Create temporary environment variables for a PYTHONSTARTUP script.""" + """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: From b004839d30354af5b73ecadeca071121c1eb8277 Mon Sep 17 00:00:00 2001 From: bswck Date: Sat, 8 Nov 2025 01:33:31 +0100 Subject: [PATCH 20/28] More meaningful line breaks --- Lib/test/test_repl.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index 4dd6c8b7711231..6a425a0f300d56 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -392,9 +392,12 @@ def test_pythonstartup_success(self): kill_python(p) output_lines = p.stderr.read().splitlines() p.stderr.close() + self.assertEqual(output_lines[3], 'notice from pythonstartup in asyncio repl') + tb_start_lines = output_lines[4:6] tb_final_lines = output_lines[13:] + expected_lines = [ '>>> import asyncio', 'Traceback (most recent call last):', @@ -405,6 +408,7 @@ def test_pythonstartup_success(self): '', 'exiting asyncio REPL...', ] + self.assertEqual(tb_start_lines + tb_final_lines, expected_lines) def test_pythonstartup_failure(self): @@ -415,6 +419,7 @@ def test_pythonstartup_failure(self): kill_python(p) output = p.stderr.read() p.stderr.close() + tb_start_lines = output.splitlines()[3:5] tb_final_lines = output.splitlines()[12:] @@ -431,6 +436,7 @@ def test_pythonstartup_failure(self): '', 'exiting asyncio REPL...', ] + self.assertEqual(tb_start_lines + tb_final_lines, expected_lines) From ce03ccef689e6fed09ddf8ed7f2d41ea1c4b38f0 Mon Sep 17 00:00:00 2001 From: bswck Date: Sat, 8 Nov 2025 01:39:34 +0100 Subject: [PATCH 21/28] Better variables --- Lib/test/test_repl.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index 6a425a0f300d56..1902a8eb5888ad 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -253,6 +253,9 @@ def test_pythonstartup_success(self): p = spawn_repl("-q", env=os.environ | startup_env, isolated=False) p.stdin.write("1/0") output_lines = kill_python(p).splitlines() + + self.assertEqual(output_lines[0], 'notice from pythonstartup') + traceback_lines = output_lines[2:-1] expected_lines = [ 'Traceback (most recent call last):', @@ -261,7 +264,6 @@ def test_pythonstartup_success(self): ' ~^~', 'ZeroDivisionError: division by zero', ] - self.assertEqual(output_lines[0], 'notice from pythonstartup') self.assertEqual(traceback_lines, expected_lines) def test_pythonstartup_failure(self): @@ -417,11 +419,11 @@ def test_pythonstartup_failure(self): p = spawn_asyncio_repl(env=os.environ | startup_env, stderr=subprocess.PIPE, isolated=False) p.stdin.write("foo()") kill_python(p) - output = p.stderr.read() + output_lines = p.stderr.read().splitlines() p.stderr.close() - tb_start_lines = output.splitlines()[3:5] - tb_final_lines = output.splitlines()[12:] + tb_start_lines = output_lines[3:5] + tb_final_lines = output_lines[12:] expected_lines = [ '>>> import asyncio', From 9e9251032bea7f1a6a8948970f83c3940a5211dc Mon Sep 17 00:00:00 2001 From: bswck Date: Sat, 8 Nov 2025 13:35:15 +0100 Subject: [PATCH 22/28] Use default histfile --- Lib/test/test_repl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index 1902a8eb5888ad..601b9e18089492 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -248,7 +248,7 @@ 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')" - with new_startup_env(code=startup_code, histfile=".pythonhist") as startup_env: + with new_startup_env(code=startup_code) as startup_env: # -q to suppress noise p = spawn_repl("-q", env=os.environ | startup_env, isolated=False) p.stdin.write("1/0") @@ -269,7 +269,7 @@ def test_pythonstartup_success(self): def test_pythonstartup_failure(self): # case 2: error in PYTHONSTARTUP triggered by user input startup_code = "def foo():\n 1/0\n" - with new_startup_env(code=startup_code, histfile=".asyncio_history") as startup_env: + with new_startup_env(code=startup_code) as startup_env: # -q to suppress noise p = spawn_repl("-q", env=os.environ | startup_env, isolated=False) p.stdin.write("foo()") From 0a50a506bcc1f13e55768bdd386afda1d43b9f80 Mon Sep 17 00:00:00 2001 From: bswck Date: Sat, 8 Nov 2025 13:35:20 +0100 Subject: [PATCH 23/28] Fix newline --- Lib/test/test_repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index 601b9e18089492..fd9991dd744eca 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -74,7 +74,7 @@ def new_startup_env(*, code: str, histfile: str = ".pythonhist"): with os_helper.temp_dir() as tmpdir: filename = os.path.join(tmpdir, "pythonstartup.py") with open(filename, "w") as f: - f.write(os.linesep.join(code.splitlines())) + f.write('\n'.join(code.splitlines())) yield {"PYTHONSTARTUP": filename, "PYTHON_HISTORY": os.path.join(tmpdir, histfile)} From f8b8d533f06a827443a920800fcda76650d909eb Mon Sep 17 00:00:00 2001 From: bswck Date: Sat, 8 Nov 2025 13:41:48 +0100 Subject: [PATCH 24/28] Use `TestCase.enterContext` --- Lib/test/test_repl.py | 159 ++++++++++++++++++++---------------------- 1 file changed, 77 insertions(+), 82 deletions(-) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index fd9991dd744eca..f537468eafd690 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -248,43 +248,42 @@ 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')" - with new_startup_env(code=startup_code) as startup_env: - # -q to suppress noise - p = spawn_repl("-q", env=os.environ | startup_env, isolated=False) - p.stdin.write("1/0") - output_lines = kill_python(p).splitlines() - - self.assertEqual(output_lines[0], 'notice from pythonstartup') - - traceback_lines = output_lines[2:-1] - expected_lines = [ - 'Traceback (most recent call last):', - ' File "", line 1, in ', - ' 1/0', - ' ~^~', - 'ZeroDivisionError: division by zero', - ] - self.assertEqual(traceback_lines, expected_lines) + startup_env = self.enterContext(new_startup_env(code=startup_code)) + # -q to suppress noise + p = spawn_repl("-q", env=os.environ | startup_env, isolated=False) + p.stdin.write("1/0") + output_lines = kill_python(p).splitlines() + self.assertEqual(output_lines[0], 'notice from pythonstartup') + + traceback_lines = output_lines[2:-1] + expected_lines = [ + 'Traceback (most recent call last):', + ' File "", line 1, in ', + ' 1/0', + ' ~^~', + 'ZeroDivisionError: division by zero', + ] + self.assertEqual(traceback_lines, expected_lines) def test_pythonstartup_failure(self): # case 2: error in PYTHONSTARTUP triggered by user input startup_code = "def foo():\n 1/0\n" - with new_startup_env(code=startup_code) as startup_env: - # -q to suppress noise - p = spawn_repl("-q", env=os.environ | startup_env, isolated=False) - p.stdin.write("foo()") - traceback_lines = kill_python(p).splitlines()[1:-1] - expected_lines = [ - 'Traceback (most recent call last):', - ' File "", line 1, in ', - ' foo()', - ' ~~~^^', - f' File "{startup_env['PYTHONSTARTUP']}", line 2, in foo', - ' 1/0', - ' ~^~', - 'ZeroDivisionError: division by zero', - ] - self.assertEqual(traceback_lines, expected_lines) + startup_env = self.enterContext(new_startup_env(code=startup_code)) + # -q to suppress noise + p = spawn_repl("-q", env=os.environ | startup_env, isolated=False) + p.stdin.write("foo()") + traceback_lines = kill_python(p).splitlines()[1:-1] + expected_lines = [ + 'Traceback (most recent call last):', + ' File "", line 1, in ', + ' foo()', + ' ~~~^^', + f' File "{startup_env['PYTHONSTARTUP']}", line 2, in foo', + ' 1/0', + ' ~^~', + 'ZeroDivisionError: division by zero', + ] + self.assertEqual(traceback_lines, expected_lines) @unittest.skipUnless(pty, "requires pty") def test_asyncio_repl_is_ok(self): @@ -388,58 +387,54 @@ def test_toplevel_contextvars_async(self): def test_pythonstartup_success(self): startup_code = "import sys\nprint('notice from pythonstartup in asyncio repl', file=sys.stderr)" - with new_startup_env(code=startup_code, histfile=".asyncio_history") as startup_env: - p = spawn_asyncio_repl(env=os.environ | startup_env, stderr=subprocess.PIPE, isolated=False) - p.stdin.write("1/0") - kill_python(p) - output_lines = p.stderr.read().splitlines() - p.stderr.close() - - self.assertEqual(output_lines[3], 'notice from pythonstartup in asyncio repl') - - tb_start_lines = output_lines[4:6] - tb_final_lines = output_lines[13:] - - expected_lines = [ - '>>> import asyncio', - 'Traceback (most recent call last):', - ' File "", line 1, in ', - ' 1/0', - ' ~^~', - 'ZeroDivisionError: division by zero', - '', - 'exiting asyncio REPL...', - ] - - self.assertEqual(tb_start_lines + tb_final_lines, expected_lines) + startup_env = self.enterContext(new_startup_env(code=startup_code, histfile=".asyncio_history")) + p = spawn_asyncio_repl(env=os.environ | startup_env, stderr=subprocess.PIPE, isolated=False) + p.stdin.write("1/0") + kill_python(p) + output_lines = p.stderr.read().splitlines() + p.stderr.close() + + self.assertEqual(output_lines[3], 'notice from pythonstartup in asyncio repl') + + tb_start_lines = output_lines[4:6] + tb_final_lines = output_lines[13:] + expected_lines = [ + '>>> import asyncio', + 'Traceback (most recent call last):', + ' File "", line 1, in ', + ' 1/0', + ' ~^~', + 'ZeroDivisionError: division by zero', + '', + 'exiting asyncio REPL...', + ] + self.assertEqual(tb_start_lines + tb_final_lines, expected_lines) def test_pythonstartup_failure(self): startup_code = "def foo():\n 1/0\n" - with new_startup_env(code=startup_code, histfile=".asyncio_history") as startup_env: - p = spawn_asyncio_repl(env=os.environ | startup_env, stderr=subprocess.PIPE, isolated=False) - p.stdin.write("foo()") - kill_python(p) - output_lines = p.stderr.read().splitlines() - p.stderr.close() - - tb_start_lines = output_lines[3:5] - tb_final_lines = output_lines[12:] - - expected_lines = [ - '>>> import asyncio', - 'Traceback (most recent call last):', - ' File "", line 1, in ', - ' foo()', - ' ~~~^^', - f' File "{startup_env['PYTHONSTARTUP']}", line 2, in foo', - ' 1/0', - ' ~^~', - 'ZeroDivisionError: division by zero', - '', - 'exiting asyncio REPL...', - ] - - self.assertEqual(tb_start_lines + tb_final_lines, expected_lines) + startup_env = self.enterContext(new_startup_env(code=startup_code, histfile=".asyncio_history")) + p = spawn_asyncio_repl(env=os.environ | startup_env, stderr=subprocess.PIPE, isolated=False) + p.stdin.write("foo()") + kill_python(p) + output_lines = p.stderr.read().splitlines() + p.stderr.close() + + tb_start_lines = output_lines[3:5] + tb_final_lines = output_lines[12:] + expected_lines = [ + '>>> import asyncio', + 'Traceback (most recent call last):', + ' File "", line 1, in ', + ' foo()', + ' ~~~^^', + f' File "{startup_env['PYTHONSTARTUP']}", line 2, in foo', + ' 1/0', + ' ~^~', + 'ZeroDivisionError: division by zero', + '', + 'exiting asyncio REPL...', + ] + self.assertEqual(tb_start_lines + tb_final_lines, expected_lines) if __name__ == "__main__": From 5701fefeeff38fe81e89add86965d05b6e645d4b Mon Sep 17 00:00:00 2001 From: bswck Date: Sat, 8 Nov 2025 16:00:18 +0100 Subject: [PATCH 25/28] Remove lines with ps1 --- Lib/test/test_repl.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index f537468eafd690..689df971f87ad6 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -396,10 +396,9 @@ def test_pythonstartup_success(self): self.assertEqual(output_lines[3], 'notice from pythonstartup in asyncio repl') - tb_start_lines = output_lines[4:6] + tb_start_lines = output_lines[5:6] tb_final_lines = output_lines[13:] expected_lines = [ - '>>> import asyncio', 'Traceback (most recent call last):', ' File "", line 1, in ', ' 1/0', @@ -419,10 +418,9 @@ def test_pythonstartup_failure(self): output_lines = p.stderr.read().splitlines() p.stderr.close() - tb_start_lines = output_lines[3:5] + tb_start_lines = output_lines[4:5] tb_final_lines = output_lines[12:] expected_lines = [ - '>>> import asyncio', 'Traceback (most recent call last):', ' File "", line 1, in ', ' foo()', From 875fd2a42a01436402070f7cae5ac68e7a098af5 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Tue, 16 Dec 2025 04:36:36 +0100 Subject: [PATCH 26/28] Employ `asyncio.Runner` in the asyncio REPL --- Lib/asyncio/__main__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/asyncio/__main__.py b/Lib/asyncio/__main__.py index 6cd21a2c9df0f2..573cc9223476a7 100644 --- a/Lib/asyncio/__main__.py +++ b/Lib/asyncio/__main__.py @@ -192,8 +192,8 @@ def interrupt(self) -> None: from _pyrepl.main import CAN_USE_PYREPL return_code = 0 - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) + runner = asyncio.Runner() + loop = runner.get_loop() repl_locals = {'asyncio': asyncio} for key in {'__name__', '__package__', @@ -245,4 +245,5 @@ def interrupt(self) -> None: break console.write('exiting asyncio REPL...\n') + runner.close() sys.exit(return_code) From 4377d82c8dd96117271aa2a7ee1b6ff0a0698d39 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Tue, 16 Dec 2025 05:36:11 +0100 Subject: [PATCH 27/28] Revert "Employ `asyncio.Runner` in the asyncio REPL" This reverts commit 875fd2a42a01436402070f7cae5ac68e7a098af5. --- Lib/asyncio/__main__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Lib/asyncio/__main__.py b/Lib/asyncio/__main__.py index 573cc9223476a7..6cd21a2c9df0f2 100644 --- a/Lib/asyncio/__main__.py +++ b/Lib/asyncio/__main__.py @@ -192,8 +192,8 @@ def interrupt(self) -> None: from _pyrepl.main import CAN_USE_PYREPL return_code = 0 - runner = asyncio.Runner() - loop = runner.get_loop() + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) repl_locals = {'asyncio': asyncio} for key in {'__name__', '__package__', @@ -245,5 +245,4 @@ def interrupt(self) -> None: break console.write('exiting asyncio REPL...\n') - runner.close() sys.exit(return_code) From b9ffea764c03679f35a8787aa31ba8b009e70dad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20S=C5=82awecki?= Date: Sat, 20 Dec 2025 18:36:11 +0100 Subject: [PATCH 28/28] Add a todo for future --- Lib/asyncio/__main__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/asyncio/__main__.py b/Lib/asyncio/__main__.py index db788531e225ce..b7c3af1356f97d 100644 --- a/Lib/asyncio/__main__.py +++ b/Lib/asyncio/__main__.py @@ -103,6 +103,7 @@ def run(self): startup_code = compile(f.read(), startup_path, "exec") try: exec(startup_code, console.locals) + # TODO: Revisit in GH-143023 except SystemExit: raise except BaseException: