Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
1 change: 1 addition & 0 deletions Include/cpython/pyerrors.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ typedef struct {
PyException_HEAD
PyObject *msg;
PyObject *excs;
Py_ssize_t leaf_count;
} PyBaseExceptionGroupObject;

typedef struct {
Expand Down
65 changes: 65 additions & 0 deletions Lib/test/test_exception_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,71 @@ class MyBEG(BaseExceptionGroup):
MyBEG)


class StrAndReprTests(unittest.TestCase):
def test_ExceptionGroup(self):
eg = BaseExceptionGroup(
'flat', [ValueError(1), TypeError(2)])

self.assertEqual(str(eg), "flat (group of 2 exceptions)")
self.assertEqual(repr(eg),
"ExceptionGroup('flat', [ValueError(1), TypeError(2)])")

eg = BaseExceptionGroup(
'nested', [eg, ValueError(1), eg, TypeError(2)])

self.assertEqual(str(eg), "nested (group of 6 exceptions)")
self.assertEqual(repr(eg),
"ExceptionGroup('nested', "
"[ExceptionGroup('flat', "
"[ValueError(1), TypeError(2)]), "
"ValueError(1), "
"ExceptionGroup('flat', "
"[ValueError(1), TypeError(2)]), TypeError(2)])")

def test_BaseExceptionGroup(self):
eg = BaseExceptionGroup(
'flat', [ValueError(1), KeyboardInterrupt(2)])

self.assertEqual(str(eg), "flat (group of 2 exceptions)")
self.assertEqual(repr(eg),
"BaseExceptionGroup("
"'flat', "
"[ValueError(1), KeyboardInterrupt(2)])")

eg = BaseExceptionGroup(
'nested', [eg, ValueError(1), eg])

self.assertEqual(str(eg), "nested (group of 5 exceptions)")
self.assertEqual(repr(eg),
"BaseExceptionGroup('nested', "
"[BaseExceptionGroup('flat', "
"[ValueError(1), KeyboardInterrupt(2)]), "
"ValueError(1), "
"BaseExceptionGroup('flat', "
"[ValueError(1), KeyboardInterrupt(2)])])")

def test_custom_exception(self):
class MyEG(ExceptionGroup):
pass

eg = MyEG(
'flat', [ValueError(1), TypeError(2)])

self.assertEqual(str(eg), "flat (group of 2 exceptions)")
self.assertEqual(repr(eg), "MyEG('flat', [ValueError(1), TypeError(2)])")

eg = MyEG(
'nested', [eg, ValueError(1), eg, TypeError(2)])

self.assertEqual(str(eg), "nested (group of 6 exceptions)")
self.assertEqual(repr(eg), (
"MyEG('nested', "
"[MyEG('flat', [ValueError(1), TypeError(2)]), "
"ValueError(1), "
"MyEG('flat', [ValueError(1), TypeError(2)]), "
"TypeError(2)])"))


def create_simple_eg():
excs = []
try:
Expand Down
56 changes: 28 additions & 28 deletions Lib/test/test_traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -1403,7 +1403,7 @@ def exc():
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' | ExceptionGroup: eg (group of 2 exceptions)\n'
f' +-+---------------- 1 ----------------\n'
f' | ValueError: 1\n'
f' +---------------- 2 ----------------\n'
Expand All @@ -1425,7 +1425,7 @@ def exc():
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' | ExceptionGroup: eg1 (group of 2 exceptions)\n'
f' +-+---------------- 1 ----------------\n'
f' | ValueError: 1\n'
f' +---------------- 2 ----------------\n'
Expand All @@ -1441,7 +1441,7 @@ def exc():
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' | ExceptionGroup: eg2 (group of 2 exceptions)\n'
f' +-+---------------- 1 ----------------\n'
f' | ValueError: 3\n'
f' +---------------- 2 ----------------\n'
Expand All @@ -1467,7 +1467,7 @@ def exc():
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' | ExceptionGroup: eg1 (group of 2 exceptions)\n'
f' +-+---------------- 1 ----------------\n'
f' | ValueError: 1\n'
f' +---------------- 2 ----------------\n'
Expand All @@ -1480,7 +1480,7 @@ def exc():
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' | ExceptionGroup: eg2 (group of 2 exceptions)\n'
f' +-+---------------- 1 ----------------\n'
f' | ValueError: 3\n'
f' +---------------- 2 ----------------\n'
Expand Down Expand Up @@ -1519,15 +1519,15 @@ def exc():
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' | ExceptionGroup: eg (group of 4 exceptions)\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' | ExceptionGroup: nested (group of 2 exceptions)\n'
f' +-+---------------- 1 ----------------\n'
f' | TypeError: 2\n'
f' +---------------- 2 ----------------\n'
Expand All @@ -1546,7 +1546,7 @@ def exc():
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' | ExceptionGroup: top (group of 1 exception)\n'
f' +-+---------------- 1 ----------------\n'
f' | ValueError: 5\n'
f' +------------------------------------\n')
Expand All @@ -1560,7 +1560,7 @@ def test_exception_group_width_limit(self):
excs.append(ValueError(i))
eg = ExceptionGroup('eg', excs)

