Skip to content

Commit 1e79bf6

Browse files
Locked-chess-officialpicnixzgpshead
authored
pythongh-139551: add support for BaseExceptionGroup in IDLE (pythonGH-139563)
Meaningfully render ExceptionGroup tracebacks in the IDLE GUI REPL. --------- Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> Co-authored-by: Gregory P. Smith <greg@krypto.org>
1 parent 3ab94d6 commit 1e79bf6

File tree

3 files changed

+174
-17
lines changed

3 files changed

+174
-17
lines changed

Lib/idlelib/idle_test/test_run.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,99 @@ def test_get_multiple_message(self, mock):
8282
subtests += 1
8383
self.assertEqual(subtests, len(data2)) # All subtests ran?
8484

85+
def _capture_exception(self):
86+
"""Call run.print_exception() and return its stderr output."""
87+
with captured_stderr() as output:
88+
with mock.patch.object(run, 'cleanup_traceback') as ct:
89+
ct.side_effect = lambda t, e: t
90+
run.print_exception()
91+
return output.getvalue()
92+
93+
@force_not_colorized
94+
def test_print_exception_group_nested(self):
95+
try:
96+
try:
97+
raise ExceptionGroup('inner', [ValueError('v1')])
98+
except ExceptionGroup as inner:
99+
raise ExceptionGroup('outer', [inner, TypeError('t1')])
100+
except ExceptionGroup:
101+
tb = self._capture_exception()
102+
103+
self.assertIn('ExceptionGroup: outer (2 sub-exceptions)', tb)
104+
self.assertIn('ExceptionGroup: inner', tb)
105+
self.assertIn('ValueError: v1', tb)
106+
self.assertIn('TypeError: t1', tb)
107+
# Verify tree structure characters.
108+
self.assertIn('+-+---------------- 1 ----------------', tb)
109+
self.assertIn('+---------------- 2 ----------------', tb)
110+
self.assertIn('+------------------------------------', tb)
111+
112+
@force_not_colorized
113+
def test_print_exception_group_chaining(self):
114+
# __cause__ on a sub-exception exercises the prefixed
115+
# chaining-message path (margin chars on separator lines).
116+
sub = TypeError('t1')
117+
sub.__cause__ = ValueError('original')
118+
try:
119+
raise ExceptionGroup('eg1', [sub])
120+
except ExceptionGroup:
121+
tb = self._capture_exception()
122+
self.assertIn('ValueError: original', tb)
123+
self.assertIn('| The above exception was the direct cause', tb)
124+
self.assertIn('ExceptionGroup: eg1', tb)
125+
126+
# __context__ (implicit chaining) on a sub-exception.
127+
sub = TypeError('t2')
128+
sub.__context__ = ValueError('first')
129+
try:
130+
raise ExceptionGroup('eg2', [sub])
131+
except ExceptionGroup:
132+
tb = self._capture_exception()
133+
self.assertIn('ValueError: first', tb)
134+
self.assertIn('| During handling of the above exception', tb)
135+
self.assertIn('ExceptionGroup: eg2', tb)
136+
137+
@force_not_colorized
138+
def test_print_exception_group_seen(self):
139+
shared = ValueError('shared')
140+
try:
141+
raise ExceptionGroup('eg', [shared, shared])
142+
except ExceptionGroup:
143+
tb = self._capture_exception()
144+
145+
self.assertIn('ValueError: shared', tb)
146+
self.assertIn('<exception ValueError has printed>', tb)
147+
148+
@force_not_colorized
149+
def test_print_exception_group_max_width(self):
150+
excs = [ValueError(f'v{i}') for i in range(20)]
151+
try:
152+
raise ExceptionGroup('eg', excs)
153+
except ExceptionGroup:
154+
tb = self._capture_exception()
155+
156+
self.assertIn('+---------------- 15 ----------------', tb)
157+
self.assertIn('+---------------- ... ----------------', tb)
158+
self.assertIn('and 5 more exceptions', tb)
159+
self.assertNotIn('+---------------- 16 ----------------', tb)
160+
161+
@force_not_colorized
162+
def test_print_exception_group_max_depth(self):
163+
def make_nested(depth):
164+
if depth == 0:
165+
return ValueError('leaf')
166+
return ExceptionGroup(f'level{depth}',
167+
[make_nested(depth - 1)])
168+
169+
try:
170+
raise make_nested(15)
171+
except ExceptionGroup:
172+
tb = self._capture_exception()
173+
174+
self.assertIn('... (max_group_depth is 10)', tb)
175+
self.assertIn('ExceptionGroup: level15', tb)
176+
self.assertNotIn('ValueError: leaf', tb)
177+
85178
# StdioFile tests.
86179

87180
class S(str):

Lib/idlelib/run.py

