0
0
mirror of https://github.com/python/cpython.git synced 2024-11-24 00:38:00 +01:00

bpo-45292: [PEP 654] Update traceback display code to work with exception groups (GH-29207)

This commit is contained in:
Irit Katriel 2021-11-05 09:39:18 +00:00 committed by GitHub
parent e52f9bee80
commit 3509b26c91
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 1017 additions and 87 deletions

View File

@ -87,6 +87,17 @@ PyAPI_FUNC(PyObject*) _PyTraceBack_FromFrame(
PyObject *tb_next,
PyFrameObject *frame);
#define EXCEPTION_TB_HEADER "Traceback (most recent call last):\n"
#define EXCEPTION_GROUP_TB_HEADER "Exception Group Traceback (most recent call last):\n"
/* Write the traceback tb to file f. Prefix each line with
indent spaces followed by the margin (if it is not NULL). */
PyAPI_FUNC(int) _PyTraceBack_Print_Indented(
PyObject *tb, int indent, const char* margin,
const char *header_margin, const char *header, PyObject *f);
PyAPI_FUNC(int) _Py_WriteIndentedMargin(int, const char*, PyObject *);
PyAPI_FUNC(int) _Py_WriteIndent(int, PyObject *);
#ifdef __cplusplus
}
#endif

View File

