Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
b1afcb8
Use `self.exceptions` in `BaseExceptionGroup` repr by supporting cust…
dr-carlos Nov 19, 2025
59bc3c7
Update `ExceptionGroup` tests for new repr and add mutation test
dr-carlos Nov 19, 2025
ca2d21d
Fix typo in test comment
dr-carlos Nov 19, 2025
493ab01
Update docs for `BaseExceptionGroup` repr change
dr-carlos Nov 19, 2025
cd90d51
Add NEWS entry
dr-carlos Nov 19, 2025
177b535
Update `ExceptionGroup` repr in `test_traceback`
dr-carlos Nov 19, 2025
af4f086
Revert "Update `ExceptionGroup` repr in `test_traceback`"
dr-carlos Nov 21, 2025
72918ea
Update `ExceptionGroup` tests to no longer change the repr
dr-carlos Nov 21, 2025
b45402a
Store exceptions copy in `BaseExceptionGroup->excs_orig` and use in `…
dr-carlos Nov 21, 2025
62d72a4
Revert "Update docs for `BaseExceptionGroup` repr change"
dr-carlos Nov 21, 2025
f93ae8f
Remove `BaseExceptionGroup->excs_orig` and reconstruct in `repr` instead
dr-carlos Nov 21, 2025
56eefee
Ensure that the exceptions copy in `BaseExceptionGroup` repr is freed
dr-carlos Nov 21, 2025
79dd615
Move `ExceptionGroup` repr tests into new function
dr-carlos Nov 22, 2025
e9fec3d
Use the exceptions tuple for all non-lists in `ExceptionGroup` repr
dr-carlos Nov 22, 2025
f5acfef
Test custom sequence's `ExceptionGroup` repr
dr-carlos Nov 22, 2025
2fca592
Update code style in `exceptions.c`
dr-carlos Nov 22, 2025
4885a6b
Update code style in `exceptions.c`
dr-carlos Nov 22, 2025
4801399
Remove unused `name` variable with new `%Y` format
dr-carlos Nov 23, 2025
1bcddeb
Revert "Remove unused `name` variable with new `%Y` format"
dr-carlos Nov 23, 2025
ccef7c5
Revert "Update code style in `exceptions.c`"
dr-carlos Nov 23, 2025
9ca573e
Save repr of non-standard sequences in `BaseExceptionGroup` constructor
dr-carlos Nov 25, 2025
9531fe9
Update tests for non-standard exceptions sequence in `ExceptionGroup`
dr-carlos Nov 25, 2025
1da9238
Document implementation details for `ExceptionGroup` exception sequence
dr-carlos Nov 25, 2025
fcaf314
Update Misc/NEWS.d/next/Core_and_Builtins/2025-11-19-16-40-24.gh-issu…
iritkatriel Nov 26, 2025
2c67e71
Merge branch 'main' into exception-group-repr
iritkatriel Nov 26, 2025
d962581
Reword 'optimum' to 'optimal' in `exceptions.rst`
dr-carlos Nov 26, 2025
ce39eec
Update NEWS Entry
dr-carlos Nov 26, 2025
9211d01
Handle error on `__repr__` for exceptions sequence passed to `Excepti…
dr-carlos Nov 26, 2025
737206a
Change grammar in `exceptions.c`
dr-carlos Nov 26, 2025
8d17ef3
Update comment relating to `Py_XNewRef` in `BaseExceptionGroup_repr()`
dr-carlos Nov 26, 2025
383af2f
Handle `PySequence_List()` and `PyObject_Repr()` errors in `BaseExcep…
dr-carlos Nov 26, 2025
5353340
Conditionally use `Py_NewRef` in `BaseExceptionGroup_repr()`
dr-carlos Nov 26, 2025
9997c01
Refactor `goto error` into `return NULL` within `BaseExceptionGroup_r…
dr-carlos Nov 26, 2025
3cd38db
Test custom sequences which raise errors in the `ExceptionGroup()` co…
dr-carlos Nov 26, 2025
5feb970
Fix formatting of if-else in `exceptions.c`
dr-carlos Nov 26, 2025
d7aec51
Simplify f-string to normal str in `test_exceptions_mutation()`
dr-carlos Nov 26, 2025
e21fad0
Remove unnecessary `_exceptions_str` member of `ExceptionGroup`
dr-carlos Nov 26, 2025
79094f6
Fix if-else formatting in `exceptions.c`
dr-carlos Nov 26, 2025
b5fcfa7
Keep lines less than 80 chars
dr-carlos Nov 26, 2025
57f8bf6
Revert "Fix formatting of if-else in `exceptions.c`"
dr-carlos Nov 27, 2025
9cba6b6
Construct custom sequence outside of `assertRaises` context manager i…
dr-carlos Nov 27, 2025
d61e6b9
Swap order of if-else branches and clarify comment in `BaseExceptionG…
dr-carlos Nov 27, 2025
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
34 changes: 33 additions & 1 deletion Lib/test/test_exception_group.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import collections.abc
import collections
import types
import unittest
from test.support import skip_emscripten_stack_overflow, skip_wasi_stack_overflow, exceeds_recursion_limit
Expand Down Expand Up @@ -193,6 +193,38 @@ class MyEG(ExceptionGroup):
"MyEG('flat', [ValueError(1), TypeError(2)]), "
"TypeError(2)])"))

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

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

self.assertEqual(repr(eg), "MyEG('test', [ValueError(1), TypeError(2)])")
excs.clear()

# Ensure that clearing the exceptions sequence doesn't change the repr.
self.assertEqual(repr(eg), "MyEG('test', [ValueError(1), TypeError(2)])")

# Ensure that the args are still as passed.
self.assertEqual(eg.args, ('test', []))

excs = (ValueError(1), KeyboardInterrupt(2))
eg = BaseExceptionGroup('test', excs)

# Ensure that immutable sequences still work fine.
self.assertEqual(repr(eg), "BaseExceptionGroup('test', (ValueError(1), KeyboardInterrupt(2)))")

# Test non-standard custom sequences.
excs = collections.deque([ValueError(1), TypeError(2)])
eg = ExceptionGroup('test', excs)

self.assertEqual(repr(eg), "ExceptionGroup('test', (ValueError(1), TypeError(2)))")
excs.clear()

# Ensure that clearing the exceptions sequence doesn't change the repr.
self.assertEqual(repr(eg), "ExceptionGroup('test', (ValueError(1), TypeError(2)))")


def create_simple_eg():
excs = []
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Ensure the repr for :exc:`ExceptionGroup` and :exc:`BaseExceptionGroup` does
not change when its original exception list is mutated.
45 changes: 35 additions & 10 deletions Objects/exceptions.c
Original file line number Diff line number Diff line change
Expand Up @@ -694,12 +694,12 @@ PyTypeObject _PyExc_ ## EXCNAME = { \

#define ComplexExtendsException(EXCBASE, EXCNAME, EXCSTORE, EXCNEW, \
EXCMETHODS, EXCMEMBERS, EXCGETSET, \
EXCSTR, EXCDOC) \
EXCSTR, EXCREPR, EXCDOC) \
static PyTypeObject _PyExc_ ## EXCNAME = { \
PyVarObject_HEAD_INIT(NULL, 0) \
# EXCNAME, \
sizeof(Py ## EXCSTORE ## Object), 0, \
EXCSTORE ## _dealloc, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, \
EXCSTORE ## _dealloc, 0, 0, 0, 0, EXCREPR, 0, 0, 0, 0, 0, \
EXCSTR, 0, 0, 0, \
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, \
PyDoc_STR(EXCDOC), EXCSTORE ## _traverse, \
Expand Down Expand Up @@ -792,7 +792,7 @@ StopIteration_traverse(PyObject *op, visitproc visit, void *arg)
}

ComplexExtendsException(PyExc_Exception, StopIteration, StopIteration,
0, 0, StopIteration_members, 0, 0,
0, 0, StopIteration_members, 0, 0, 0,
"Signal the end from iterator.__next__().");


Expand Down Expand Up @@ -865,7 +865,7 @@ static PyMemberDef SystemExit_members[] = {
};

ComplexExtendsException(PyExc_BaseException, SystemExit, SystemExit,
0, 0, SystemExit_members, 0, 0,
0, 0, SystemExit_members, 0, 0, 0,
"Request to exit from the interpreter.");

/*
Expand Down Expand Up @@ -1063,6 +1063,31 @@ BaseExceptionGroup_str(PyObject *op)
self->msg, num_excs, num_excs > 1 ? "s" : "");
}

static PyObject *
BaseExceptionGroup_repr(PyObject *op)
{
PyBaseExceptionGroupObject *self = PyBaseExceptionGroupObject_CAST(op);
assert(self->msg);
assert(self->excs);

/* Use the actual exceptions tuple for accuracy, but make it look like the
* original exception sequence, if possible, for backwards compatibility. */
PyObject* excs_orig = PyTuple_GET_ITEM(self->args, 1);
if (PyList_Check(excs_orig)) {
excs_orig = PySequence_List(self->excs);
} else {
excs_orig = Py_NewRef(self->excs);
}

const char *name = _PyType_Name(Py_TYPE(self));
PyObject *repr = PyUnicode_FromFormat(
"%s(%R, %R)", name,
self->msg, excs_orig);

Py_DECREF(excs_orig);
return repr;
}

/*[clinic input]
@critical_section
BaseExceptionGroup.derive
Expand Down Expand Up @@ -1697,7 +1722,7 @@ static PyMethodDef BaseExceptionGroup_methods[] = {
ComplexExtendsException(PyExc_BaseException, BaseExceptionGroup,
BaseExceptionGroup, BaseExceptionGroup_new /* new */,
BaseExceptionGroup_methods, BaseExceptionGroup_members,
0 /* getset */, BaseExceptionGroup_str,
0 /* getset */, BaseExceptionGroup_str, BaseExceptionGroup_repr,
"A combination of multiple unrelated exceptions.");

/*
Expand Down Expand Up @@ -2425,7 +2450,7 @@ static PyGetSetDef OSError_getset[] = {
ComplexExtendsException(PyExc_Exception, OSError,
OSError, OSError_new,
OSError_methods, OSError_members, OSError_getset,
OSError_str,
OSError_str, 0,
"Base class for I/O related errors.");


Expand Down Expand Up @@ -2566,7 +2591,7 @@ static PyMethodDef NameError_methods[] = {
ComplexExtendsException(PyExc_Exception, NameError,
NameError, 0,
NameError_methods, NameError_members,
0, BaseException_str, "Name not found globally.");
0, BaseException_str, 0, "Name not found globally.");

/*
* UnboundLocalError extends NameError
Expand Down Expand Up @@ -2700,7 +2725,7 @@ static PyMethodDef AttributeError_methods[] = {
ComplexExtendsException(PyExc_Exception, AttributeError,
AttributeError, 0,
AttributeError_methods, AttributeError_members,
0, BaseException_str, "Attribute not found.");
0, BaseException_str, 0, "Attribute not found.");

/*
* SyntaxError extends Exception
Expand Down Expand Up @@ -2899,7 +2924,7 @@ static PyMemberDef SyntaxError_members[] = {

ComplexExtendsException(PyExc_Exception, SyntaxError, SyntaxError,
0, 0, SyntaxError_members, 0,
SyntaxError_str, "Invalid syntax.");
SyntaxError_str, 0, "Invalid syntax.");


/*
Expand Down Expand Up @@ -2959,7 +2984,7 @@ KeyError_str(PyObject *op)
}

ComplexExtendsException(PyExc_LookupError, KeyError, BaseException,
0, 0, 0, 0, KeyError_str, "Mapping key not found.");
0, 0, 0, 0, KeyError_str, 0, "Mapping key not found.");


/*
Expand Down
Loading