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:
parent
e52f9bee80
commit
3509b26c91
@ -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
|
||||
|
@ -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):
|
||||
|
141
Lib/traceback.py
141
Lib/traceback.py
@ -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'."""
|
||||
|
@ -1 +1 @@
|
||||
Implement :pep:`654` Add :class:`ExceptionGroup` and :class:`BaseExceptionGroup`.
|
||||
Implement :pep:`654`. Add :class:`ExceptionGroup` and :class:`BaseExceptionGroup`. Update traceback display code.
|
||||
|
@ -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);
|
||||
|
@ -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.
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user