@ -987,6 +987,35 @@ class TracebackFormatTests(unittest.TestCase):
self.assertIn('UnhashableException: ex2', tb[4])
self.assertIn('UnhashableException: ex1', tb[12])
def deep_eg(self):
e = TypeError(1)
for i in range(2000):
e = ExceptionGroup('eg', [e])
return e
@cpython_only
def test_exception_group_deep_recursion_capi(self):
from _testcapi import exception_print
LIMIT = 75
eg = self.deep_eg()
with captured_output("stderr") as stderr_f:
with support.infinite_recursion(max_depth=LIMIT):
exception_print(eg)
output = stderr_f.getvalue()
self.assertIn('ExceptionGroup', output)
self.assertLessEqual(output.count('ExceptionGroup'), LIMIT)
def test_exception_group_deep_recursion_traceback(self):
LIMIT = 75
eg = self.deep_eg()
with captured_output("stderr") as stderr_f:
with support.infinite_recursion(max_depth=LIMIT):
traceback.print_exception(type(eg), eg, eg.__traceback__)
output = stderr_f.getvalue()
self.assertIn('ExceptionGroup', output)
self.assertLessEqual(output.count('ExceptionGroup'), LIMIT)
cause_message = (
"\nThe above exception was the direct cause "
"of the following exception:\n\n")
@ -998,7 +1027,6 @@ context_message = (
boundaries = re.compile(
'(%s|%s)' % (re.escape(cause_message), re.escape(context_message)))
class BaseExceptionReportingTests:
def get_exception(self, exception_or_callable):
@ -1009,6 +1037,8 @@ class BaseExceptionReportingTests:
except Exception as e:
return e
callable_line = get_exception.__code__.co_firstlineno + 4
def zero_div(self):
1/0 # In zero_div
@ -1234,6 +1264,298 @@ class BaseExceptionReportingTests:
self.assertEqual(err, f"{str_name}: {str_value}\n")
# #### Exception Groups ####
def test_exception_group_basic(self):
def exc():
raise ExceptionGroup("eg", [ValueError(1), TypeError(2)])
expected = (
f' + Exception Group Traceback (most recent call last):\n'
f' | File "{__file__}", line {self.callable_line}, in get_exception\n'
f' | exception_or_callable()\n'
f' | ^^^^^^^^^^^^^^^^^^^^^^^\n'
f' | File "{__file__}", line {exc.__code__.co_firstlineno + 1}, in exc\n'
f' | raise ExceptionGroup("eg", [ValueError(1), TypeError(2)])\n'
f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n'
f' | ExceptionGroup: eg\n'
f' +-+---------------- 1 ----------------\n'
f' | ValueError: 1\n'
f' +---------------- 2 ----------------\n'
f' | TypeError: 2\n'
f' +------------------------------------\n')
report = self.get_report(exc)
self.assertEqual(report, expected)
def test_exception_group_cause(self):
def exc():
EG = ExceptionGroup
try:
raise EG("eg1", [ValueError(1), TypeError(2)])
except Exception as e:
raise EG("eg2", [ValueError(3), TypeError(4)]) from e
expected = (f' + Exception Group Traceback (most recent call last):\n'
f' | File "{__file__}", line {exc.__code__.co_firstlineno + 3}, in exc\n'
f' | raise EG("eg1", [ValueError(1), TypeError(2)])\n'
f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n'
f' | ExceptionGroup: eg1\n'
f' +-+---------------- 1 ----------------\n'
f' | ValueError: 1\n'
f' +---------------- 2 ----------------\n'
f' | TypeError: 2\n'
f' +------------------------------------\n'
f'\n'
f'The above exception was the direct cause of the following exception:\n'
f'\n'
f' + Exception Group Traceback (most recent call last):\n'
f' | File "{__file__}", line {self.callable_line}, in get_exception\n'
f' | exception_or_callable()\n'
f' | ^^^^^^^^^^^^^^^^^^^^^^^\n'
f' | File "{__file__}", line {exc.__code__.co_firstlineno + 5}, in exc\n'
f' | raise EG("eg2", [ValueError(3), TypeError(4)]) from e\n'
f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n'
f' | ExceptionGroup: eg2\n'
f' +-+---------------- 1 ----------------\n'
f' | ValueError: 3\n'
f' +---------------- 2 ----------------\n'
f' | TypeError: 4\n'
f' +------------------------------------\n')
report = self.get_report(exc)
self.assertEqual(report, expected)
def test_exception_group_context_with_context(self):
def exc():
EG = ExceptionGroup
try:
try:
raise EG("eg1", [ValueError(1), TypeError(2)])
except:
raise EG("eg2", [ValueError(3), TypeError(4)])
except:
raise ImportError(5)
expected = (
f' + Exception Group Traceback (most recent call last):\n'
f' | File "{__file__}", line {exc.__code__.co_firstlineno + 4}, in exc\n'
f' | raise EG("eg1", [ValueError(1), TypeError(2)])\n'
f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n'
f' | ExceptionGroup: eg1\n'
f' +-+---------------- 1 ----------------\n'
f' | ValueError: 1\n'
f' +---------------- 2 ----------------\n'
f' | TypeError: 2\n'
f' +------------------------------------\n'
f'\n'
f'During handling of the above exception, another exception occurred:\n'
f'\n'
f' + Exception Group Traceback (most recent call last):\n'
f' | File "{__file__}", line {exc.__code__.co_firstlineno + 6}, in exc\n'
f' | raise EG("eg2", [ValueError(3), TypeError(4)])\n'
f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n'
f' | ExceptionGroup: eg2\n'
f' +-+---------------- 1 ----------------\n'
f' | ValueError: 3\n'
f' +---------------- 2 ----------------\n'
f' | TypeError: 4\n'
f' +------------------------------------\n'
f'\n'
f'During handling of the above exception, another exception occurred:\n'
f'\n'
f'Traceback (most recent call last):\n'
f' File "{__file__}", line {self.callable_line}, in get_exception\n'
f' exception_or_callable()\n'
f' ^^^^^^^^^^^^^^^^^^^^^^^\n'
f' File "{__file__}", line {exc.__code__.co_firstlineno + 8}, in exc\n'
f' raise ImportError(5)\n'
f' ^^^^^^^^^^^^^^^^^^^^\n'
f'ImportError: 5\n')
report = self.get_report(exc)
self.assertEqual(report, expected)
def test_exception_group_nested(self):
def exc():
EG = ExceptionGroup
VE = ValueError
TE = TypeError
try:
try:
raise EG("nested", [TE(2), TE(3)])
except Exception as e:
exc = e
raise EG("eg", [VE(1), exc, VE(4)])
except:
raise EG("top", [VE(5)])
expected = (f' + Exception Group Traceback (most recent call last):\n'
f' | File "{__file__}", line {exc.__code__.co_firstlineno + 9}, in exc\n'
f' | raise EG("eg", [VE(1), exc, VE(4)])\n'
f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n'
f' | ExceptionGroup: eg\n'
f' +-+---------------- 1 ----------------\n'
f' | ValueError: 1\n'
f' +---------------- 2 ----------------\n'
f' | Exception Group Traceback (most recent call last):\n'
f' | File "{__file__}", line {exc.__code__.co_firstlineno + 6}, in exc\n'
f' | raise EG("nested", [TE(2), TE(3)])\n'
f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n'
f' | ExceptionGroup: nested\n'
f' +-+---------------- 1 ----------------\n'
f' | TypeError: 2\n'
f' +---------------- 2 ----------------\n'
f' | TypeError: 3\n'
f' +------------------------------------\n'
f' +---------------- 3 ----------------\n'
f' | ValueError: 4\n'
f' +------------------------------------\n'
f'\n'
f'During handling of the above exception, another exception occurred:\n'
f'\n'
f' + Exception Group Traceback (most recent call last):\n'
f' | File "{__file__}", line {self.callable_line}, in get_exception\n'
f' | exception_or_callable()\n'
f' | ^^^^^^^^^^^^^^^^^^^^^^^\n'
f' | File "{__file__}", line {exc.__code__.co_firstlineno + 11}, in exc\n'
f' | raise EG("top", [VE(5)])\n'
f' | ^^^^^^^^^^^^^^^^^^^^^^^^\n'
f' | ExceptionGroup: top\n'
f' +-+---------------- 1 ----------------\n'
f' | ValueError: 5\n'
f' +------------------------------------\n')
report = self.get_report(exc)
self.assertEqual(report, expected)
def test_exception_group_width_limit(self):
excs = []
for i in range(1000):
excs.append(ValueError(i))
eg = ExceptionGroup('eg', excs)
expected = (' | ExceptionGroup: eg\n'
' +-+---------------- 1 ----------------\n'
' | ValueError: 0\n'
' +---------------- 2 ----------------\n'
' | ValueError: 1\n'
' +---------------- 3 ----------------\n'
' | ValueError: 2\n'
' +---------------- 4 ----------------\n'
' | ValueError: 3\n'
' +---------------- 5 ----------------\n'
' | ValueError: 4\n'
' +---------------- 6 ----------------\n'
' | ValueError: 5\n'
' +---------------- 7 ----------------\n'
' | ValueError: 6\n'
' +---------------- 8 ----------------\n'
' | ValueError: 7\n'
' +---------------- 9 ----------------\n'
' | ValueError: 8\n'
' +---------------- 10 ----------------\n'
' | ValueError: 9\n'
' +---------------- 11 ----------------\n'
' | ValueError: 10\n'
' +---------------- 12 ----------------\n'
' | ValueError: 11\n'
' +---------------- 13 ----------------\n'
' | ValueError: 12\n'
' +---------------- 14 ----------------\n'
' | ValueError: 13\n'
' +---------------- 15 ----------------\n'
' | ValueError: 14\n'
' +---------------- ... ----------------\n'
' | and 985 more exceptions\n'
' +------------------------------------\n')
report = self.get_report(eg)
self.assertEqual(report, expected)
def test_exception_group_depth_limit(self):
exc = TypeError('bad type')
for i in range(1000):
exc = ExceptionGroup(
f'eg{i}',
[ValueError(i), exc, ValueError(-i)])
expected = (' | ExceptionGroup: eg999\n'
' +-+---------------- 1 ----------------\n'
' | ValueError: 999\n'
' +---------------- 2 ----------------\n'
' | ExceptionGroup: eg998\n'
' +-+---------------- 1 ----------------\n'
' | ValueError: 998\n'
' +---------------- 2 ----------------\n'
' | ExceptionGroup: eg997\n'
' +-+---------------- 1 ----------------\n'
' | ValueError: 997\n'
' +---------------- 2 ----------------\n'
' | ExceptionGroup: eg996\n'
' +-+---------------- 1 ----------------\n'
' | ValueError: 996\n'
' +---------------- 2 ----------------\n'
' | ExceptionGroup: eg995\n'
' +-+---------------- 1 ----------------\n'
' | ValueError: 995\n'
' +---------------- 2 ----------------\n'
' | ExceptionGroup: eg994\n'
' +-+---------------- 1 ----------------\n'
' | ValueError: 994\n'
' +---------------- 2 ----------------\n'
' | ExceptionGroup: eg993\n'
' +-+---------------- 1 ----------------\n'
' | ValueError: 993\n'
' +---------------- 2 ----------------\n'
' | ExceptionGroup: eg992\n'
' +-+---------------- 1 ----------------\n'
' | ValueError: 992\n'
' +---------------- 2 ----------------\n'
' | ExceptionGroup: eg991\n'
' +-+---------------- 1 ----------------\n'
' | ValueError: 991\n'
' +---------------- 2 ----------------\n'
' | ExceptionGroup: eg990\n'
' +-+---------------- 1 ----------------\n'
' | ValueError: 990\n'
' +---------------- 2 ----------------\n'
' | ... (max_group_depth is 10)\n'
' +---------------- 3 ----------------\n'
' | ValueError: -990\n'
' +------------------------------------\n'
' +---------------- 3 ----------------\n'
' | ValueError: -991\n'
' +------------------------------------\n'
' +---------------- 3 ----------------\n'
' | ValueError: -992\n'
' +------------------------------------\n'
' +---------------- 3 ----------------\n'
' | ValueError: -993\n'
' +------------------------------------\n'
' +---------------- 3 ----------------\n'
' | ValueError: -994\n'
' +------------------------------------\n'
' +---------------- 3 ----------------\n'
' | ValueError: -995\n'
' +------------------------------------\n'
' +---------------- 3 ----------------\n'
' | ValueError: -996\n'
' +------------------------------------\n'
' +---------------- 3 ----------------\n'
' | ValueError: -997\n'
' +------------------------------------\n'
' +---------------- 3 ----------------\n'
' | ValueError: -998\n'
' +------------------------------------\n'
' +---------------- 3 ----------------\n'
' | ValueError: -999\n'
' +------------------------------------\n')
report = self.get_report(exc)
self.assertEqual(report, expected)
class PyExcReportingTests(BaseExceptionReportingTests, unittest.TestCase):
#
# This checks reporting through the 'traceback' module, with both
@ -1913,6 +2235,197 @@ class TestTracebackException(unittest.TestCase):
''])
class TestTracebackException_ExceptionGroups(unittest.TestCase):
def setUp(self):
super().setUp()
self.eg_info = self._get_exception_group()
def _get_exception_group(self):
def f():
1/0
def g(v):
raise ValueError(v)
self.lno_f = f.__code__.co_firstlineno
self.lno_g = g.__code__.co_firstlineno
try:
try:
try:
f()
except Exception as e:
exc1 = e
try:
g(42)
except Exception as e:
exc2 = e
raise ExceptionGroup("eg1", [exc1, exc2])
except ExceptionGroup as e:
exc3 = e
try:
g(24)
except Exception as e:
exc4 = e
raise ExceptionGroup("eg2", [exc3, exc4])
except ExceptionGroup:
return sys.exc_info()
self.fail('Exception Not Raised')
def test_exception_group_construction(self):
eg_info = self.eg_info
teg1 = traceback.TracebackException(*eg_info)
teg2 = traceback.TracebackException.from_exception(eg_info[1])
self.assertIsNot(teg1, teg2)
self.assertEqual(teg1, teg2)
def test_exception_group_format_exception_only(self):
teg = traceback.TracebackException(*self.eg_info)
formatted = ''.join(teg.format_exception_only()).split('\n')
expected = "ExceptionGroup: eg2\n".split('\n')
self.assertEqual(formatted, expected)
def test_exception_group_format(self):
teg = traceback.TracebackException(*self.eg_info)
formatted = ''.join(teg.format()).split('\n')
lno_f = self.lno_f
lno_g = self.lno_g
expected = [
f' + Exception Group Traceback (most recent call last):',
f' | File "{__file__}", line {lno_g+23}, in _get_exception_group',
f' | raise ExceptionGroup("eg2", [exc3, exc4])',
f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^',
f' | ExceptionGroup: eg2',
f' +-+---------------- 1 ----------------',
f' | Exception Group Traceback (most recent call last):',
f' | File "{__file__}", line {lno_g+16}, in _get_exception_group',
f' | raise ExceptionGroup("eg1", [exc1, exc2])',
f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^',
f' | ExceptionGroup: eg1',
f' +-+---------------- 1 ----------------',
f' | Traceback (most recent call last):',
f' | File "{__file__}", line {lno_g+9}, in _get_exception_group',
f' | f()',
f' | ^^^',
f' | File "{__file__}", line {lno_f+1}, in f',
f' | 1/0',
f' | ~^~',
f' | ZeroDivisionError: division by zero',
f' +---------------- 2 ----------------',
f' | Traceback (most recent call last):',
f' | File "{__file__}", line {lno_g+13}, in _get_exception_group',
f' | g(42)',
f' | ^^^^^',
f' | File "{__file__}", line {lno_g+1}, in g',
f' | raise ValueError(v)',
f' | ^^^^^^^^^^^^^^^^^^^',
f' | ValueError: 42',
f' +------------------------------------',
f' +---------------- 2 ----------------',
f' | Traceback (most recent call last):',
f' | File "{__file__}", line {lno_g+20}, in _get_exception_group',
f' | g(24)',
f' | ^^^^^',
f' | File "{__file__}", line {lno_g+1}, in g',
f' | raise ValueError(v)',
f' | ^^^^^^^^^^^^^^^^^^^',
f' | ValueError: 24',
f' +------------------------------------',
f'']
self.assertEqual(formatted, expected)
def test_max_group_width(self):
excs1 = []
excs2 = []
for i in range(3):
excs1.append(ValueError(i))
for i in range(10):
excs2.append(TypeError(i))
EG = ExceptionGroup
eg = EG('eg', [EG('eg1', excs1), EG('eg2', excs2)])
teg = traceback.TracebackException.from_exception(eg, max_group_width=2)
formatted = ''.join(teg.format()).split('\n')
expected = [
f' | ExceptionGroup: eg',
f' +-+---------------- 1 ----------------',
f' | ExceptionGroup: eg1',
f' +-+---------------- 1 ----------------',
f' | ValueError: 0',
f' +---------------- 2 ----------------',
f' | ValueError: 1',
f' +---------------- ... ----------------',
f' | and 1 more exception',
f' +------------------------------------',
f' +---------------- 2 ----------------',
f' | ExceptionGroup: eg2',
f' +-+---------------- 1 ----------------',
f' | TypeError: 0',
f' +---------------- 2 ----------------',
f' | TypeError: 1',
f' +---------------- ... ----------------',
f' | and 8 more exceptions',
f' +------------------------------------',
f'']
self.assertEqual(formatted, expected)
def test_max_group_depth(self):
exc = TypeError('bad type')
for i in range(3):
exc = ExceptionGroup('exc', [ValueError(-i), exc, ValueError(i)])
teg = traceback.TracebackException.from_exception(exc, max_group_depth=2)
formatted = ''.join(teg.format()).split('\n')
expected = [
f' | ExceptionGroup: exc',
f' +-+---------------- 1 ----------------',
f' | ValueError: -2',
f' +---------------- 2 ----------------',
f' | ExceptionGroup: exc',
f' +-+---------------- 1 ----------------',
f' | ValueError: -1',
f' +---------------- 2 ----------------',
f' | ... (max_group_depth is 2)',
f' +---------------- 3 ----------------',
f' | ValueError: 1',
f' +------------------------------------',
f' +---------------- 3 ----------------',
f' | ValueError: 2',
f' +------------------------------------',
f'']
self.assertEqual(formatted, expected)
def test_comparison(self):
try:
raise self.eg_info[1]
except ExceptionGroup:
exc_info = sys.exc_info()
for _ in range(5):
try:
raise exc_info[1]
except:
exc_info = sys.exc_info()
exc = traceback.TracebackException(*exc_info)
exc2 = traceback.TracebackException(*exc_info)
exc3 = traceback.TracebackException(*exc_info, limit=300)
ne = traceback.TracebackException(*exc_info, limit=3)
self.assertIsNot(exc, exc2)
self.assertEqual(exc, exc2)
self.assertEqual(exc, exc3)
self.assertNotEqual(exc, ne)
self.assertNotEqual(exc, object())
self.assertEqual(exc, ALWAYS_EQ)
class MiscTest(unittest.TestCase):
def test_all(self):

