Skip to content

Commit edc1034

Browse files
serhiy-storchakaterryjreedyclaude
authored
[3.14] gh-69919: Catch all compile errors in the code module, pyrepl and IDLE (GH-157585) (#158029)
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: Terry Jan Reedy <tjreedy@udel.edu> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 78bdd3d commit edc1034

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
@@ -84,7 +84,7 @@ def _more_lines(console: code.InteractiveConsole, unicodetext: str) -> bool:
8484
src = _strip_final_indent(unicodetext)
8585
try:
8686
code = console.compile(src, "<stdin>", "single")
87-
except (OverflowError, SyntaxError, ValueError):
87+
except Exception:
8888
lines = src.splitlines(keepends=True)
8989
if len(lines) == 1:
9090
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
@@ -693,7 +693,7 @@ def execfile(self, filename, source=None):
693693
+ source + "\ndel __file__")
694694
try:
695695
code = compile(source, filename, "exec")
696-
except (OverflowError, SyntaxError):
696+
except Exception:
697697
self.tkconsole.resetoutput()
698698
print('*** Error in script or command!\n'
699699
'Traceback (most recent call last):',
@@ -743,19 +743,23 @@ def showsyntaxerror(self, filename=None, **kwargs):
743743
text = tkconsole.text
744744
text.tag_remove("ERROR", "1.0", "end")
745745
type, value, tb = sys.exc_info()
746-
msg = getattr(value, 'msg', '') or value or "<no detail available>"
747-
lineno = getattr(value, 'lineno', '') or 1
748-
offset = getattr(value, 'offset', '') or 0
746+
if not issubclass(type, SyntaxError):
747+
tkconsole.resetoutput()
748+
InteractiveInterpreter.showsyntaxerror(self, filename, **kwargs)
749+
tkconsole.showprompt()
750+
return
751+
msg = value.msg or "<no detail available>"
752+
lineno = value.lineno or 1
753+
offset = value.offset or 0
749754
if offset == 0:
750755
lineno += 1 #mark end of offending line
751756
if lineno == 1:
752-
pos = "iomark + %d chars" % (offset-1)
757+
pos = f"iomark + {offset-1} chars"
753758
else:
754-
pos = "iomark linestart + %d lines + %d chars" % \
755-
(lineno-1, offset-1)
759+
pos = f"iomark linestart + {lineno-1} lines + {offset-1} chars"
756760
tkconsole.colorize_syntax_error(text, pos)
757761
tkconsole.resetoutput()
758-
self.write("SyntaxError: %s\n" % msg)
762+
self.write(f"{type.__name__}: {msg}\n")
759763
tkconsole.showprompt()
760764

761765
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
@@ -151,7 +151,7 @@ def find_function(funcname, filename):
151151
if funcdef:
152152
try:
153153
code = compile(funcdef, filename, 'exec')
154-
except SyntaxError:
154+
except Exception:
155155
continue
156156
# We should always be able to find the code object here
157157
funccode = next(c for c in code.co_consts if
@@ -2572,7 +2572,7 @@ def _compile_error_message(self, expr):
25722572
"""Return the error message as string if compiling `expr` fails."""
25732573
try:
25742574
compile(expr, "<stdin>", "eval")
2575-
except SyntaxError as exc:
2575+
except Exception as exc:
25762576
return _rstr(self._format_exc(exc))
25772577
return ""
25782578

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