diff --git a/Doc/builtins/functions.rst b/Doc/builtins/functions.rst index 5cce5e3c87628a..ece666030db981 100644 --- a/Doc/builtins/functions.rst +++ b/Doc/builtins/functions.rst @@ -384,8 +384,14 @@ are always available. They are listed here in alphabetical order. It is needed to unambiguous :ref:`filter ` syntax warnings by module name. - This function raises :exc:`SyntaxError` or :exc:`ValueError` if the compiled - source is invalid. + This function raises :exc:`SyntaxError` if the compiled source is invalid, + including a *source* containing a null character or that cannot be decoded; + :exc:`ValueError` if *mode* or *flags* is invalid, + or if a string *source* contains surrogate characters; + :exc:`MemoryError` or :exc:`RecursionError` if *source* is too complex + to parse or compile, + for example an expression with many thousands of nested operators; + and :exc:`OverflowError` if *source* is too large. If you want to parse Python code into its AST representation, see :func:`ast.parse`. @@ -417,11 +423,15 @@ are always available. They are listed here in alphabetical order. Previously, :exc:`TypeError` was raised when null bytes were encountered in *source*. - .. versionadded:: 3.8 + .. versionchanged:: 3.8 ``ast.PyCF_ALLOW_TOP_LEVEL_AWAIT`` can now be passed in flags to enable support for top-level ``await``, ``async for``, and ``async with``. - .. versionadded:: 3.15 + .. versionchanged:: 3.12 + :exc:`SyntaxError` is raised instead of :exc:`ValueError` when null bytes + are encountered in *source*. + + .. versionchanged:: 3.15 Added the *module* parameter. diff --git a/Doc/library/code.rst b/Doc/library/code.rst index 59c016d21501b0..71eb3e8646cd01 100644 --- a/Doc/library/code.rst +++ b/Doc/library/code.rst @@ -92,7 +92,7 @@ Interactive Interpreter Objects *symbol* is ``'single'``. One of several things can happen: * The input is incorrect; :func:`compile_command` raised an exception - (:exc:`SyntaxError` or :exc:`OverflowError`). A syntax traceback will be + (usually :exc:`SyntaxError`). A syntax traceback will be printed by calling the :meth:`showsyntaxerror` method. :meth:`runsource` returns ``False``. diff --git a/Lib/_pyrepl/console.py b/Lib/_pyrepl/console.py index dcf8ff9b083caa..0da3c36ef9ed47 100644 --- a/Lib/_pyrepl/console.py +++ b/Lib/_pyrepl/console.py @@ -257,7 +257,7 @@ def runsource(self, source, filename="", symbol="single"): ) self.showsyntaxerror(filename, source=source) return False - except (OverflowError, ValueError): + except Exception: self.showsyntaxerror(filename, source=source) return False if tree.body: @@ -278,7 +278,7 @@ def runsource(self, source, filename="", symbol="single"): ) self.showsyntaxerror(filename, source=source) return False - except (OverflowError, ValueError): + except Exception: self.showsyntaxerror(filename, source=source) return False diff --git a/Lib/_pyrepl/simple_interact.py b/Lib/_pyrepl/simple_interact.py index e6c355388a7c07..5a7c8c20202640 100644 --- a/Lib/_pyrepl/simple_interact.py +++ b/Lib/_pyrepl/simple_interact.py @@ -82,7 +82,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 Exception: lines = src.splitlines(keepends=True) if len(lines) == 1: return False diff --git a/Lib/code.py b/Lib/code.py index df1d7199e33934..f658049197e1b7 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 (usually SyntaxError). 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 Exception: # Case 1 self.showsyntaxerror(filename, source=source) return False diff --git a/Lib/idlelib/idle_test/test_runscript.py b/Lib/idlelib/idle_test/test_runscript.py index 1e47f402d504f9..69d253b8f2c73f 100644 --- a/Lib/idlelib/idle_test/test_runscript.py +++ b/Lib/idlelib/idle_test/test_runscript.py @@ -29,6 +29,19 @@ def test_init(self): sb = runscript.ScriptBinding(ew) ew._close() + def test_checksyntax_compile_error(self): + # gh-69919: any error raised by compile() is reported. + ew = EditorWindow(root=self.root) + sb = runscript.ScriptBinding(ew) + sb.flist = mock.Mock() + sb.errorbox = mock.Mock() + with (mock.patch('idlelib.runscript.compile', create=True, + side_effect=MemoryError()), + mock.patch('idlelib.runscript.open', mock.mock_open(read_data=b'x\n'))): + self.assertFalse(sb.checksyntax('test.py')) + sb.errorbox.assert_called_once_with('MemoryError', '') + ew._close() + def test_run_module_event_shell_busy_no_restart(self): # gh-82183: running without restarting the busy shell aborts. ew = EditorWindow(root=self.root) diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py index 6e57e306a3678d..a24e8c72b46e2b 100755 --- a/Lib/idlelib/pyshell.py +++ b/Lib/idlelib/pyshell.py @@ -682,7 +682,7 @@ def execfile(self, filename, source=None): + source + "\ndel __file__") try: code = compile(source, filename, "exec") - except (OverflowError, SyntaxError): + except Exception: self.tkconsole.resetoutput() print('*** Error in script or command!\n' 'Traceback (most recent call last):', @@ -732,19 +732,23 @@ def showsyntaxerror(self, filename=None, **kwargs): text = tkconsole.text text.tag_remove("ERROR", "1.0", "end") type, value, tb = sys.exc_info() - msg = getattr(value, 'msg', '') or value or "" - lineno = getattr(value, 'lineno', '') or 1 - offset = getattr(value, 'offset', '') or 0 + if not issubclass(type, SyntaxError): + tkconsole.resetoutput() + InteractiveInterpreter.showsyntaxerror(self, filename, **kwargs) + tkconsole.showprompt() + return + msg = value.msg or "" + lineno = value.lineno or 1 + offset = value.offset or 0 if offset == 0: lineno += 1 #mark end of offending line if lineno == 1: - pos = "iomark + %d chars" % (offset-1) + pos = f"iomark + {offset-1} chars" else: - pos = "iomark linestart + %d lines + %d chars" % \ - (lineno-1, offset-1) + pos = f"iomark linestart + {lineno-1} lines + {offset-1} chars" tkconsole.colorize_syntax_error(text, pos) tkconsole.resetoutput() - self.write("SyntaxError: %s\n" % msg) + self.write(f"{type.__name__}: {msg}\n") tkconsole.showprompt() def showtraceback(self): diff --git a/Lib/idlelib/runscript.py b/Lib/idlelib/runscript.py index cd52d206c9ca0f..af3581ce0ce773 100644 --- a/Lib/idlelib/runscript.py +++ b/Lib/idlelib/runscript.py @@ -93,15 +93,19 @@ def checksyntax(self, filename): try: # If successful, return the compiled code return compile(source, filename, "exec") - except (SyntaxError, OverflowError, ValueError) as value: - msg = getattr(value, 'msg', '') or value or "" - lineno = getattr(value, 'lineno', '') or 1 - offset = getattr(value, 'offset', '') or 0 + except SyntaxError as value: + msg = value.msg or "" + lineno = value.lineno or 1 + offset = value.offset or 0 if offset == 0: lineno += 1 #mark end of offending line pos = "0.0 + %d lines + %d chars" % (lineno-1, offset-1) editwin.colorize_syntax_error(text, pos) - self.errorbox("SyntaxError", "%-20s" % msg) + self.errorbox(type(value).__name__, msg) + return False + except Exception as value: + msg = str(value) or "" + self.errorbox(type(value).__name__, msg) return False finally: shell.set_warning_stream(saved_stream) diff --git a/Lib/pdb.py b/Lib/pdb.py index 1ef877cce4dd96..e03f4fcf62cf3f 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -156,7 +156,7 @@ def find_function(funcname, filename): if funcdef: try: code = compile(funcdef, filename, 'exec') - except SyntaxError: + except Exception: continue # We should always be able to find the code object here funccode = next(c for c in code.co_consts if @@ -2781,7 +2781,7 @@ def _compile_error_message(self, expr): """Return the error message as string if compiling `expr` fails.""" try: compile(expr, "", "eval") - except SyntaxError as exc: + except Exception as exc: return _rstr(self._format_exc(exc)) return "" diff --git a/Lib/test/test_code_module.py b/Lib/test/test_code_module.py index 3642b47c2c1f03..c0c3454a97fbd8 100644 --- a/Lib/test/test_code_module.py +++ b/Lib/test/test_code_module.py @@ -140,6 +140,17 @@ def test_unicode_error(self): self.assertIsNone(self.sysmod.last_value.__traceback__) self.assertIs(self.sysmod.last_exc, self.sysmod.last_value) + def test_compile_error(self): + # Any error raised by compile() must be reported (gh-69919). + self.infunc.side_effect = ['-' * 100_000 + '1', EOFError('Finished')] + self.console.interact() + output = ''.join(''.join(call[1]) for call in self.stderr.method_calls) + output = output[output.index('(InteractiveConsole)'):] + output = output[output.index('\n') + 1:] + self.assertRegex(output, r'^(MemoryError|RecursionError): ') + self.assertIn(self.sysmod.last_type, (MemoryError, RecursionError)) + self.assertIs(self.sysmod.last_exc, self.sysmod.last_value) + def test_sysexcepthook(self): self.infunc.side_effect = ["def f():", " raise ValueError('BOOM!')", diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index 0aa4ceb71c866f..8876b3f5ce37a3 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -3653,6 +3653,23 @@ def quux(): ('bœr', 5), ) + def test_find_function_too_complex(self): + # gh-69919: compile() can raise more than SyntaxError. + self._assert_find_function( + b"def foo():\n return " + b"-" * 100_000 + b"1\n" + b"def bar():\n pass\n", + 'bar', + ('bar', 4), + ) + + def test_compile_error_message(self): + p = pdb.Pdb() + self.assertEqual(p._compile_error_message('1 + 1'), '') + self.assertIn('SyntaxError', p._compile_error_message('1 +')) + # gh-69919: compile() can raise more than SyntaxError. + self.assertRegex(p._compile_error_message('-' * 100_000 + '1'), + r'^(MemoryError|RecursionError|SyntaxError):') + def test_print_stack_entry_uses_dynamic_line_prefix(self): """Test that pdb.line_prefix binding is dynamic (gh-141781).""" stdout = io.StringIO() diff --git a/Lib/test/test_pyrepl/test_interact.py b/Lib/test/test_pyrepl/test_interact.py index fd4530ebc004aa..673827608abc47 100644 --- a/Lib/test/test_pyrepl/test_interact.py +++ b/Lib/test/test_pyrepl/test_interact.py @@ -131,6 +131,17 @@ def test_runsource_shows_syntax_error_for_failed_compilation(self): console.runsource(source) mock_showsyntaxerror.assert_called_once() + @force_not_colorized + def test_runsource_compile_error(self): + # Any error raised by compile() is reported (gh-69919). + console = InteractiveColoredConsole() + source = '-' * 100_000 + '1' + f = io.StringIO() + with contextlib.redirect_stderr(f): + result = console.runsource(source) + self.assertFalse(result) + self.assertRegex(f.getvalue(), r'^(MemoryError|RecursionError): ') + def test_runsource_survives_null_bytes(self): console = InteractiveColoredConsole() source = "\x00\n" @@ -182,6 +193,12 @@ def test_invalid_syntax_single_line(self): console = InteractiveColoredConsole(namespace, filename="") self.assertFalse(_more_lines(console, code)) + def test_compile_error_single_line(self): + namespace = {} + code = '-' * 100_000 + '1' # MemoryError or RecursionError + console = InteractiveColoredConsole(namespace, filename="") + self.assertFalse(_more_lines(console, code)) + def test_empty_line(self): namespace = {} code = "" diff --git a/Misc/NEWS.d/next/IDLE/2026-09-15-16-37-19.gh-issue-69919.f6AamT.rst b/Misc/NEWS.d/next/IDLE/2026-09-15-16-37-19.gh-issue-69919.f6AamT.rst new file mode 100644 index 00000000000000..fc909152113077 --- /dev/null +++ b/Misc/NEWS.d/next/IDLE/2026-09-15-16-37-19.gh-issue-69919.f6AamT.rst @@ -0,0 +1,2 @@ +IDLE now reports any exception raised by compiling the source, such as +:exc:`MemoryError` for too deeply nested source, instead of hanging the Shell. diff --git a/Misc/NEWS.d/next/Library/2026-09-15-16-37-19.gh-issue-69919.izH1zP.rst b/Misc/NEWS.d/next/Library/2026-09-15-16-37-19.gh-issue-69919.izH1zP.rst new file mode 100644 index 00000000000000..87fd4a7aedee84 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-15-16-37-19.gh-issue-69919.izH1zP.rst @@ -0,0 +1,4 @@ +:meth:`code.InteractiveInterpreter.runsource` and the REPL now report any +exception raised by compiling the source, such as :exc:`MemoryError` or +:exc:`RecursionError` for too deeply nested source, instead of propagating +it or printing a traceback of the REPL internals.