View File

@ -4,6 +4,7 @@ import collections
import itertools
import linecache
import sys
import textwrap
from contextlib import suppress
__all__ = ['extract_stack', 'extract_tb', 'format_exception',
@ -601,6 +602,29 @@ def _extract_caret_anchors_from_line_segment(segment):
return None
class _ExceptionPrintContext:
def __init__(self):
self.seen = set()
self.exception_group_depth = 0
self.need_close = False
def indent(self):
return ' ' * (2 * self.exception_group_depth)
def emit(self, text_gen, margin_char=None):
if margin_char is None:
margin_char = '|'
indent_str = self.indent()
if self.exception_group_depth:
indent_str += margin_char + ' '
if isinstance(text_gen, str):
yield textwrap.indent(text_gen, indent_str, lambda line: True)
else:
for text in text_gen:
yield textwrap.indent(text, indent_str, lambda line: True)
class TracebackException:
"""An exception ready for rendering.
@ -608,6 +632,11 @@ class TracebackException:
to this intermediary form to ensure that no references are held, while
still being able to fully print or format it.
max_group_width and max_group_depth control the formatting of exception
groups. The depth refers to the nesting level of the group, and the width
refers to the size of a single exception group's exceptions array. The
formatted output is truncated when either limit is exceeded.
Use `from_exception` to create TracebackException instances from exception
objects, or the constructor to create TracebackException instances from
individual components.
@ -635,7 +664,7 @@ class TracebackException:
def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
lookup_lines=True, capture_locals=False, compact=False,
_seen=None):
max_group_width=15, max_group_depth=10, _seen=None):
# NB: we need to accept exc_traceback, exc_value, exc_traceback to
# permit backwards compat with the existing API, otherwise we
# need stub thunk objects just to glue it together.
@ -645,7 +674,9 @@ class TracebackException:
_seen = set()
_seen.add(id(exc_value))
# TODO: locals.
self.max_group_width = max_group_width
self.max_group_depth = max_group_depth
self.stack = StackSummary._extract_from_extended_frame_gen(
_walk_tb_with_full_positions(exc_traceback),
limit=limit, lookup_lines=lookup_lines,
@ -685,6 +716,8 @@ class TracebackException:
limit=limit,
lookup_lines=lookup_lines,
capture_locals=capture_locals,
max_group_width=max_group_width,
max_group_depth=max_group_depth,
_seen=_seen)
else:
cause = None
@ -704,15 +737,38 @@ class TracebackException:
limit=limit,
lookup_lines=lookup_lines,
capture_locals=capture_locals,
max_group_width=max_group_width,
max_group_depth=max_group_depth,
_seen=_seen)
else:
context = None
if e and isinstance(e, BaseExceptionGroup):
exceptions = []
for exc in e.exceptions:
texc = TracebackException(
type(exc),
exc,
exc.__traceback__,
limit=limit,
lookup_lines=lookup_lines,
capture_locals=capture_locals,
max_group_width=max_group_width,
max_group_depth=max_group_depth,
_seen=_seen)
exceptions.append(texc)
else:
exceptions = None
te.__cause__ = cause
te.__context__ = context
te.exceptions = exceptions
if cause:
queue.append((te.__cause__, e.__cause__))
if context:
queue.append((te.__context__, e.__context__))
if exceptions:
queue.extend(zip(te.exceptions, e.exceptions))
@classmethod
def from_exception(cls, exc, *args, **kwargs):
@ -795,7 +851,7 @@ class TracebackException:
msg = self.msg or "<no detail available>"
yield "{}: {}{}\n".format(stype, msg, filename_suffix)
def format(self, *, chain=True):
def format(self, *, chain=True, _ctx=None):
"""Format the exception.
If chain is not *True*, *__cause__* and *__context__* will not be formatted.
@ -808,10 +864,13 @@ class TracebackException:
string in the output.
"""
if _ctx is None:
_ctx = _ExceptionPrintContext()
output = []
exc = self
while exc:
if chain:
if chain:
while exc:
if exc.__cause__ is not None:
chained_msg = _cause_message
chained_exc = exc.__cause__
@ -825,17 +884,73 @@ class TracebackException:
output.append((chained_msg, exc))
exc = chained_exc
else:
output.append((None, exc))
exc = None
else:
output.append((None, exc))
for msg, exc in reversed(output):
if msg is not None:
yield msg
if exc.stack:
yield 'Traceback (most recent call last):\n'
yield from exc.stack.format()
yield from exc.format_exception_only()
yield from _ctx.emit(msg)
if exc.exceptions is None:
if exc.stack:
yield from _ctx.emit('Traceback (most recent call last):\n')
yield from _ctx.emit(exc.stack.format())
yield from _ctx.emit(exc.format_exception_only())
elif _ctx.exception_group_depth > self.max_group_depth:
# exception group, but depth exceeds limit
yield from _ctx.emit(
f"... (max_group_depth is {self.max_group_depth})\n")
else:
# format exception group
is_toplevel = (_ctx.exception_group_depth == 0)
if is_toplevel:
_ctx.exception_group_depth += 1
if exc.stack:
yield from _ctx.emit(
'Exception Group Traceback (most recent call last):\n',
margin_char = '+' if is_toplevel else None)
yield from _ctx.emit(exc.stack.format())
yield from _ctx.emit(exc.format_exception_only())
num_excs = len(exc.exceptions)
if num_excs <= self.max_group_width:
n = num_excs
else:
n = self.max_group_width + 1
_ctx.need_close = False
for i in range(n):
last_exc = (i == n-1)
if last_exc:
# The closing frame may be added by a recursive call
_ctx.need_close = True
if self.max_group_width is not None:
truncated = (i >= self.max_group_width)
else:
truncated = False
title = f'{i+1}' if not truncated else '...'
yield (_ctx.indent() +
('+-' if i==0 else ' ') +
f'+---------------- {title} ----------------\n')
_ctx.exception_group_depth += 1
if not truncated:
yield from exc.exceptions[i].format(chain=chain, _ctx=_ctx)
else:
remaining = num_excs - self.max_group_width
plural = 's' if remaining > 1 else ''
yield from _ctx.emit(
f"and {remaining} more exception{plural}\n")
if last_exc and _ctx.need_close:
yield (_ctx.indent() +
"+------------------------------------\n")
_ctx.need_close = False
_ctx.exception_group_depth -= 1
if is_toplevel:
assert _ctx.exception_group_depth == 1
_ctx.exception_group_depth = 0
def print(self, *, file=None, chain=True):
"""Print the result of self.format(chain=chain) to 'file'."""

View File

@ -1 +1 @@
Implement :pep:`654` Add :class:`ExceptionGroup` and :class:`BaseExceptionGroup`.
Implement :pep:`654`. Add :class:`ExceptionGroup` and :class:`BaseExceptionGroup`. Update traceback display code.

View File

@ -8,6 +8,8 @@
/* TODO: Cull includes following phase split */
#include <stdbool.h>
#include "Python.h"
#include "pycore_ast.h" // PyAST_mod2obj
@ -19,6 +21,7 @@
#include "pycore_pylifecycle.h" // _Py_UnhandledKeyboardInterrupt
#include "pycore_pystate.h" // _PyInterpreterState_GET()
#include "pycore_sysmodule.h" // _PySys_Audit()
#include "pycore_traceback.h" // _PyTraceBack_Print_Indented()
#include "token.h" // INDENT
#include "errcode.h" // E_EOF
@ -886,19 +889,50 @@ PyErr_Print(void)
PyErr_PrintEx(1);
}
struct exception_print_context
{
PyObject *file;
PyObject *seen; // Prevent cycles in recursion
int exception_group_depth; // nesting level of current exception group
bool need_close; // Need a closing bottom frame
int max_group_width; // Maximum number of children of each EG
int max_group_depth; // Maximum nesting level of EGs
};
#define EXC_MARGIN(ctx) ((ctx)->exception_group_depth ? "| " : "")
#define EXC_INDENT(ctx) (2 * (ctx)->exception_group_depth)
static int
write_indented_margin(struct exception_print_context *ctx, PyObject *f)
{
return _Py_WriteIndentedMargin(EXC_INDENT(ctx), EXC_MARGIN(ctx), f);
}
static void
print_exception(PyObject *f, PyObject *value)
print_exception(struct exception_print_context *ctx, PyObject *value)
{
int err = 0;
PyObject *type, *tb, *tmp;
PyObject *f = ctx->file;
_Py_IDENTIFIER(print_file_and_line);
if (!PyExceptionInstance_Check(value)) {
err = PyFile_WriteString("TypeError: print_exception(): Exception expected for value, ", f);
err += PyFile_WriteString(Py_TYPE(value)->tp_name, f);
err += PyFile_WriteString(" found\n", f);
if (err)
if (err == 0) {
err = _Py_WriteIndent(EXC_INDENT(ctx), f);
}
if (err == 0) {
err = PyFile_WriteString("TypeError: print_exception(): Exception expected for value, ", f);
}
if (err == 0) {
err = PyFile_WriteString(Py_TYPE(value)->tp_name, f);
}
if (err == 0) {
err = PyFile_WriteString(" found\n", f);
}
if (err != 0) {
PyErr_Clear();
}
return;
}
@ -906,8 +940,18 @@ print_exception(PyObject *f, PyObject *value)
fflush(stdout);
type = (PyObject *) Py_TYPE(value);
tb = PyException_GetTraceback(value);
if (tb && tb != Py_None)
err = PyTraceBack_Print(tb, f);
if (tb && tb != Py_None) {
const char *header = EXCEPTION_TB_HEADER;
const char *header_margin = EXC_MARGIN(ctx);
if (_PyBaseExceptionGroup_Check(value)) {
header = EXCEPTION_GROUP_TB_HEADER;
if (ctx->exception_group_depth == 1) {
header_margin = "+ ";
}
}
err = _PyTraceBack_Print_Indented(
tb, EXC_INDENT(ctx), EXC_MARGIN(ctx), header_margin, header, f);
}
if (err == 0 &&
(err = _PyObject_LookupAttrId(value, &PyId_print_file_and_line, &tmp)) > 0)
{
@ -917,8 +961,9 @@ print_exception(PyObject *f, PyObject *value)
Py_DECREF(tmp);
if (!parse_syntax_error(value, &message, &filename,
&lineno, &offset,
&end_lineno, &end_offset, &text))
&end_lineno, &end_offset, &text)) {
PyErr_Clear();
}
else {
PyObject *line;
@ -929,7 +974,10 @@ print_exception(PyObject *f, PyObject *value)
filename, lineno);
Py_DECREF(filename);
if (line != NULL) {
PyFile_WriteObject(line, f, Py_PRINT_RAW);
err = write_indented_margin(ctx, f);
if (err == 0) {
err = PyFile_WriteObject(line, f, Py_PRINT_RAW);
}
Py_DECREF(line);
}
@ -958,7 +1006,7 @@ print_exception(PyObject *f, PyObject *value)
err = -1;
}
}
if (err) {
if (err != 0) {
/* Don't do anything else */
}
else {
@ -967,21 +1015,26 @@ print_exception(PyObject *f, PyObject *value)
_Py_IDENTIFIER(__module__);
assert(PyExceptionClass_Check(type));
modulename = _PyObject_GetAttrId(type, &PyId___module__);
if (modulename == NULL || !PyUnicode_Check(modulename))
{
Py_XDECREF(modulename);
PyErr_Clear();
err = PyFile_WriteString("<unknown>", f);
}
else {
if (!_PyUnicode_EqualToASCIIId(modulename, &PyId_builtins) &&
!_PyUnicode_EqualToASCIIId(modulename, &PyId___main__))
err = write_indented_margin(ctx, f);
if (err == 0) {
modulename = _PyObject_GetAttrId(type, &PyId___module__);
if (modulename == NULL || !PyUnicode_Check(modulename))
{
err = PyFile_WriteObject(modulename, f, Py_PRINT_RAW);
err += PyFile_WriteString(".", f);
Py_XDECREF(modulename);
PyErr_Clear();
err = PyFile_WriteString("<unknown>", f);
}
else {
if (!_PyUnicode_EqualToASCIIId(modulename, &PyId_builtins) &&
!_PyUnicode_EqualToASCIIId(modulename, &PyId___main__))
{
err = PyFile_WriteObject(modulename, f, Py_PRINT_RAW);
if (err == 0) {
err = PyFile_WriteString(".", f);
}
}
Py_DECREF(modulename);
}
Py_DECREF(modulename);
}
if (err == 0) {
PyObject* qualname = PyType_GetQualName((PyTypeObject *)type);
@ -1039,26 +1092,67 @@ print_exception(PyObject *f, PyObject *value)
}
static const char cause_message[] =
"\nThe above exception was the direct cause "
"of the following exception:\n\n";
"The above exception was the direct cause "
"of the following exception:\n";
static const char context_message[] =
"\nDuring handling of the above exception, "
"another exception occurred:\n\n";
"During handling of the above exception, "
"another exception occurred:\n";
static void
print_exception_recursive(PyObject *f, PyObject *value, PyObject *seen)
print_exception_recursive(struct exception_print_context*, PyObject*);
static int
print_chained(struct exception_print_context* ctx, PyObject *value,
const char * message, const char *tag)
{
PyObject *f = ctx->file;
bool need_close = ctx->need_close;
int err = Py_EnterRecursiveCall(" in print_chained");
if (err == 0) {
print_exception_recursive(ctx, value);
Py_LeaveRecursiveCall();
if (err == 0) {
err = write_indented_margin(ctx, f);
}
if (err == 0) {
err = PyFile_WriteString("\n", f);
}
if (err == 0) {
err = write_indented_margin(ctx, f);
}
if (err == 0) {
err = PyFile_WriteString(message, f);
}
if (err == 0) {
err = write_indented_margin(ctx, f);
}
if (err == 0) {
err = PyFile_WriteString("\n", f);
}
}
ctx->need_close = need_close;
return err;
}
static void
print_exception_recursive(struct exception_print_context* ctx, PyObject *value)
{
int err = 0, res;
PyObject *cause, *context;
if (seen != NULL) {
if (ctx->seen != NULL) {
/* Exception chaining */
PyObject *value_id = PyLong_FromVoidPtr(value);
if (value_id == NULL || PySet_Add(seen, value_id) == -1)
if (value_id == NULL || PySet_Add(ctx->seen, value_id) == -1)
PyErr_Clear();
else if (PyExceptionInstance_Check(value)) {
PyObject *check_id = NULL;
cause = PyException_GetCause(value);
context = PyException_GetContext(value);
if (cause) {
@ -1066,16 +1160,13 @@ print_exception_recursive(PyObject *f, PyObject *value, PyObject *seen)
if (check_id == NULL) {
res = -1;
} else {
res = PySet_Contains(seen, check_id);
res = PySet_Contains(ctx->seen, check_id);
Py_DECREF(check_id);
}
if (res == -1)
PyErr_Clear();
if (res == 0) {
print_exception_recursive(
f, cause, seen);
err |= PyFile_WriteString(
cause_message, f);
err = print_chained(ctx, cause, cause_message, "cause");
}
}
else if (context &&
@ -1084,16 +1175,13 @@ print_exception_recursive(PyObject *f, PyObject *value, PyObject *seen)
if (check_id == NULL) {
res = -1;
} else {
res = PySet_Contains(seen, check_id);
res = PySet_Contains(ctx->seen, check_id);
Py_DECREF(check_id);
}
if (res == -1)
PyErr_Clear();
if (res == 0) {
print_exception_recursive(
f, context, seen);
err |= PyFile_WriteString(
context_message, f);
err = print_chained(ctx, context, context_message, "context");
}
}
Py_XDECREF(context);
@ -1101,17 +1189,146 @@ print_exception_recursive(PyObject *f, PyObject *value, PyObject *seen)
}
Py_XDECREF(value_id);
}
print_exception(f, value);
if (err) {
/* don't do anything else */
}
else if (!_PyBaseExceptionGroup_Check(value)) {
print_exception(ctx, value);
}
else if (ctx->exception_group_depth > ctx->max_group_depth) {
/* exception group but depth exceeds limit */
PyObject *line = PyUnicode_FromFormat(
"... (max_group_depth is %d)\n", ctx->max_group_depth);
if (line) {
PyObject *f = ctx->file;
if (err == 0) {
err = write_indented_margin(ctx, f);
}
if (err == 0) {
err = PyFile_WriteObject(line, f, Py_PRINT_RAW);
}
Py_DECREF(line);
}
else {
err = -1;
}
}
else {
/* format exception group */
if (ctx->exception_group_depth == 0) {
ctx->exception_group_depth += 1;
}
print_exception(ctx, value);
PyObject *excs = ((PyBaseExceptionGroupObject *)value)->excs;
assert(excs && PyTuple_Check(excs));
Py_ssize_t num_excs = PyTuple_GET_SIZE(excs);
assert(num_excs > 0);
Py_ssize_t n;
if (num_excs <= ctx->max_group_width) {
n = num_excs;
}
else {
n = ctx->max_group_width + 1;
}
PyObject *f = ctx->file;
ctx->need_close = false;
for (Py_ssize_t i = 0; i < n; i++) {
int last_exc = (i == n - 1);
if (last_exc) {
// The closing frame may be added in a recursive call
ctx->need_close = true;
}
PyObject *line;
bool truncated = (i >= ctx->max_group_width);
if (!truncated) {
line = PyUnicode_FromFormat(
"%s+---------------- %zd ----------------\n",
(i == 0) ? "+-" : " ", i + 1);
}
else {
line = PyUnicode_FromFormat(
"%s+---------------- ... ----------------\n",
(i == 0) ? "+-" : " ");
}
if (line) {
if (err == 0) {
err = _Py_WriteIndent(EXC_INDENT(ctx), f);
}
if (err == 0) {
err = PyFile_WriteObject(line, f, Py_PRINT_RAW);
}
Py_DECREF(line);
}
else {
err = -1;
}
if (err == 0) {
ctx->exception_group_depth += 1;
PyObject *exc = PyTuple_GET_ITEM(excs, i);
if (!truncated) {
if (!Py_EnterRecursiveCall(" in print_exception_recursive")) {
print_exception_recursive(ctx, exc);
Py_LeaveRecursiveCall();
}
else {
err = -1;
}
}
else {
Py_ssize_t excs_remaining = num_excs - ctx->max_group_width;
PyObject *line = PyUnicode_FromFormat(
"and %zd more exception%s\n",
excs_remaining, excs_remaining > 1 ? "s" : "");
if (line) {
if (err == 0) {
err = write_indented_margin(ctx, f);
}
if (err == 0) {
err = PyFile_WriteObject(line, f, Py_PRINT_RAW);
}
Py_DECREF(line);
}
else {
err = -1;
}
}
if (err == 0 && last_exc && ctx->need_close) {
err = _Py_WriteIndent(EXC_INDENT(ctx), f);
if (err == 0) {
err = PyFile_WriteString(
"+------------------------------------\n", f);
}
ctx->need_close = false;
}
ctx->exception_group_depth -= 1;
}
}
if (ctx->exception_group_depth == 1) {
ctx->exception_group_depth -= 1;
}
}
if (err != 0)
PyErr_Clear();
}
#define PyErr_MAX_GROUP_WIDTH 15
#define PyErr_MAX_GROUP_DEPTH 10
void
_PyErr_Display(PyObject *file, PyObject *exception, PyObject *value, PyObject *tb)
{
assert(file != NULL && file != Py_None);
PyObject *seen;
if (PyExceptionInstance_Check(value)
&& tb != NULL && PyTraceBack_Check(tb)) {
/* Put the traceback on the exception, otherwise it won't get
@ -1123,15 +1340,21 @@ _PyErr_Display(PyObject *file, PyObject *exception, PyObject *value, PyObject *t
Py_DECREF(cur_tb);
}
struct exception_print_context ctx;
ctx.file = file;
ctx.exception_group_depth = 0;
ctx.max_group_width = PyErr_MAX_GROUP_WIDTH;
ctx.max_group_depth = PyErr_MAX_GROUP_DEPTH;
/* We choose to ignore seen being possibly NULL, and report
at least the main exception (it could be a MemoryError).
*/
seen = PySet_New(NULL);
if (seen == NULL) {
ctx.seen = PySet_New(NULL);
if (ctx.seen == NULL) {
PyErr_Clear();
}
print_exception_recursive(file, value, seen);
Py_XDECREF(seen);
print_exception_recursive(&ctx, value);
Py_XDECREF(ctx.seen);
/* Call file.flush() */
PyObject *res = _PyObject_CallMethodIdNoArgs(file, &PyId_flush);

View File

@ -14,6 +14,7 @@
#include "pycore_pyarena.h" // _PyArena_Free()
#include "pycore_pyerrors.h" // _PyErr_Fetch()
#include "pycore_pystate.h" // _PyThreadState_GET()
#include "pycore_traceback.h" // EXCEPTION_TB_HEADER
#include "../Parser/pegen.h" // _PyPegen_byte_offset_to_character_offset()
#include "structmember.h" // PyMemberDef
#include "osdefs.h" // SEP
@ -379,8 +380,44 @@ finally:
return result;
}
/* Writes indent spaces. Returns 0 on success and non-zero on failure.
*/
int
_Py_DisplaySourceLine(PyObject *f, PyObject *filename, int lineno, int indent, int *truncation, PyObject **line)
_Py_WriteIndent(int indent, PyObject *f)
{
int err = 0;
char buf[11] = " ";
assert(strlen(buf) == 10);
while (indent > 0) {
if (indent < 10) {
buf[indent] = '\0';
}
err = PyFile_WriteString(buf, f);
if (err != 0) {
return err;
}
indent -= 10;
}
return 0;
}
/* Writes indent spaces, followed by the margin if it is not `\0`.
Returns 0 on success and non-zero on failure.
*/
int
_Py_WriteIndentedMargin(int indent, const char *margin, PyObject *f)
{
int err = _Py_WriteIndent(indent, f);
if (err == 0 && margin) {
err = PyFile_WriteString(margin, f);
}
return err;
}
static int
display_source_line_with_margin(PyObject *f, PyObject *filename, int lineno, int indent,
int margin_indent, const char *margin,
int *truncation, PyObject **line)
{
int err = 0;
int fd;
@ -508,27 +545,33 @@ _Py_DisplaySourceLine(PyObject *f, PyObject *filename, int lineno, int indent, i
*truncation = i - indent;
}
if (err == 0) {
err = _Py_WriteIndentedMargin(margin_indent, margin, f);
}
/* Write some spaces before the line */
strcpy(buf, " ");
assert (strlen(buf) == 10);
while (indent > 0) {
if (indent < 10)
buf[indent] = '\0';
err = PyFile_WriteString(buf, f);
if (err != 0)
break;
indent -= 10;
if (err == 0) {
err = _Py_WriteIndent(indent, f);
}
/* finally display the line */
if (err == 0)
if (err == 0) {
err = PyFile_WriteObject(lineobj, f, Py_PRINT_RAW);
}
Py_DECREF(lineobj);
if (err == 0)
if (err == 0) {
err = PyFile_WriteString("\n", f);
}
return err;
}
int
_Py_DisplaySourceLine(PyObject *f, PyObject *filename, int lineno, int indent,
int *truncation, PyObject **line)
{
return display_source_line_with_margin(f, filename, lineno, indent, 0,
NULL, truncation, line);
}
/* AST based Traceback Specialization
*
* When displaying a new traceback line, for certain syntactical constructs
@ -697,7 +740,7 @@ print_error_location_carets(PyObject *f, int offset, Py_ssize_t start_offset, Py
static int
tb_displayline(PyTracebackObject* tb, PyObject *f, PyObject *filename, int lineno,
PyFrameObject *frame, PyObject *name)
PyFrameObject *frame, PyObject *name, int margin_indent, const char *margin)
{
int err;
PyObject *line;
@ -708,15 +751,20 @@ tb_displayline(PyTracebackObject* tb, PyObject *f, PyObject *filename, int linen
filename, lineno, name);
if (line == NULL)
return -1;
err = PyFile_WriteObject(line, f, Py_PRINT_RAW);
err = _Py_WriteIndentedMargin(margin_indent, margin, f);
if (err == 0) {
err = PyFile_WriteObject(line, f, Py_PRINT_RAW);
}
Py_DECREF(line);
if (err != 0)
return err;
int truncation = _TRACEBACK_SOURCE_LINE_INDENT;
PyObject* source_line = NULL;
if (_Py_DisplaySourceLine(f, filename, lineno, _TRACEBACK_SOURCE_LINE_INDENT,
&truncation, &source_line) != 0 || !source_line) {
int rc = display_source_line_with_margin(
f, filename, lineno, _TRACEBACK_SOURCE_LINE_INDENT,
margin_indent, margin, &truncation, &source_line);
if (rc != 0 || !source_line) {
/* ignore errors since we can't report them, can we? */
err = ignore_source_errors();
goto done;
@ -801,9 +849,12 @@ tb_displayline(PyTracebackObject* tb, PyObject *f, PyObject *filename, int linen
end_offset = i + 1;
}
err = print_error_location_carets(f, truncation, start_offset, end_offset,
right_start_offset, left_end_offset,
primary_error_char, secondary_error_char);
err = _Py_WriteIndentedMargin(margin_indent, margin, f);
if (err == 0) {
err = print_error_location_carets(f, truncation, start_offset, end_offset,
right_start_offset, left_end_offset,
primary_error_char, secondary_error_char);
}
done:
Py_XDECREF(source_line);
@ -830,7 +881,8 @@ tb_print_line_repeated(PyObject *f, long cnt)
}
static int
tb_printinternal(PyTracebackObject *tb, PyObject *f, long limit)
tb_printinternal(PyTracebackObject *tb, PyObject *f, long limit,
int indent, const char *margin)
{
int err = 0;
Py_ssize_t depth = 0;
@ -864,7 +916,7 @@ tb_printinternal(PyTracebackObject *tb, PyObject *f, long limit)
cnt++;
if (err == 0 && cnt <= TB_RECURSIVE_CUTOFF) {
err = tb_displayline(tb, f, code->co_filename, tb->tb_lineno,
tb->tb_frame, code->co_name);
tb->tb_frame, code->co_name, indent, margin);
if (err == 0) {
err = PyErr_CheckSignals();
}
@ -881,7 +933,8 @@ tb_printinternal(PyTracebackObject *tb, PyObject *f, long limit)
#define PyTraceBack_LIMIT 1000
int
PyTraceBack_Print(PyObject *v, PyObject *f)
_PyTraceBack_Print_Indented(PyObject *v, int indent, const char *margin,
const char *header_margin, const char *header, PyObject *f)
{
int err;
PyObject *limitv;
@ -904,12 +957,27 @@ PyTraceBack_Print(PyObject *v, PyObject *f)
return 0;
}
}
err = PyFile_WriteString("Traceback (most recent call last):\n", f);
if (!err)
err = tb_printinternal((PyTracebackObject *)v, f, limit);
err = _Py_WriteIndentedMargin(indent, header_margin, f);
if (err == 0) {
err = PyFile_WriteString(header, f);
}
if (err == 0) {
err = tb_printinternal((PyTracebackObject *)v, f, limit, indent, margin);
}
return err;
}
int
PyTraceBack_Print(PyObject *v, PyObject *f)
{
int indent = 0;
const char *margin = NULL;
const char *header_margin = NULL;
const char *header = EXCEPTION_TB_HEADER;
return _PyTraceBack_Print_Indented(v, indent, margin, header_margin, header, f);
}
/* Format an integer in range [0; 0xffffffff] to decimal and write it
into the file fd.