From 44d0f88ecab8f9554540f877fce67bf67fa63e3d Mon Sep 17 00:00:00 2001 From: johnslavik Date: Mon, 1 Jun 2026 23:33:24 +0200 Subject: [PATCH 1/4] Fix SystemError during REPL input compilation corrupting display --- Lib/_pyrepl/simple_interact.py | 2 +- Lib/code.py | 6 +++--- Lib/codeop.py | 8 +++++--- Lib/test/test_pyrepl/support.py | 2 +- Lib/test/test_pyrepl/test_interact.py | 7 +++++++ .../2026-06-01-21-31-06.gh-issue-150732.WqHBvP.rst | 1 + 6 files changed, 18 insertions(+), 8 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-06-01-21-31-06.gh-issue-150732.WqHBvP.rst diff --git a/Lib/_pyrepl/simple_interact.py b/Lib/_pyrepl/simple_interact.py index c169d0191bd833..6de6b9995f1b2a 100644 --- a/Lib/_pyrepl/simple_interact.py +++ b/Lib/_pyrepl/simple_interact.py @@ -83,7 +83,7 @@ def _more_lines(console: code.InteractiveConsole, unicodetext: str) -> bool: src = _strip_final_indent(unicodetext) try: code = console.compile(src, "", "single") - except (OverflowError, SyntaxError, ValueError): + except (OverflowError, SyntaxError, ValueError, SystemError): lines = src.splitlines(keepends=True) if len(lines) == 1: return False diff --git a/Lib/code.py b/Lib/code.py index df1d7199e33934..887072749a1271 100644 --- a/Lib/code.py +++ b/Lib/code.py @@ -44,8 +44,8 @@ def runsource(self, source, filename="", symbol="single"): One of several things can happen: 1) The input is incorrect; compile_command() raised an - exception (SyntaxError or OverflowError). A syntax traceback - will be printed by calling the showsyntaxerror() method. + exception. A syntax traceback will be printed by calling + the showsyntaxerror() method. 2) The input is incomplete, and more input is required; compile_command() returned None. Nothing happens. @@ -62,7 +62,7 @@ def runsource(self, source, filename="", symbol="single"): """ try: code = self.compile(source, filename, symbol) - except (OverflowError, SyntaxError, ValueError): + except (OverflowError, SyntaxError, ValueError, SystemError): # Case 1 self.showsyntaxerror(filename, source=source) return False diff --git a/Lib/codeop.py b/Lib/codeop.py index 40e88423119bc4..2b50c7dd5bacd9 100644 --- a/Lib/codeop.py +++ b/Lib/codeop.py @@ -71,6 +71,8 @@ def _maybe_compile(compiler, source, filename, symbol, flags): except SyntaxError: pass # fallthrough + except SystemError as exc: + raise exc from None return compiler(source, filename, symbol, incomplete_input=False) @@ -147,8 +149,8 @@ def __call__(self, source, filename="", symbol="single"): - Return a code object if the command is complete and valid - Return None if the command is incomplete - - Raise SyntaxError, ValueError or OverflowError if the command is a - syntax error (OverflowError and ValueError can be produced by - malformed literals). + - Raise SyntaxError, ValueError, OverflowError or SystemError. + OverflowError and ValueError can be produced by malformed + literals, SystemError if source cannot be compiled. """ return _maybe_compile(self.compiler, source, filename, symbol, flags=self.compiler.flags) diff --git a/Lib/test/test_pyrepl/support.py b/Lib/test/test_pyrepl/support.py index c879a2f93b6313..ebccf9822bb899 100644 --- a/Lib/test/test_pyrepl/support.py +++ b/Lib/test/test_pyrepl/support.py @@ -44,7 +44,7 @@ def more_lines(text: str, namespace: dict | None = None): console = InteractiveConsole(namespace, filename="") try: code = console.compile(src, "", "single") - except (OverflowError, SyntaxError, ValueError): + except (OverflowError, SyntaxError, ValueError, SystemError): return False else: return code is None diff --git a/Lib/test/test_pyrepl/test_interact.py b/Lib/test/test_pyrepl/test_interact.py index fd4530ebc004aa..55dadb9a6e3aaf 100644 --- a/Lib/test/test_pyrepl/test_interact.py +++ b/Lib/test/test_pyrepl/test_interact.py @@ -275,6 +275,13 @@ def test_incomplete_statement(self): console = InteractiveColoredConsole(namespace, filename="") self.assertTrue(_more_lines(console, code)) + def test_system_error_during_compilation(self): + namespace = {} + console = InteractiveColoredConsole(namespace, filename="") + with patch.object(console, "compile", side_effect=SystemError("compiler bug")): + result = _more_lines(console, "some code") + self.assertFalse(result) + class TestWarnings(unittest.TestCase): def test_pep_765_warning(self): diff --git a/Misc/NEWS.d/next/Library/2026-06-01-21-31-06.gh-issue-150732.WqHBvP.rst b/Misc/NEWS.d/next/Library/2026-06-01-21-31-06.gh-issue-150732.WqHBvP.rst new file mode 100644 index 00000000000000..03e9f7ed3b7655 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-01-21-31-06.gh-issue-150732.WqHBvP.rst @@ -0,0 +1 @@ +Fix ``PyREPL`` and :mode:`code` REPL mishandling a :exc:`SystemError` from the compiler. Patch by Bartosz Sławecki. From f954ab259c14eb6b3371461a84f56de9722ed5ea Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sun, 14 Jun 2026 20:51:07 +0200 Subject: [PATCH 2/4] Add tests for SystemError handling in codeop and code module --- Lib/test/test_code_module.py | 9 +++++++++ Lib/test/test_codeop.py | 12 ++++++++++++ .../2026-06-01-21-31-06.gh-issue-150732.WqHBvP.rst | 2 +- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_code_module.py b/Lib/test/test_code_module.py index 3642b47c2c1f03..6143baf59340ed 100644 --- a/Lib/test/test_code_module.py +++ b/Lib/test/test_code_module.py @@ -324,6 +324,15 @@ def test_context_tb(self): self.assertIsNotNone(self.sysmod.last_traceback) self.assertIs(self.sysmod.last_exc, self.sysmod.last_value) + def test_system_error(self): + # SystemError from the compiler should be reported (runsource returns + # False) rather than propagating to the caller. + from unittest import mock + err = SystemError("compiler internal error") + self.console.compile = mock.Mock(side_effect=err) + result = self.console.runsource("x = 1") + self.assertFalse(result) + class TestInteractiveConsoleLocalExit(unittest.TestCase, MockSys): diff --git a/Lib/test/test_codeop.py b/Lib/test/test_codeop.py index ed10bd3dcb6d2b..1e8dfa3c5242a1 100644 --- a/Lib/test/test_codeop.py +++ b/Lib/test/test_codeop.py @@ -324,6 +324,18 @@ def foo(x,x): pass """), "duplicate parameter 'x' in function definition") + def test_system_error(self): + # SystemError from the compiler should propagate with context suppressed. + # The first call raises SyntaxError to enter the inner try block; + # the second (source + "\n") raises SystemError. + import unittest.mock as mock + err = SystemError("compiler internal error") + with mock.patch('codeop._compile', side_effect=[SyntaxError(), err]): + with self.assertRaises(SystemError) as cm: + compile_command("x = 1") + self.assertIs(cm.exception, err) + self.assertIsNone(err.__cause__) + self.assertTrue(err.__suppress_context__) if __name__ == "__main__": diff --git a/Misc/NEWS.d/next/Library/2026-06-01-21-31-06.gh-issue-150732.WqHBvP.rst b/Misc/NEWS.d/next/Library/2026-06-01-21-31-06.gh-issue-150732.WqHBvP.rst index 03e9f7ed3b7655..e9423988e58d5f 100644 --- a/Misc/NEWS.d/next/Library/2026-06-01-21-31-06.gh-issue-150732.WqHBvP.rst +++ b/Misc/NEWS.d/next/Library/2026-06-01-21-31-06.gh-issue-150732.WqHBvP.rst @@ -1 +1 @@ -Fix ``PyREPL`` and :mode:`code` REPL mishandling a :exc:`SystemError` from the compiler. Patch by Bartosz Sławecki. +Handle :exc:`SystemError` from the compiler in ``PyREPL``, :mod:`code`, and :mod:`codeop`. Patch by Bartosz Sławecki. From 66c7b819614db9c7c9e2d69e4ebc4c56750ef75b Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sun, 14 Jun 2026 20:54:44 +0200 Subject: [PATCH 3/4] Remove test_system_error from test_codeop --- Lib/test/test_codeop.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/Lib/test/test_codeop.py b/Lib/test/test_codeop.py index 1e8dfa3c5242a1..ed10bd3dcb6d2b 100644 --- a/Lib/test/test_codeop.py +++ b/Lib/test/test_codeop.py @@ -324,18 +324,6 @@ def foo(x,x): pass """), "duplicate parameter 'x' in function definition") - def test_system_error(self): - # SystemError from the compiler should propagate with context suppressed. - # The first call raises SyntaxError to enter the inner try block; - # the second (source + "\n") raises SystemError. - import unittest.mock as mock - err = SystemError("compiler internal error") - with mock.patch('codeop._compile', side_effect=[SyntaxError(), err]): - with self.assertRaises(SystemError) as cm: - compile_command("x = 1") - self.assertIs(cm.exception, err) - self.assertIsNone(err.__cause__) - self.assertTrue(err.__suppress_context__) if __name__ == "__main__": From 8aa7870f18c634f814d487ee3112ad6121229b8f Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sun, 14 Jun 2026 21:01:14 +0200 Subject: [PATCH 4/4] Fix the codeop change --- Lib/codeop.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Lib/codeop.py b/Lib/codeop.py index 2b50c7dd5bacd9..f4b5e971738665 100644 --- a/Lib/codeop.py +++ b/Lib/codeop.py @@ -66,13 +66,11 @@ def _maybe_compile(compiler, source, filename, symbol, flags): try: compiler(source + "\n", filename, symbol, flags=flags) return None - except _IncompleteInputError: + except _IncompleteInputError, SystemError: return None except SyntaxError: pass # fallthrough - except SystemError as exc: - raise exc from None return compiler(source, filename, symbol, incomplete_input=False)