From faa22f483021f06e887e35199a629cd784be2037 Mon Sep 17 00:00:00 2001 From: bswck Date: Sat, 18 Oct 2025 12:07:27 +0200 Subject: [PATCH 01/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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