expected = (' | ExceptionGroup: eg\n'
expected = (' | ExceptionGroup: eg (group of 1000 exceptions)\n'
' +-+---------------- 1 ----------------\n'
' | ValueError: 0\n'
' +---------------- 2 ----------------\n'
Expand Down Expand Up @@ -1605,43 +1605,43 @@ def test_exception_group_depth_limit(self):
f'eg{i}',
[ValueError(i), exc, ValueError(-i)])

expected = (' | ExceptionGroup: eg999\n'
expected = (' | ExceptionGroup: eg999 (group of 2001 exceptions)\n'
' +-+---------------- 1 ----------------\n'
' | ValueError: 999\n'
' +---------------- 2 ----------------\n'
' | ExceptionGroup: eg998\n'
' | ExceptionGroup: eg998 (group of 1999 exceptions)\n'
' +-+---------------- 1 ----------------\n'
' | ValueError: 998\n'
' +---------------- 2 ----------------\n'
' | ExceptionGroup: eg997\n'
' | ExceptionGroup: eg997 (group of 1997 exceptions)\n'
' +-+---------------- 1 ----------------\n'
' | ValueError: 997\n'
' +---------------- 2 ----------------\n'
' | ExceptionGroup: eg996\n'
' | ExceptionGroup: eg996 (group of 1995 exceptions)\n'
' +-+---------------- 1 ----------------\n'
' | ValueError: 996\n'
' +---------------- 2 ----------------\n'
' | ExceptionGroup: eg995\n'
' | ExceptionGroup: eg995 (group of 1993 exceptions)\n'
' +-+---------------- 1 ----------------\n'
' | ValueError: 995\n'
' +---------------- 2 ----------------\n'
' | ExceptionGroup: eg994\n'
' | ExceptionGroup: eg994 (group of 1991 exceptions)\n'
' +-+---------------- 1 ----------------\n'
' | ValueError: 994\n'
' +---------------- 2 ----------------\n'
' | ExceptionGroup: eg993\n'
' | ExceptionGroup: eg993 (group of 1989 exceptions)\n'
' +-+---------------- 1 ----------------\n'
' | ValueError: 993\n'
' +---------------- 2 ----------------\n'
' | ExceptionGroup: eg992\n'
' | ExceptionGroup: eg992 (group of 1987 exceptions)\n'
' +-+---------------- 1 ----------------\n'
' | ValueError: 992\n'
' +---------------- 2 ----------------\n'
' | ExceptionGroup: eg991\n'
' | ExceptionGroup: eg991 (group of 1985 exceptions)\n'
' +-+---------------- 1 ----------------\n'
' | ValueError: 991\n'
' +---------------- 2 ----------------\n'
' | ExceptionGroup: eg990\n'
' | ExceptionGroup: eg990 (group of 1983 exceptions)\n'
' +-+---------------- 1 ----------------\n'
' | ValueError: 990\n'
' +---------------- 2 ----------------\n'
Expand Down Expand Up @@ -1707,7 +1707,7 @@ def exc():
f' | File "{__file__}", line {exc.__code__.co_firstlineno + 9}, in exc\n'
f' | raise ExceptionGroup("nested", excs)\n'
f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n'
f' | ExceptionGroup: nested\n'
f' | ExceptionGroup: nested (group of 2 exceptions)\n'
f' | >> Multi line note\n'
f' | >> Because I am such\n'
f' | >> an important exception.\n'
Expand Down Expand Up @@ -2460,7 +2460,7 @@ def test_exception_group_construction(self):
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')
expected = "ExceptionGroup: eg2 (group of 3 exceptions)\n".split('\n')

self.assertEqual(formatted, expected)

Expand All @@ -2476,13 +2476,13 @@ def test_exception_group_format(self):
f' | File "{__file__}", line {lno_g+23}, in _get_exception_group',
f' | raise ExceptionGroup("eg2", [exc3, exc4])',
f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^',
f' | ExceptionGroup: eg2',
f' | ExceptionGroup: eg2 (group of 3 exceptions)',
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' | ExceptionGroup: eg1 (group of 2 exceptions)',
f' +-+---------------- 1 ----------------',
f' | Traceback (most recent call last):',
f' | File "{__file__}", line {lno_g+9}, in _get_exception_group',
Expand Down Expand Up @@ -2531,9 +2531,9 @@ def test_max_group_width(self):
formatted = ''.join(teg.format()).split('\n')

expected = [
f' | ExceptionGroup: eg',
f' | ExceptionGroup: eg (group of 13 exceptions)',
f' +-+---------------- 1 ----------------',
f' | ExceptionGroup: eg1',
f' | ExceptionGroup: eg1 (group of 3 exceptions)',
f' +-+---------------- 1 ----------------',
f' | ValueError: 0',
f' +---------------- 2 ----------------',
Expand All @@ -2542,7 +2542,7 @@ def test_max_group_width(self):
f' | and 1 more exception',
f' +------------------------------------',
f' +---------------- 2 ----------------',
f' | ExceptionGroup: eg2',
f' | ExceptionGroup: eg2 (group of 10 exceptions)',
f' +-+---------------- 1 ----------------',
f' | TypeError: 0',
f' +---------------- 2 ----------------',
Expand All @@ -2563,11 +2563,11 @@ def test_max_group_depth(self):
formatted = ''.join(teg.format()).split('\n')

expected = [
f' | ExceptionGroup: exc',
f' | ExceptionGroup: exc (group of 7 exceptions)',
f' +-+---------------- 1 ----------------',
f' | ValueError: -2',
f' +---------------- 2 ----------------',
f' | ExceptionGroup: exc',
f' | ExceptionGroup: exc (group of 5 exceptions)',
f' +-+---------------- 1 ----------------',
f' | ValueError: -1',
f' +---------------- 2 ----------------',
Expand Down
13 changes: 12 additions & 1 deletion Objects/exceptions.c
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,7 @@ BaseExceptionGroup_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
goto error;
}

Py_ssize_t leaf_count = 0;
bool nested_base_exceptions = false;
for (Py_ssize_t i = 0; i < numexcs; i++) {
PyObject *exc = PyTuple_GET_ITEM(exceptions, i);
Expand All @@ -734,6 +735,12 @@ BaseExceptionGroup_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
else if (is_nonbase_exception == 0) {
nested_base_exceptions = true;
}
if (_PyBaseExceptionGroup_Check(exc)) {
leaf_count += ((PyBaseExceptionGroupObject *)exc)->leaf_count;
}
else {
leaf_count++;
}
}

PyTypeObject *cls = type;
Expand Down Expand Up @@ -770,6 +777,7 @@ BaseExceptionGroup_new(PyTypeObject *type, PyObject *args, PyObject *kwds)

self->msg = Py_NewRef(message);
self->excs = exceptions;
self->leaf_count = leaf_count;
return (PyObject*)self;
error:
Py_DECREF(exceptions);
Expand Down Expand Up @@ -836,7 +844,10 @@ BaseExceptionGroup_str(PyBaseExceptionGroupObject *self)
{
assert(self->msg);
assert(PyUnicode_Check(self->msg));
return Py_NewRef(self->msg);

return PyUnicode_FromFormat(
"%S (group of %zd exception%s)",
self->msg, self->leaf_count, self->leaf_count > 1 ? "s" : "");
}

static PyObject *
Expand Down