Lines changed: 80 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -249,31 +249,94 @@ def print_exception():
249249
sys.last_type, sys.last_value, sys.last_traceback = excinfo
250250
sys.last_exc = val
251251
seen = set()
252+
exclude = ("run.py", "rpc.py", "threading.py", "queue.py",
253+
"debugger_r.py", "bdb.py")
254+
max_group_width = 15
255+
max_group_depth = 10
256+
group_depth = 0
257+
258+
def print_exc_group(typ, exc, tb, prefix=""):
259+
nonlocal group_depth
260+
group_depth += 1
261+
prefix2 = prefix or " "
262+
if group_depth > max_group_depth:
263+
print(f"{prefix2}| ... (max_group_depth is {max_group_depth})",
264+
file=efile)
265+
group_depth -= 1
266+
return
267+
if tb:
268+
if not prefix:
269+
print(" + Exception Group Traceback (most recent call last):", file=efile)
270+
else:
271+
print(f"{prefix}| Exception Group Traceback (most recent call last):", file=efile)
272+
tbe = traceback.extract_tb(tb)
273+
cleanup_traceback(tbe, exclude)
274+
for line in traceback.format_list(tbe):
275+
for subline in line.rstrip().splitlines():
276+
print(f"{prefix2}| {subline}", file=efile)
277+
lines = get_message_lines(typ, exc, tb)
278+
for line in lines:
279+
print(f"{prefix2}| {line}", end="", file=efile)
280+
num_excs = len(exc.exceptions)
281+
if num_excs <= max_group_width:
282+
n = num_excs
283+
else:
284+
n = max_group_width + 1
285+
for i, sub in enumerate(exc.exceptions[:n], 1):
286+
truncated = (i > max_group_width)
287+
first_line_pre = "+-" if i == 1 else " "
288+
title = str(i) if not truncated else '...'
289+
print(f"{prefix2}{first_line_pre}+---------------- {title} ----------------", file=efile)
290+
if truncated:
291+
remaining = num_excs - max_group_width
292+
plural = 's' if remaining > 1 else ''
293+
print(f"{prefix2} | and {remaining} more exception{plural}",
294+
file=efile)
295+
need_print_underline = True
296+
elif id(sub) not in seen:
297+
if not prefix:
298+
print_exc(type(sub), sub, sub.__traceback__, " ")
299+
else:
300+
print_exc(type(sub), sub, sub.__traceback__, prefix + " ")
301+
need_print_underline = not isinstance(sub, BaseExceptionGroup)
302+
else:
303+
print(f"{prefix2} | <exception {type(sub).__name__} has printed>", file=efile)
304+
need_print_underline = True
305+
if need_print_underline and i == n:
306+
print(f"{prefix2} +------------------------------------", file=efile)
307+
group_depth -= 1
252308

253-
def print_exc(typ, exc, tb):
309+
def print_exc(typ, exc, tb, prefix=""):
254310
seen.add(id(exc))
255311
context = exc.__context__
256312
cause = exc.__cause__
313+
prefix2 = f"{prefix}| " if prefix else ""
257314
if cause is not None and id(cause) not in seen:
258-
print_exc(type(cause), cause, cause.__traceback__)
259-
print("\nThe above exception was the direct cause "
260-
"of the following exception:\n", file=efile)
315+
print_exc(type(cause), cause, cause.__traceback__, prefix)
316+
print(f"{prefix2}\n{prefix2}The above exception was the direct cause "
317+
f"of the following exception:\n{prefix2}", file=efile)
261318
elif (context is not None and
262319
not exc.__suppress_context__ and
263320
id(context) not in seen):
264-
print_exc(type(context), context, context.__traceback__)
265-
print("\nDuring handling of the above exception, "
266-
"another exception occurred:\n", file=efile)
267-
if tb:
268-
tbe = traceback.extract_tb(tb)
269-
print('Traceback (most recent call last):', file=efile)
270-
exclude = ("run.py", "rpc.py", "threading.py", "queue.py",
271-
"debugger_r.py", "bdb.py")
272-
cleanup_traceback(tbe, exclude)
273-
traceback.print_list(tbe, file=efile)
274-
lines = get_message_lines(typ, exc, tb)
275-
for line in lines:
276-
print(line, end='', file=efile)
321+
print_exc(type(context), context, context.__traceback__, prefix)
322+
print(f"{prefix2}\n{prefix2}During handling of the above exception, "
323+
f"another exception occurred:\n{prefix2}", file=efile)
324+
if isinstance(exc, BaseExceptionGroup):
325+
print_exc_group(typ, exc, tb, prefix=prefix)
326+
else:
327+
if tb:
328+
print(f"{prefix2}Traceback (most recent call last):", file=efile)
329+
tbe = traceback.extract_tb(tb)
330+
cleanup_traceback(tbe, exclude)
331+
if prefix:
332+
for line in traceback.format_list(tbe):
333+
for subline in line.rstrip().splitlines():
334+
print(f"{prefix}| {subline}", file=efile)
335+
else:
336+
traceback.print_list(tbe, file=efile)
337+
lines = get_message_lines(typ, exc, tb)
338+
for line in lines:
339+
print(f"{prefix2}{line}", end="", file=efile)
277340

278341
print_exc(typ, val, tb)
279342

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Support rendering :exc:`BaseExceptionGroup` in IDLE.

0 commit comments

Comments
 (0)