Skip to content

Commit e35c437

Browse files
serhiy-storchakaterryjreedyclaude
committed
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> (cherry picked from commit 4bc392c)
1 parent 50e0bc0 commit e35c437

14 files changed

Lines changed: 107 additions & 25 deletions

File tree

‎Doc/builtins/functions.rst‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -336,8 +336,14 @@ are always available. They are listed here in alphabetical order.
336336
``__debug__`` is true), ``1`` (asserts are removed, ``__debug__`` is false)
337337
or ``2`` (docstrings are removed too).
338338

339-
This function raises :exc:`SyntaxError` or :exc:`ValueError` if the compiled
340-
source is invalid.
339+
This function raises :exc:`SyntaxError` if the compiled source is invalid,
340+
including a *source* containing a null character or that cannot be decoded;
341+
:exc:`ValueError` if *mode* or *flags* is invalid,
342+
or if a string *source* contains surrogate characters;
343+
:exc:`MemoryError` or :exc:`RecursionError` if *source* is too complex
344+
to parse or compile,
345+
for example an expression with many thousands of nested operators;
346+
and :exc:`OverflowError` if *source* is too large.
341347

342348
If you want to parse Python code into its AST representation, see
343349
:func:`ast.parse`.
@@ -369,10 +375,14 @@ are always available. They are listed here in alphabetical order.
369375
Previously, :exc:`TypeError` was raised when null bytes were encountered
370376
in *source*.
371377

372-
.. versionadded:: 3.8
378+
.. versionchanged:: 3.8
373379
``ast.PyCF_ALLOW_TOP_LEVEL_AWAIT`` can now be passed in flags to enable
374380
support for top-level ``await``, ``async for``, and ``async with``.
375381

382+
.. versionchanged:: 3.12
383+
:exc:`SyntaxError` is raised instead of :exc:`ValueError` when null bytes
384+
are encountered in *source*.
385+
376386

377387
.. class:: complex(number=0, /)
378388
complex(string, /)

‎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
@@ -195,7 +195,7 @@ def runsource(self, source, filename="<input>", symbol="single"):
195195
ast.PyCF_ONLY_AST,
196196
incomplete_input=False,
197197
)
198-
except (SyntaxError, OverflowError, ValueError):
198+
except Exception:
199199
self.showsyntaxerror(filename, source=source)
200200
return False
201201
if tree.body:
@@ -216,7 +216,7 @@ def runsource(self, source, filename="<input>", symbol="single"):
216216
)
217217
self.showsyntaxerror(filename, source=source)
218218
return False
219-
except (OverflowError, ValueError):
219+
except Exception:
220220
self.showsyntaxerror(filename, source=source)
221221
return False
222222

‎Lib/_pyrepl/simple_interact.py‎

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

‎Lib/code.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ def runsource(self, source, filename="<input>", symbol="single"):
4545
One of several things can happen:
4646
4747
1) The input is incorrect; compile_command() raised an
48-
exception (SyntaxError or OverflowError). A syntax traceback
49-
will be printed by calling the showsyntaxerror() method.
48+
exception (usually SyntaxError). A syntax traceback will be
49+
printed by calling the showsyntaxerror() method.
5050
5151
2) The input is incomplete, and more input is required;
5252
compile_command() returned None. Nothing happens.
@@ -63,7 +63,7 @@ def runsource(self, source, filename="<input>", symbol="single"):
6363
"""
6464
try:
6565
code = self.compile(source, filename, symbol)
66-
except (OverflowError, SyntaxError, ValueError):
66+
except Exception:
6767
# Case 1
6868
self.showsyntaxerror(filename, source=source)
6969
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
@@ -140,7 +140,7 @@ def find_function(funcname, filename):
140140
if funcdef:
141141
try:
142142
code = compile(funcdef, filename, 'exec')
143-
except SyntaxError:
143+
except Exception:
144144
continue
145145
# We should always be able to find the code object here
146146
funccode = next(c for c in code.co_consts if
@@ -2270,7 +2270,7 @@ def _compile_error_message(self, expr):
22702270
"""Return the error message as string if compiling `expr` fails."""
22712271
try:
22722272
compile(expr, "<stdin>", "eval")
2273-
except SyntaxError as exc:
2273+
except Exception as exc:
22742274
return _rstr(self._format_exc(exc))
22752275
return ""
22762276

‎Lib/test/test_code_module.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,17 @@ def test_unicode_error(self):
112112
self.assertIsNone(self.sysmod.last_value.__traceback__)
113113
self.assertIs(self.sysmod.last_exc, self.sysmod.last_value)
114114

115+
def test_compile_error(self):
116+
# Any error raised by compile() must be reported (gh-69919).
117+
self.infunc.side_effect = ['-' * 100_000 + '1', EOFError('Finished')]
118+
self.console.interact()
119+
output = ''.join(''.join(call[1]) for call in self.stderr.method_calls)
120+
output = output[output.index('(InteractiveConsole)'):]
121+
output = output[output.index('\n') + 1:]
122+
self.assertRegex(output, r'^(MemoryError|RecursionError): ')
123+
self.assertIn(self.sysmod.last_type, (MemoryError, RecursionError))
124+
self.assertIs(self.sysmod.last_exc, self.sysmod.last_value)
125+
115126
def test_sysexcepthook(self):
116127
self.infunc.side_effect = ["def f():",
117128
" raise ValueError('BOOM!')",

0 commit comments

Comments
 (0)