Skip to content

Commit 4bc392c

Browse files
serhiy-storchakaterryjreedyclaude
authored
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. Co-authored-by: Terry Jan Reedy <tjreedy@udel.edu> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent b54ae4c commit 4bc392c

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
@@ -384,8 +384,14 @@ are always available. They are listed here in alphabetical order.
384384
It is needed to unambiguous :ref:`filter <warning-filter>` syntax warnings
385385
by module name.
386386

387-
This function raises :exc:`SyntaxError` or :exc:`ValueError` if the compiled
388-
source is invalid.
387+
This function raises :exc:`SyntaxError` if the compiled source is invalid,
388+
including a *source* containing a null character or that cannot be decoded;
389+
:exc:`ValueError` if *mode* or *flags* is invalid,
390+
or if a string *source* contains surrogate characters;
391+
:exc:`MemoryError` or :exc:`RecursionError` if *source* is too complex
392+
to parse or compile,
393+
for example an expression with many thousands of nested operators;
394+
and :exc:`OverflowError` if *source* is too large.
389395

390396
If you want to parse Python code into its AST representation, see
391397
:func:`ast.parse`.
@@ -417,11 +423,15 @@ are always available. They are listed here in alphabetical order.
417423
Previously, :exc:`TypeError` was raised when null bytes were encountered
418424
in *source*.
419425

420-
.. versionadded:: 3.8
426+
.. versionchanged:: 3.8
421427
``ast.PyCF_ALLOW_TOP_LEVEL_AWAIT`` can now be passed in flags to enable
422428
support for top-level ``await``, ``async for``, and ``async with``.
423429

424-
.. versionadded:: 3.15
430+
.. versionchanged:: 3.12
431+
:exc:`SyntaxError` is raised instead of :exc:`ValueError` when null bytes
432+
are encountered in *source*.
433+
434+
.. versionchanged:: 3.15
425435
Added the *module* parameter.
426436

427437

‎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
@@ -257,7 +257,7 @@ def runsource(self, source, filename="<input>", symbol="single"):
257257
)
258258
self.showsyntaxerror(filename, source=source)
259259
return False
260-
except (OverflowError, ValueError):
260+
except Exception:
261261
self.showsyntaxerror(filename, source=source)
262262
return False
263263
if tree.body:
@@ -278,7 +278,7 @@ def runsource(self, source, filename="<input>", symbol="single"):
278278
)
279279
self.showsyntaxerror(filename, source=source)
280280
return False
281-
except (OverflowError, ValueError):
281+
except Exception:
282282
self.showsyntaxerror(filename, source=source)
283283
return False
284284

‎Lib/_pyrepl/simple_interact.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def _more_lines(console: code.InteractiveConsole, unicodetext: str) -> bool:
8282
src = _strip_final_indent(unicodetext)
8383
try:
8484
code = console.compile(src, "<stdin>", "single")
85-
except (OverflowError, SyntaxError, ValueError):
85+
except Exception:
8686
lines = src.splitlines(keepends=True)
8787
if len(lines) == 1:
8888
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):',
@@ -736,19 +736,23 @@ def showsyntaxerror(self, filename=None, **kwargs):
736736
text = tkconsole.text
737737
text.tag_remove("ERROR", "1.0", "end")
738738
type, value, tb = sys.exc_info()
739-
msg = getattr(value, 'msg', '') or value or "<no detail available>"
740-
lineno = getattr(value, 'lineno', '') or 1
741-
offset = getattr(value, 'offset', '') or 0
739+
if not issubclass(type, SyntaxError):
740+
tkconsole.resetoutput()
741+
InteractiveInterpreter.showsyntaxerror(self, filename, **kwargs)
742+
tkconsole.showprompt()
743+
return
744+
msg = value.msg or "<no detail available>"
745+
lineno = value.lineno or 1
746+
offset = value.offset or 0
742747
if offset == 0:
743748
lineno += 1 #mark end of offending line
744749
if lineno == 1:
745-
pos = "iomark + %d chars" % (offset-1)
750+
pos = f"iomark + {offset-1} chars"
746751
else:
747-
pos = "iomark linestart + %d lines + %d chars" % \
748-
(lineno-1, offset-1)
752+
pos = f"iomark linestart + {lineno-1} lines + {offset-1} chars"
749753
tkconsole.colorize_syntax_error(text, pos)
750754
tkconsole.resetoutput()
751-
self.write("SyntaxError: %s\n" % msg)
755+
self.write(f"{type.__name__}: {msg}\n")
752756
tkconsole.showprompt()
753757

754758
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
@@ -156,7 +156,7 @@ def find_function(funcname, filename):
156156
if funcdef:
157157
try:
158158
code = compile(funcdef, filename, 'exec')
159-
except SyntaxError:
159+
except Exception:
160160
continue
161161
# We should always be able to find the code object here
162162
funccode = next(c for c in code.co_consts if
@@ -2781,7 +2781,7 @@ def _compile_error_message(self, expr):
27812781
"""Return the error message as string if compiling `expr` fails."""
27822782
try:
27832783
compile(expr, "<stdin>", "eval")
2784-
except SyntaxError as exc:
2784+
except Exception as exc:
27852785
return _rstr(self._format_exc(exc))
27862786
return ""
27872787

‎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)