Skip to content

Commit e2b711c

Browse files
miss-islingtonserhiy-storchakaterryjreedyclaude
authored
[3.15] gh-69919: Catch all compile errors in the code module, pyrepl and IDLE (GH-157585) (#158025)
gh-69919: Catch all compile errors in the code module, pyrepl and IDLE (GH-157585) compile() can raise MemoryError or RecursionError for too deeply nested source, not only SyntaxError, OverflowError and ValueError. IDLE's Shell then lost its prompt until the input was deleted. (cherry picked from commit 4bc392c) Co-authored-by: Serhiy Storchaka <storchaka@gmail.com> Co-authored-by: Terry Jan Reedy <tjreedy@udel.edu> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 278a9d7 commit e2b711c

14 files changed

Lines changed: 108 additions & 26 deletions

File tree

‎Doc/builtins/functions.rst‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -344,8 +344,14 @@ are always available. They are listed here in alphabetical order.
344344
It is needed to unambiguous :ref:`filter <warning-filter>` syntax warnings
345345
by module name.
346346

347-
This function raises :exc:`SyntaxError` or :exc:`ValueError` if the compiled
348-
source is invalid.
347+
This function raises :exc:`SyntaxError` if the compiled source is invalid,
348+
including a *source* containing a null character or that cannot be decoded;
349+
:exc:`ValueError` if *mode* or *flags* is invalid,
350+
or if a string *source* contains surrogate characters;
351+
:exc:`MemoryError` or :exc:`RecursionError` if *source* is too complex
352+
to parse or compile,
353+
for example an expression with many thousands of nested operators;
354+
and :exc:`OverflowError` if *source* is too large.
349355

350356
If you want to parse Python code into its AST representation, see
351357
:func:`ast.parse`.
@@ -377,11 +383,15 @@ are always available. They are listed here in alphabetical order.
377383
Previously, :exc:`TypeError` was raised when null bytes were encountered
378384
in *source*.
379385

380-
.. versionadded:: 3.8
386+
.. versionchanged:: 3.8
381387
``ast.PyCF_ALLOW_TOP_LEVEL_AWAIT`` can now be passed in flags to enable
382388
support for top-level ``await``, ``async for``, and ``async with``.
383389

384-
.. versionadded:: 3.15
390+
.. versionchanged:: 3.12
391+
:exc:`SyntaxError` is raised instead of :exc:`ValueError` when null bytes
392+
are encountered in *source*.
393+
394+
.. versionchanged:: 3.15
385395
Added the *module* parameter.
386396

387397

‎Doc/library/code.rst‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ Interactive Interpreter Objects
9292
*symbol* is ``'single'``. One of several things can happen:
9393

9494
* The input is incorrect; :func:`compile_command` raised an exception
95-
(:exc:`SyntaxError` or :exc:`OverflowError`). A syntax traceback will be
95+
(usually :exc:`SyntaxError`). A syntax traceback will be
9696
printed by calling the :meth:`showsyntaxerror` method. :meth:`runsource`
9797
returns ``False``.
9898

‎Lib/_pyrepl/console.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ def runsource(self, source, filename="<input>", symbol="single"):
261261
)
262262
self.showsyntaxerror(filename, source=source)
263263
return False
264-
except (OverflowError, ValueError):
264+
except Exception:
265265
self.showsyntaxerror(filename, source=source)
266266
return False
267267
if tree.body:
@@ -282,7 +282,7 @@ def runsource(self, source, filename="<input>", symbol="single"):
282282
)
283283
self.showsyntaxerror(filename, source=source)
284284
return False
285-
except (OverflowError, ValueError):
285+
except Exception:
286286
self.showsyntaxerror(filename, source=source)
287287
return False
288288

‎Lib/_pyrepl/simple_interact.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def _more_lines(console: code.InteractiveConsole, unicodetext: str) -> bool:
8383
src = _strip_final_indent(unicodetext)
8484
try:
8585
code = console.compile(src, "<stdin>", "single")
86-
except (OverflowError, SyntaxError, ValueError):
86+
except Exception:
8787
lines = src.splitlines(keepends=True)
8888
if len(lines) == 1:
8989
return False

