Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 27 commits into from
Nov 5, 2021
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
42bed01
C traceback code
iritkatriel Oct 5, 2021
a2daa23
add ExceptionGroups support to traceback.py
iritkatriel Oct 8, 2021
261917a
remove 'with X sub-exceptions' line from tracebacks
iritkatriel Oct 24, 2021
7613b43
pass margin instead of margin_char
iritkatriel Oct 25, 2021
d98a72b
update news
iritkatriel Oct 25, 2021
d69916e
excs is tuple, use PyTuple apis. Change check to assertion.
iritkatriel Oct 25, 2021
f5cab69
remove redundant num_excs > 0 check (it is asserted above)
iritkatriel Oct 25, 2021
5170f00
remove cpython_only from exception group tests
iritkatriel Oct 25, 2021
2052c77
handle recursion errors (vert deeply nested EGs)
iritkatriel Oct 25, 2021
5097300
WRITE_INDENTED_MARGIN macro --> write_indented_margin function
iritkatriel Oct 26, 2021
dc21cf8
move new traceback utils to internal/
iritkatriel Oct 26, 2021
d4007b7
test improvements
iritkatriel Oct 26, 2021
169934e
pep7, improve error checking and clarity
iritkatriel Oct 26, 2021
aa4da45
add missing test to cover print_chained with/without parent_label
iritkatriel Oct 26, 2021
6ee84f7
compare the complete expected tb text
iritkatriel Oct 26, 2021
ac7f34c
Update Misc/NEWS.d/next/Core and Builtins/2021-09-26-18-18-50.bpo-452…
iritkatriel Oct 26, 2021
d0d4961
don't need the regex anymore
iritkatriel Oct 26, 2021
5c1015d
remove full-path labels
iritkatriel Oct 29, 2021
83abebd
int --> bool
iritkatriel Oct 29, 2021
16d077d
move code around
iritkatriel Oct 29, 2021
64fb164
Tweak the top-level of traceback box as suggested by Yury
iritkatriel Oct 31, 2021
88019f5
tidy up error handling
iritkatriel Nov 1, 2021
e963835
add limits for width and depth of formatted exception groups
iritkatriel Nov 2, 2021
e85510a
use _PyBaseExceptionGroup_Check macro
iritkatriel Nov 2, 2021
c15a7bd
remove redundant PyErr_Clear
iritkatriel Nov 2, 2021
d8cc6e8
minor tweak - move if out of loop
iritkatriel Nov 2, 2021
61fab3f
remove excess whitespace
iritkatriel Nov 3, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Include/internal/pycore_traceback.h
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ PyAPI_FUNC(PyObject*) _PyTraceBack_FromFrame(
PyObject *tb_next,
PyFrameObject *frame);

/* 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, PyObject *f);
PyAPI_FUNC(int) _Py_WriteIndentedMargin(int, const char*, PyObject *);
PyAPI_FUNC(int) _Py_WriteIndent(int, PyObject *);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need to add these to the API, or can you just use the export keyword? Will external software use these APIs?


#ifdef __cplusplus
}
#endif
Expand Down
335 changes: 334 additions & 1 deletion Lib/test/test_traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -987,6 +987,35 @@ def __eq__(self, other):
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")
Expand All @@ -998,7 +1027,6 @@ def __eq__(self, other):
boundaries = re.compile(
'(%s|%s)' % (re.escape(cause_message), re.escape(context_message)))


class BaseExceptionReportingTests:

def get_exception(self, exception_or_callable):
Expand All @@ -1009,6 +1037,8 @@ def get_exception(self, exception_or_callable):
except Exception as e:
return e

callable_line = get_exception.__code__.co_firstlineno + 4

def zero_div(self):
1/0 # In zero_div

Expand Down Expand Up @@ -1234,6 +1264,172 @@ def __str__(self):
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' | 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):
iritkatriel marked this conversation as resolved.
Show resolved Hide resolved
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' | 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' +-+---------------- cause.1 ----------------\n'
f' | ValueError: 1\n'
f' +---------------- cause.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' | 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' | 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' +-+---------------- context.context.1 ----------------\n'
f' | ValueError: 1\n'
f' +---------------- context.context.2 ----------------\n'
f' | TypeError: 2\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 {exc.__code__.co_firstlineno + 6}, in exc\n'
f' | raise EG("eg2", [ValueError(3), TypeError(4)])\n'
f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n'
f' | ExceptionGroup: eg2\n'
f' +-+---------------- context.1 ----------------\n'
f' | ValueError: 3\n'
f' +---------------- context.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' | 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' +-+---------------- context.1 ----------------\n'
f' | ValueError: 1\n'
f' +---------------- context.2 ----------------\n'
f' | 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' +-+---------------- context.2.1 ----------------\n'
f' | TypeError: 2\n'
f' +---------------- context.2.2 ----------------\n'
f' | TypeError: 3\n'
f' +------------------------------------\n'
f' +---------------- context.3 ----------------\n'
f' | ValueError: 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 + 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)


class PyExcReportingTests(BaseExceptionReportingTests, unittest.TestCase):
#
# This checks reporting through the 'traceback' module, with both
Expand Down Expand Up @@ -1913,6 +2109,143 @@ def f():
''])


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' | Traceback (most recent call last):',
f' | File "{__file__}", line '
f'{lno_g+23}, in _get_exception_group',
iritkatriel marked this conversation as resolved.
Show resolved Hide resolved
f' | raise ExceptionGroup("eg2", [exc3, exc4])',
f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^',
f' | ExceptionGroup: eg2',
f' +-+---------------- 1 ----------------',
f' | Traceback (most recent call last):',
f' | File "{__file__}", '
f'line {lno_g+16}, in _get_exception_group',
f' | raise ExceptionGroup("eg1", [exc1, exc2])',
f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^',
f' | ExceptionGroup: eg1',
f' +-+---------------- 1.1 ----------------',
f' | Traceback (most recent call last):',
f' | File '
f'"{__file__}", line {lno_g+9}, in '
f'_get_exception_group',
f' | f()',
f' | ^^^',
f' | File '
f'"{__file__}", line {lno_f+1}, in '
f'f',
f' | 1/0',
f' | ~^~',
f' | ZeroDivisionError: division by zero',
f' +---------------- 1.2 ----------------',
f' | Traceback (most recent call last):',
f' | File '
f'"{__file__}", line {lno_g+13}, in '
f'_get_exception_group',
f' | g(42)',
f' | ^^^^^',
f' | File '
f'"{__file__}", line {lno_g+1}, in '
f'g',
f' | raise ValueError(v)',
f' | ^^^^^^^^^^^^^^^^^^^',
f' | ValueError: 42',
f' +------------------------------------',
f' +---------------- 2 ----------------',
f' | Traceback (most recent call last):',
f' | File "{__file__}", '
f'line {lno_g+20}, in _get_exception_group',
f' | g(24)',
f' | ^^^^^',
f' | File "{__file__}", '
f'line {lno_g+1}, in g',
f' | raise ValueError(v)',
f' | ^^^^^^^^^^^^^^^^^^^',
f' | ValueError: 24',
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):
Expand Down
Loading