‎Lib/code.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ def runsource(self, source, filename="<input>", symbol="single"):
4444
One of several things can happen:
4545
4646
1) The input is incorrect; compile_command() raised an
47-
exception (SyntaxError or OverflowError). A syntax traceback
48-
will be printed by calling the showsyntaxerror() method.
47+
exception (usually SyntaxError). A syntax traceback will be
48+
printed by calling the showsyntaxerror() method.
4949
5050
2) The input is incomplete, and more input is required;
5151
compile_command() returned None. Nothing happens.
@@ -62,7 +62,7 @@ def runsource(self, source, filename="<input>", symbol="single"):
6262
"""
6363
try:
6464
code = self.compile(source, filename, symbol)
65-
except (OverflowError, SyntaxError, ValueError):
65+
except Exception:
6666
# Case 1
6767
self.showsyntaxerror(filename, source=source)
6868
return False

‎Lib/idlelib/idle_test/test_runscript.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,19 @@ def test_init(self):
2929
sb = runscript.ScriptBinding(ew)
3030
ew._close()
3131

32+
def test_checksyntax_compile_error(self):
33+
# gh-69919: any error raised by compile() is reported.
34+
ew = EditorWindow(root=self.root)
35+
sb = runscript.ScriptBinding(ew)
36+
sb.flist = mock.Mock()
37+
sb.errorbox = mock.Mock()
38+
with (mock.patch('idlelib.runscript.compile', create=True,
39+
side_effect=MemoryError()),
40+
mock.patch('idlelib.runscript.open', mock.mock_open(read_data=b'x\n'))):
41+
self.assertFalse(sb.checksyntax('test.py'))
42+
sb.errorbox.assert_called_once_with('MemoryError', '<no detail available>')
43+
ew._close()
44+
3245
def test_run_module_event_shell_busy_no_restart(self):
3346
# gh-82183: running without restarting the busy shell aborts.
3447
ew = EditorWindow(root=self.root)

‎Lib/idlelib/pyshell.py‎

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -686,7 +686,7 @@ def execfile(self, filename, source=None):
686686
+ source + "\ndel __file__")
687687
try:
688688
code = compile(source, filename, "exec")
689-
except (OverflowError, SyntaxError):
689+
except Exception:
690690
self.tkconsole.resetoutput()
691691
print('*** Error in script or command!\n'
692692
'Traceback (most recent call last):',
@@ -737,19 +737,23 @@ def showsyntaxerror(self, filename=None, **kwargs):
737737
tkconsole = self.tkconsole
738738
text = tkconsole.text
739739
type, value, tb = sys.exc_info()
740-
msg = getattr(value, 'msg', '') or value or "<no detail available>"
741-
lineno = getattr(value, 'lineno', '') or 1
742-
offset = getattr(value, 'offset', '') or 0
740+
if not issubclass(type, SyntaxError):
741+
tkconsole.resetoutput()
742+
InteractiveInterpreter.showsyntaxerror(self, filename, **kwargs)
743+
tkconsole.showprompt()
744+
return
745+
msg = value.msg or "<no detail available>"
746+
lineno = value.lineno or 1
747+
offset = value.offset or 0
743748
if offset == 0:
744749
lineno += 1 #mark end of offending line
745750
if lineno == 1:
746-
pos = "iomark + %d chars" % (offset-1)
751+
pos = f"iomark + {offset-1} chars"
747752
else:
748-
pos = "iomark linestart + %d lines + %d chars" % \
749-
(lineno-1, offset-1)
753+
pos = f"iomark linestart + {lineno-1} lines + {offset-1} chars"
750754
tkconsole.colorize_syntax_error(text, pos)
751755
tkconsole.resetoutput()
752-
self.write("SyntaxError: %s\n" % msg)
756+
self.write(f"{type.__name__}: {msg}\n")
753757
tkconsole.showprompt()
754758

755759
def showtraceback(self):

‎Lib/idlelib/runscript.py‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,15 +93,19 @@ def checksyntax(self, filename):
9393
try:
9494
# If successful, return the compiled code
9595
return compile(source, filename, "exec")
96-
except (SyntaxError, OverflowError, ValueError) as value:
97-
msg = getattr(value, 'msg', '') or value or "<no detail available>"
98-
lineno = getattr(value, 'lineno', '') or 1
99-
offset = getattr(value, 'offset', '') or 0
96+
except SyntaxError as value:
97+
msg = value.msg or "<no detail available>"
98+
lineno = value.lineno or 1
99+
offset = value.offset or 0
100100
if offset == 0:
101101
lineno += 1 #mark end of offending line
102102
pos = "0.0 + %d lines + %d chars" % (lineno-1, offset-1)
103103
editwin.colorize_syntax_error(text, pos)
104-
self.errorbox("SyntaxError", "%-20s" % msg)
104+
self.errorbox(type(value).__name__, msg)
105+
return False
106+
except Exception as value:
107+
msg = str(value) or "<no detail available>"
108+
self.errorbox(type(value).__name__, msg)
105109
return False
106110
finally:
107111
shell.set_warning_stream(saved_stream)

‎Lib/pdb.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ def find_function(funcname, filename):
155155
if funcdef:
156156
try:
157157
code = compile(funcdef, filename, 'exec')
158-
except SyntaxError:
158+
except Exception:
159159
continue
160160
# We should always be able to find the code object here
161161
funccode = next(c for c in code.co_consts if
@@ -2776,7 +2776,7 @@ def _compile_error_message(self, expr):
27762776
"""Return the error message as string if compiling `expr` fails."""
27772777
try:
27782778
compile(expr, "<stdin>", "eval")
2779-
except SyntaxError as exc:
2779+
except Exception as exc:
27802780
return _rstr(self._format_exc(exc))
27812781
return ""
27822782

‎Lib/test/test_code_module.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,17 @@ def test_unicode_error(self):
140140
self.assertIsNone(self.sysmod.last_value.__traceback__)
141141
self.assertIs(self.sysmod.last_exc, self.sysmod.last_value)
142142

143+
def test_compile_error(self):
144+
# Any error raised by compile() must be reported (gh-69919).
145+
self.infunc.side_effect = ['-' * 100_000 + '1', EOFError('Finished')]
146+
self.console.interact()
147+
output = ''.join(''.join(call[1]) for call in self.stderr.method_calls)
148+
output = output[output.index('(InteractiveConsole)'):]
149+
output = output[output.index('\n') + 1:]
150+
self.assertRegex(output, r'^(MemoryError|RecursionError): ')
151+
self.assertIn(self.sysmod.last_type, (MemoryError, RecursionError))
152+
self.assertIs(self.sysmod.last_exc, self.sysmod.last_value)
153+
143154
def test_sysexcepthook(self):
144155
self.infunc.side_effect = ["def f():",
145156
" raise ValueError('BOOM!')",

0 commit comments

Comments
 (0)