diff --git a/Doc/data/stable_abi.dat b/Doc/data/stable_abi.dat index 46ee321b660..64a0a2a247c 100644 --- a/Doc/data/stable_abi.dat +++ b/Doc/data/stable_abi.dat @@ -189,6 +189,7 @@ var,PyExc_ArithmeticError,3.2, var,PyExc_AssertionError,3.2, var,PyExc_AttributeError,3.2, var,PyExc_BaseException,3.2, +var,PyExc_BaseExceptionGroup,3.11, var,PyExc_BlockingIOError,3.7, var,PyExc_BrokenPipeError,3.7, var,PyExc_BufferError,3.2, diff --git a/Include/cpython/pyerrors.h b/Include/cpython/pyerrors.h index a3ec5afdb7c..28ab565dde4 100644 --- a/Include/cpython/pyerrors.h +++ b/Include/cpython/pyerrors.h @@ -14,6 +14,12 @@ typedef struct { PyException_HEAD } PyBaseExceptionObject; +typedef struct { + PyException_HEAD + PyObject *msg; + PyObject *excs; +} PyBaseExceptionGroupObject; + typedef struct { PyException_HEAD PyObject *msg; diff --git a/Include/internal/pycore_interp.h b/Include/internal/pycore_interp.h index 64ac3abe00f..c16f0a4b5e6 100644 --- a/Include/internal/pycore_interp.h +++ b/Include/internal/pycore_interp.h @@ -205,6 +205,8 @@ struct _Py_exc_state { PyObject *errnomap; PyBaseExceptionObject *memerrors_freelist; int memerrors_numfree; + // The ExceptionGroup type + PyObject *PyExc_ExceptionGroup; }; diff --git a/Include/internal/pycore_pylifecycle.h b/Include/internal/pycore_pylifecycle.h index 4f12fef8d65..53b94748b32 100644 --- a/Include/internal/pycore_pylifecycle.h +++ b/Include/internal/pycore_pylifecycle.h @@ -93,6 +93,7 @@ extern void _PyAsyncGen_Fini(PyInterpreterState *interp); extern int _PySignal_Init(int install_signal_handlers); extern void _PySignal_Fini(void); +extern void _PyExc_ClearExceptionGroupType(PyInterpreterState *interp); extern void _PyExc_Fini(PyInterpreterState *interp); extern void _PyImport_Fini(void); extern void _PyImport_Fini2(void); diff --git a/Include/pyerrors.h b/Include/pyerrors.h index c6c443a2d7d..77d791427d4 100644 --- a/Include/pyerrors.h +++ b/Include/pyerrors.h @@ -60,11 +60,14 @@ PyAPI_FUNC(const char *) PyExceptionClass_Name(PyObject *); #define PyExceptionInstance_Class(x) ((PyObject*)Py_TYPE(x)) +#define _PyBaseExceptionGroup_Check(x) \ + PyObject_TypeCheck(x, (PyTypeObject *)PyExc_BaseExceptionGroup) /* Predefined exceptions */ PyAPI_DATA(PyObject *) PyExc_BaseException; PyAPI_DATA(PyObject *) PyExc_Exception; +PyAPI_DATA(PyObject *) PyExc_BaseExceptionGroup; #if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x03050000 PyAPI_DATA(PyObject *) PyExc_StopAsyncIteration; #endif diff --git a/Lib/test/exception_hierarchy.txt b/Lib/test/exception_hierarchy.txt index cf54454e71a..5c0bfda3737 100644 --- a/Lib/test/exception_hierarchy.txt +++ b/Lib/test/exception_hierarchy.txt @@ -2,7 +2,9 @@ BaseException ├── SystemExit ├── KeyboardInterrupt ├── GeneratorExit + ├── BaseExceptionGroup └── Exception + ├── ExceptionGroup [BaseExceptionGroup] ├── StopIteration ├── StopAsyncIteration ├── ArithmeticError diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py index a5404b30d24..a4131bec602 100644 --- a/Lib/test/test_descr.py +++ b/Lib/test/test_descr.py @@ -4032,7 +4032,11 @@ order (MRO) for bases """ for tp in builtin_types: object.__getattribute__(tp, "__bases__") if tp is not object: - self.assertEqual(len(tp.__bases__), 1, tp) + if tp is ExceptionGroup: + num_bases = 2 + else: + num_bases = 1 + self.assertEqual(len(tp.__bases__), num_bases, tp) class L(list): pass diff --git a/Lib/test/test_doctest.py b/Lib/test/test_doctest.py index 3524a0a797c..8423cafa8c7 100644 --- a/Lib/test/test_doctest.py +++ b/Lib/test/test_doctest.py @@ -668,7 +668,7 @@ plain ol' Python and is guaranteed to be available. >>> import builtins >>> tests = doctest.DocTestFinder().find(builtins) - >>> 820 < len(tests) < 840 # approximate number of objects with docstrings + >>> 825 < len(tests) < 845 # approximate number of objects with docstrings True >>> real_tests = [t for t in tests if len(t.examples) > 0] >>> len(real_tests) # objects that actually have doctests diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py new file mode 100644 index 00000000000..5bb6094cde7 --- /dev/null +++ b/Lib/test/test_exception_group.py @@ -0,0 +1,808 @@ +import collections.abc +import traceback +import types +import unittest + + +class TestExceptionGroupTypeHierarchy(unittest.TestCase): + def test_exception_group_types(self): + self.assertTrue(issubclass(ExceptionGroup, Exception)) + self.assertTrue(issubclass(ExceptionGroup, BaseExceptionGroup)) + self.assertTrue(issubclass(BaseExceptionGroup, BaseException)) + + def test_exception_is_not_generic_type(self): + with self.assertRaises(TypeError): + Exception[OSError] + + def test_exception_group_is_generic_type(self): + E = OSError + self.assertIsInstance(ExceptionGroup[E], types.GenericAlias) + self.assertIsInstance(BaseExceptionGroup[E], types.GenericAlias) + + +class BadConstructorArgs(unittest.TestCase): + def test_bad_EG_construction__too_many_args(self): + MSG = 'function takes exactly 2 arguments' + with self.assertRaisesRegex(TypeError, MSG): + ExceptionGroup('no errors') + with self.assertRaisesRegex(TypeError, MSG): + ExceptionGroup([ValueError('no msg')]) + with self.assertRaisesRegex(TypeError, MSG): + ExceptionGroup('eg', [ValueError('too')], [TypeError('many')]) + + def test_bad_EG_construction__bad_message(self): + MSG = 'argument 1 must be str, not ' + with self.assertRaisesRegex(TypeError, MSG): + ExceptionGroup(ValueError(12), SyntaxError('bad syntax')) + with self.assertRaisesRegex(TypeError, MSG): + ExceptionGroup(None, [ValueError(12)]) + + def test_bad_EG_construction__bad_excs_sequence(self): + MSG = 'second argument \(exceptions\) must be a sequence' + with self.assertRaisesRegex(TypeError, MSG): + ExceptionGroup('errors not sequence', {ValueError(42)}) + with self.assertRaisesRegex(TypeError, MSG): + ExceptionGroup("eg", None) + + MSG = 'second argument \(exceptions\) must be a non-empty sequence' + with self.assertRaisesRegex(ValueError, MSG): + ExceptionGroup("eg", []) + + def test_bad_EG_construction__nested_non_exceptions(self): + MSG = ('Item [0-9]+ of second argument \(exceptions\)' + ' is not an exception') + with self.assertRaisesRegex(ValueError, MSG): + ExceptionGroup('expect instance, not type', [OSError]); + with self.assertRaisesRegex(ValueError, MSG): + ExceptionGroup('bad error', ["not an exception"]) + + +class InstanceCreation(unittest.TestCase): + def test_EG_wraps_Exceptions__creates_EG(self): + excs = [ValueError(1), TypeError(2)] + self.assertIs( + type(ExceptionGroup("eg", excs)), + ExceptionGroup) + + def test_BEG_wraps_Exceptions__creates_EG(self): + excs = [ValueError(1), TypeError(2)] + self.assertIs( + type(BaseExceptionGroup("beg", excs)), + ExceptionGroup) + + def test_EG_wraps_BaseException__raises_TypeError(self): + MSG= "Cannot nest BaseExceptions in an ExceptionGroup" + with self.assertRaisesRegex(TypeError, MSG): + eg = ExceptionGroup("eg", [ValueError(1), KeyboardInterrupt(2)]) + + def test_BEG_wraps_BaseException__creates_BEG(self): + beg = BaseExceptionGroup("beg", [ValueError(1), KeyboardInterrupt(2)]) + self.assertIs(type(beg), BaseExceptionGroup) + + def test_EG_subclass_wraps_anything(self): + class MyEG(ExceptionGroup): + pass + + self.assertIs( + type(MyEG("eg", [ValueError(12), TypeError(42)])), + MyEG) + self.assertIs( + type(MyEG("eg", [ValueError(12), KeyboardInterrupt(42)])), + MyEG) + + def test_BEG_subclass_wraps_anything(self): + class MyBEG(BaseExceptionGroup): + pass + + self.assertIs( + type(MyBEG("eg", [ValueError(12), TypeError(42)])), + MyBEG) + self.assertIs( + type(MyBEG("eg", [ValueError(12), KeyboardInterrupt(42)])), + MyBEG) + + +def create_simple_eg(): + excs = [] + try: + try: + raise MemoryError("context and cause for ValueError(1)") + except MemoryError as e: + raise ValueError(1) from e + except ValueError as e: + excs.append(e) + + try: + try: + raise OSError("context for TypeError") + except OSError as e: + raise TypeError(int) + except TypeError as e: + excs.append(e) + + try: + try: + raise ImportError("context for ValueError(2)") + except ImportError as e: + raise ValueError(2) + except ValueError as e: + excs.append(e) + + try: + raise ExceptionGroup('simple eg', excs) + except ExceptionGroup as e: + return e + + +class ExceptionGroupFields(unittest.TestCase): + def test_basics_ExceptionGroup_fields(self): + eg = create_simple_eg() + + # check msg + self.assertEqual(eg.message, 'simple eg') + self.assertEqual(eg.args[0], 'simple eg') + + # check cause and context + self.assertIsInstance(eg.exceptions[0], ValueError) + self.assertIsInstance(eg.exceptions[0].__cause__, MemoryError) + self.assertIsInstance(eg.exceptions[0].__context__, MemoryError) + self.assertIsInstance(eg.exceptions[1], TypeError) + self.assertIsNone(eg.exceptions[1].__cause__) + self.assertIsInstance(eg.exceptions[1].__context__, OSError) + self.assertIsInstance(eg.exceptions[2], ValueError) + self.assertIsNone(eg.exceptions[2].__cause__) + self.assertIsInstance(eg.exceptions[2].__context__, ImportError) + + # check tracebacks + line0 = create_simple_eg.__code__.co_firstlineno + tb_linenos = [line0 + 27, + [line0 + 6, line0 + 14, line0 + 22]] + self.assertEqual(eg.__traceback__.tb_lineno, tb_linenos[0]) + self.assertIsNone(eg.__traceback__.tb_next) + for i in range(3): + tb = eg.exceptions[i].__traceback__ + self.assertIsNone(tb.tb_next) + self.assertEqual(tb.tb_lineno, tb_linenos[1][i]) + + def test_fields_are_readonly(self): + eg = ExceptionGroup('eg', [TypeError(1), OSError(2)]) + + self.assertEqual(type(eg.exceptions), tuple) + + eg.message + with self.assertRaises(AttributeError): + eg.message = "new msg" + + eg.exceptions + with self.assertRaises(AttributeError): + eg.exceptions = [OSError('xyz')] + + +class ExceptionGroupTestBase(unittest.TestCase): + def assertMatchesTemplate(self, exc, exc_type, template): + """ Assert that the exception matches the template + + A template describes the shape of exc. If exc is a + leaf exception (i.e., not an exception group) then + template is an exception instance that has the + expected type and args value of exc. If exc is an + exception group, then template is a list of the + templates of its nested exceptions. + """ + if exc_type is not None: + self.assertIs(type(exc), exc_type) + + if isinstance(exc, BaseExceptionGroup): + self.assertIsInstance(template, collections.abc.Sequence) + self.assertEqual(len(exc.exceptions), len(template)) + for e, t in zip(exc.exceptions, template): + self.assertMatchesTemplate(e, None, t) + else: + self.assertIsInstance(template, BaseException) + self.assertEqual(type(exc), type(template)) + self.assertEqual(exc.args, template.args) + + +class ExceptionGroupSubgroupTests(ExceptionGroupTestBase): + def setUp(self): + self.eg = create_simple_eg() + self.eg_template = [ValueError(1), TypeError(int), ValueError(2)] + + def test_basics_subgroup_split__bad_arg_type(self): + bad_args = ["bad arg", + OSError('instance not type'), + [OSError('instance not type')],] + for arg in bad_args: + with self.assertRaises(TypeError): + self.eg.subgroup(arg) + with self.assertRaises(TypeError): + self.eg.split(arg) + + def test_basics_subgroup_by_type__passthrough(self): + eg = self.eg + self.assertIs(eg, eg.subgroup(BaseException)) + self.assertIs(eg, eg.subgroup(Exception)) + self.assertIs(eg, eg.subgroup(BaseExceptionGroup)) + self.assertIs(eg, eg.subgroup(ExceptionGroup)) + + def test_basics_subgroup_by_type__no_match(self): + self.assertIsNone(self.eg.subgroup(OSError)) + + def test_basics_subgroup_by_type__match(self): + eg = self.eg + testcases = [ + # (match_type, result_template) + (ValueError, [ValueError(1), ValueError(2)]), + (TypeError, [TypeError(int)]), + ((ValueError, TypeError), self.eg_template)] + + for match_type, template in testcases: + with self.subTest(match=match_type): + subeg = eg.subgroup(match_type) + self.assertEqual(subeg.message, eg.message) + self.assertMatchesTemplate(subeg, ExceptionGroup, template) + + def test_basics_subgroup_by_predicate__passthrough(self): + self.assertIs(self.eg, self.eg.subgroup(lambda e: True)) + + def test_basics_subgroup_by_predicate__no_match(self): + self.assertIsNone(self.eg.subgroup(lambda e: False)) + + def test_basics_subgroup_by_predicate__match(self): + eg = self.eg + testcases = [ + # (match_type, result_template) + (ValueError, [ValueError(1), ValueError(2)]), + (TypeError, [TypeError(int)]), + ((ValueError, TypeError), self.eg_template)] + + for match_type, template in testcases: + subeg = eg.subgroup(lambda e: isinstance(e, match_type)) + self.assertEqual(subeg.message, eg.message) + self.assertMatchesTemplate(subeg, ExceptionGroup, template) + + +class ExceptionGroupSplitTests(ExceptionGroupTestBase): + def setUp(self): + self.eg = create_simple_eg() + self.eg_template = [ValueError(1), TypeError(int), ValueError(2)] + + def test_basics_split_by_type__passthrough(self): + for E in [BaseException, Exception, + BaseExceptionGroup, ExceptionGroup]: + match, rest = self.eg.split(E) + self.assertMatchesTemplate( + match, ExceptionGroup, self.eg_template) + self.assertIsNone(rest) + + def test_basics_split_by_type__no_match(self): + match, rest = self.eg.split(OSError) + self.assertIsNone(match) + self.assertMatchesTemplate( + rest, ExceptionGroup, self.eg_template) + + def test_basics_split_by_type__match(self): + eg = self.eg + VE = ValueError + TE = TypeError + testcases = [ + # (matcher, match_template, rest_template) + (VE, [VE(1), VE(2)], [TE(int)]), + (TE, [TE(int)], [VE(1), VE(2)]), + ((VE, TE), self.eg_template, None), + ((OSError, VE), [VE(1), VE(2)], [TE(int)]), + ] + + for match_type, match_template, rest_template in testcases: + match, rest = eg.split(match_type) + self.assertEqual(match.message, eg.message) + self.assertMatchesTemplate( + match, ExceptionGroup, match_template) + if rest_template is not None: + self.assertEqual(rest.message, eg.message) + self.assertMatchesTemplate( + rest, ExceptionGroup, rest_template) + else: + self.assertIsNone(rest) + + def test_basics_split_by_predicate__passthrough(self): + match, rest = self.eg.split(lambda e: True) + self.assertMatchesTemplate(match, ExceptionGroup, self.eg_template) + self.assertIsNone(rest) + + def test_basics_split_by_predicate__no_match(self): + match, rest = self.eg.split(lambda e: False) + self.assertIsNone(match) + self.assertMatchesTemplate(rest, ExceptionGroup, self.eg_template) + + def test_basics_split_by_predicate__match(self): + eg = self.eg + VE = ValueError + TE = TypeError + testcases = [ + # (matcher, match_template, rest_template) + (VE, [VE(1), VE(2)], [TE(int)]), + (TE, [TE(int)], [VE(1), VE(2)]), + ((VE, TE), self.eg_template, None), + ] + + for match_type, match_template, rest_template in testcases: + match, rest = eg.split(lambda e: isinstance(e, match_type)) + self.assertEqual(match.message, eg.message) + self.assertMatchesTemplate( + match, ExceptionGroup, match_template) + if rest_template is not None: + self.assertEqual(rest.message, eg.message) + self.assertMatchesTemplate( + rest, ExceptionGroup, rest_template) + + +class DeepRecursionInSplitAndSubgroup(unittest.TestCase): + def make_deep_eg(self): + e = TypeError(1) + for i in range(2000): + e = ExceptionGroup('eg', [e]) + return e + + def test_deep_split(self): + e = self.make_deep_eg() + with self.assertRaises(RecursionError): + e.split(TypeError) + + def test_deep_subgroup(self): + e = self.make_deep_eg() + with self.assertRaises(RecursionError): + e.subgroup(TypeError) + + +def leaf_generator(exc, tbs=None): + if tbs is None: + tbs = [] + tbs.append(exc.__traceback__) + if isinstance(exc, BaseExceptionGroup): + for e in exc.exceptions: + yield from leaf_generator(e, tbs) + else: + # exc is a leaf exception and its traceback + # is the concatenation of the traceback + # segments in tbs + yield exc, tbs + tbs.pop() + + +class LeafGeneratorTest(unittest.TestCase): + # The leaf_generator is mentioned in PEP 654 as a suggestion + # on how to iterate over leaf nodes of an EG. Is is also + # used below as a test utility. So we test it here. + + def test_leaf_generator(self): + eg = create_simple_eg() + + self.assertSequenceEqual( + [e for e, _ in leaf_generator(eg)], + eg.exceptions) + + for e, tbs in leaf_generator(eg): + self.assertSequenceEqual( + tbs, [eg.__traceback__, e.__traceback__]) + + +def create_nested_eg(): + excs = [] + try: + try: + raise TypeError(bytes) + except TypeError as e: + raise ExceptionGroup("nested", [e]) + except ExceptionGroup as e: + excs.append(e) + + try: + try: + raise MemoryError('out of memory') + except MemoryError as e: + raise ValueError(1) from e + except ValueError as e: + excs.append(e) + + try: + raise ExceptionGroup("root", excs) + except ExceptionGroup as eg: + return eg + + +class NestedExceptionGroupBasicsTest(ExceptionGroupTestBase): + def test_nested_group_matches_template(self): + eg = create_nested_eg() + self.assertMatchesTemplate( + eg, + ExceptionGroup, + [[TypeError(bytes)], ValueError(1)]) + + def test_nested_group_chaining(self): + eg = create_nested_eg() + self.assertIsInstance(eg.exceptions[1].__context__, MemoryError) + self.assertIsInstance(eg.exceptions[1].__cause__, MemoryError) + self.assertIsInstance(eg.exceptions[0].__context__, TypeError) + + def test_nested_exception_group_tracebacks(self): + eg = create_nested_eg() + + line0 = create_nested_eg.__code__.co_firstlineno + for (tb, expected) in [ + (eg.__traceback__, line0 + 19), + (eg.exceptions[0].__traceback__, line0 + 6), + (eg.exceptions[1].__traceback__, line0 + 14), + (eg.exceptions[0].exceptions[0].__traceback__, line0 + 4), + ]: + self.assertEqual(tb.tb_lineno, expected) + self.assertIsNone(tb.tb_next) + + def test_iteration_full_tracebacks(self): + eg = create_nested_eg() + # check that iteration over leaves + # produces the expected tracebacks + self.assertEqual(len(list(leaf_generator(eg))), 2) + + line0 = create_nested_eg.__code__.co_firstlineno + expected_tbs = [ [line0 + 19, line0 + 6, line0 + 4], + [line0 + 19, line0 + 14]] + + for (i, (_, tbs)) in enumerate(leaf_generator(eg)): + self.assertSequenceEqual( + [tb.tb_lineno for tb in tbs], + expected_tbs[i]) + + +class ExceptionGroupSplitTestBase(ExceptionGroupTestBase): + + def split_exception_group(self, eg, types): + """ Split an EG and do some sanity checks on the result """ + self.assertIsInstance(eg, BaseExceptionGroup) + + match, rest = eg.split(types) + sg = eg.subgroup(types) + + if match is not None: + self.assertIsInstance(match, BaseExceptionGroup) + for e,_ in leaf_generator(match): + self.assertIsInstance(e, types) + + self.assertIsNotNone(sg) + self.assertIsInstance(sg, BaseExceptionGroup) + for e,_ in leaf_generator(sg): + self.assertIsInstance(e, types) + + if rest is not None: + self.assertIsInstance(rest, BaseExceptionGroup) + + def leaves(exc): + return [] if exc is None else [e for e,_ in leaf_generator(exc)] + + # match and subgroup have the same leaves + self.assertSequenceEqual(leaves(match), leaves(sg)) + + match_leaves = leaves(match) + rest_leaves = leaves(rest) + # each leaf exception of eg is in exactly one of match and rest + self.assertEqual( + len(leaves(eg)), + len(leaves(match)) + len(leaves(rest))) + + for e in leaves(eg): + self.assertNotEqual( + match and e in match_leaves, + rest and e in rest_leaves) + + # message, cause and context equal to eg + for part in [match, rest, sg]: + if part is not None: + self.assertEqual(eg.message, part.message) + self.assertIs(eg.__cause__, part.__cause__) + self.assertIs(eg.__context__, part.__context__) + self.assertIs(eg.__traceback__, part.__traceback__) + + def tbs_for_leaf(leaf, eg): + for e, tbs in leaf_generator(eg): + if e is leaf: + return tbs + + def tb_linenos(tbs): + return [tb.tb_lineno for tb in tbs if tb] + + # full tracebacks match + for part in [match, rest, sg]: + for e in leaves(part): + self.assertSequenceEqual( + tb_linenos(tbs_for_leaf(e, eg)), + tb_linenos(tbs_for_leaf(e, part))) + + return match, rest + + +class NestedExceptionGroupSplitTest(ExceptionGroupSplitTestBase): + + def test_split_by_type(self): + class MyExceptionGroup(ExceptionGroup): + pass + + def raiseVE(v): + raise ValueError(v) + + def raiseTE(t): + raise TypeError(t) + + def nested_group(): + def level1(i): + excs = [] + for f, arg in [(raiseVE, i), (raiseTE, int), (raiseVE, i+1)]: + try: + f(arg) + except Exception as e: + excs.append(e) + raise ExceptionGroup('msg1', excs) + + def level2(i): + excs = [] + for f, arg in [(level1, i), (level1, i+1), (raiseVE, i+2)]: + try: + f(arg) + except Exception as e: + excs.append(e) + raise MyExceptionGroup('msg2', excs) + + def level3(i): + excs = [] + for f, arg in [(level2, i+1), (raiseVE, i+2)]: + try: + f(arg) + except Exception as e: + excs.append(e) + raise ExceptionGroup('msg3', excs) + + level3(5) + + try: + nested_group() + except ExceptionGroup as e: + eg = e + + eg_template = [ + [ + [ValueError(6), TypeError(int), ValueError(7)], + [ValueError(7), TypeError(int), ValueError(8)], + ValueError(8), + ], + ValueError(7)] + + valueErrors_template = [ + [ + [ValueError(6), ValueError(7)], + [ValueError(7), ValueError(8)], + ValueError(8), + ], + ValueError(7)] + + typeErrors_template = [[[TypeError(int)], [TypeError(int)]]] + + self.assertMatchesTemplate(eg, ExceptionGroup, eg_template) + + # Match Nothing + match, rest = self.split_exception_group(eg, SyntaxError) + self.assertIsNone(match) + self.assertMatchesTemplate(rest, ExceptionGroup, eg_template) + + # Match Everything + match, rest = self.split_exception_group(eg, BaseException) + self.assertMatchesTemplate(match, ExceptionGroup, eg_template) + self.assertIsNone(rest) + match, rest = self.split_exception_group(eg, (ValueError, TypeError)) + self.assertMatchesTemplate(match, ExceptionGroup, eg_template) + self.assertIsNone(rest) + + # Match ValueErrors + match, rest = self.split_exception_group(eg, ValueError) + self.assertMatchesTemplate(match, ExceptionGroup, valueErrors_template) + self.assertMatchesTemplate(rest, ExceptionGroup, typeErrors_template) + + # Match TypeErrors + match, rest = self.split_exception_group(eg, (TypeError, SyntaxError)) + self.assertMatchesTemplate(match, ExceptionGroup, typeErrors_template) + self.assertMatchesTemplate(rest, ExceptionGroup, valueErrors_template) + + # Match ExceptionGroup + match, rest = eg.split(ExceptionGroup) + self.assertIs(match, eg) + self.assertIsNone(rest) + + # Match MyExceptionGroup (ExceptionGroup subclass) + match, rest = eg.split(MyExceptionGroup) + self.assertMatchesTemplate(match, ExceptionGroup, [eg_template[0]]) + self.assertMatchesTemplate(rest, ExceptionGroup, [eg_template[1]]) + + def test_split_BaseExceptionGroup(self): + def exc(ex): + try: + raise ex + except BaseException as e: + return e + + try: + raise BaseExceptionGroup( + "beg", [exc(ValueError(1)), exc(KeyboardInterrupt(2))]) + except BaseExceptionGroup as e: + beg = e + + # Match Nothing + match, rest = self.split_exception_group(beg, TypeError) + self.assertIsNone(match) + self.assertMatchesTemplate( + rest, BaseExceptionGroup, [ValueError(1), KeyboardInterrupt(2)]) + + # Match Everything + match, rest = self.split_exception_group( + beg, (ValueError, KeyboardInterrupt)) + self.assertMatchesTemplate( + match, BaseExceptionGroup, [ValueError(1), KeyboardInterrupt(2)]) + self.assertIsNone(rest) + + # Match ValueErrors + match, rest = self.split_exception_group(beg, ValueError) + self.assertMatchesTemplate( + match, ExceptionGroup, [ValueError(1)]) + self.assertMatchesTemplate( + rest, BaseExceptionGroup, [KeyboardInterrupt(2)]) + + # Match KeyboardInterrupts + match, rest = self.split_exception_group(beg, KeyboardInterrupt) + self.assertMatchesTemplate( + match, BaseExceptionGroup, [KeyboardInterrupt(2)]) + self.assertMatchesTemplate( + rest, ExceptionGroup, [ValueError(1)]) + + +class NestedExceptionGroupSubclassSplitTest(ExceptionGroupSplitTestBase): + + def test_split_ExceptionGroup_subclass_no_derive_no_new_override(self): + class EG(ExceptionGroup): + pass + + try: + try: + try: + raise TypeError(2) + except TypeError as te: + raise EG("nested", [te]) + except EG as nested: + try: + raise ValueError(1) + except ValueError as ve: + raise EG("eg", [ve, nested]) + except EG as e: + eg = e + + self.assertMatchesTemplate(eg, EG, [ValueError(1), [TypeError(2)]]) + + # Match Nothing + match, rest = self.split_exception_group(eg, OSError) + self.assertIsNone(match) + self.assertMatchesTemplate( + rest, ExceptionGroup, [ValueError(1), [TypeError(2)]]) + + # Match Everything + match, rest = self.split_exception_group(eg, (ValueError, TypeError)) + self.assertMatchesTemplate( + match, ExceptionGroup, [ValueError(1), [TypeError(2)]]) + self.assertIsNone(rest) + + # Match ValueErrors + match, rest = self.split_exception_group(eg, ValueError) + self.assertMatchesTemplate(match, ExceptionGroup, [ValueError(1)]) + self.assertMatchesTemplate(rest, ExceptionGroup, [[TypeError(2)]]) + + # Match TypeErrors + match, rest = self.split_exception_group(eg, TypeError) + self.assertMatchesTemplate(match, ExceptionGroup, [[TypeError(2)]]) + self.assertMatchesTemplate(rest, ExceptionGroup, [ValueError(1)]) + + def test_split_BaseExceptionGroup_subclass_no_derive_new_override(self): + class EG(BaseExceptionGroup): + def __new__(cls, message, excs, unused): + # The "unused" arg is here to show that split() doesn't call + # the actual class constructor from the default derive() + # implementation (it would fail on unused arg if so because + # it assumes the BaseExceptionGroup.__new__ signature). + return super().__new__(cls, message, excs) + + try: + raise EG("eg", [ValueError(1), KeyboardInterrupt(2)], "unused") + except EG as e: + eg = e + + self.assertMatchesTemplate( + eg, EG, [ValueError(1), KeyboardInterrupt(2)]) + + # Match Nothing + match, rest = self.split_exception_group(eg, OSError) + self.assertIsNone(match) + self.assertMatchesTemplate( + rest, BaseExceptionGroup, [ValueError(1), KeyboardInterrupt(2)]) + + # Match Everything + match, rest = self.split_exception_group( + eg, (ValueError, KeyboardInterrupt)) + self.assertMatchesTemplate( + match, BaseExceptionGroup, [ValueError(1), KeyboardInterrupt(2)]) + self.assertIsNone(rest) + + # Match ValueErrors + match, rest = self.split_exception_group(eg, ValueError) + self.assertMatchesTemplate(match, ExceptionGroup, [ValueError(1)]) + self.assertMatchesTemplate( + rest, BaseExceptionGroup, [KeyboardInterrupt(2)]) + + # Match KeyboardInterrupt + match, rest = self.split_exception_group(eg, KeyboardInterrupt) + self.assertMatchesTemplate( + match, BaseExceptionGroup, [KeyboardInterrupt(2)]) + self.assertMatchesTemplate(rest, ExceptionGroup, [ValueError(1)]) + + def test_split_ExceptionGroup_subclass_derive_and_new_overrides(self): + class EG(ExceptionGroup): + def __new__(cls, message, excs, code): + obj = super().__new__(cls, message, excs) + obj.code = code + return obj + + def derive(self, excs): + return EG(self.message, excs, self.code) + + try: + try: + try: + raise TypeError(2) + except TypeError as te: + raise EG("nested", [te], 101) + except EG as nested: + try: + raise ValueError(1) + except ValueError as ve: + raise EG("eg", [ve, nested], 42) + except EG as e: + eg = e + + self.assertMatchesTemplate(eg, EG, [ValueError(1), [TypeError(2)]]) + + # Match Nothing + match, rest = self.split_exception_group(eg, OSError) + self.assertIsNone(match) + self.assertMatchesTemplate(rest, EG, [ValueError(1), [TypeError(2)]]) + self.assertEqual(rest.code, 42) + self.assertEqual(rest.exceptions[1].code, 101) + + # Match Everything + match, rest = self.split_exception_group(eg, (ValueError, TypeError)) + self.assertMatchesTemplate(match, EG, [ValueError(1), [TypeError(2)]]) + self.assertEqual(match.code, 42) + self.assertEqual(match.exceptions[1].code, 101) + self.assertIsNone(rest) + + # Match ValueErrors + match, rest = self.split_exception_group(eg, ValueError) + self.assertMatchesTemplate(match, EG, [ValueError(1)]) + self.assertEqual(match.code, 42) + self.assertMatchesTemplate(rest, EG, [[TypeError(2)]]) + self.assertEqual(rest.code, 42) + self.assertEqual(rest.exceptions[0].code, 101) + + # Match TypeErrors + match, rest = self.split_exception_group(eg, TypeError) + self.assertMatchesTemplate(match, EG, [[TypeError(2)]]) + self.assertEqual(match.code, 42) + self.assertEqual(match.exceptions[0].code, 101) + self.assertMatchesTemplate(rest, EG, [ValueError(1)]) + self.assertEqual(rest.code, 42) + + +if __name__ == '__main__': + unittest.main() diff --git a/Lib/test/test_pickle.py b/Lib/test/test_pickle.py index 8775ff4b791..057af21e71f 100644 --- a/Lib/test/test_pickle.py +++ b/Lib/test/test_pickle.py @@ -489,7 +489,9 @@ class CompatPickleTests(unittest.TestCase): ResourceWarning, StopAsyncIteration, RecursionError, - EncodingWarning): + EncodingWarning, + BaseExceptionGroup, + ExceptionGroup): continue if exc is not OSError and issubclass(exc, OSError): self.assertEqual(reverse_mapping('builtins', name), diff --git a/Lib/test/test_stable_abi_ctypes.py b/Lib/test/test_stable_abi_ctypes.py index 750aa181083..1e27bcaf889 100644 --- a/Lib/test/test_stable_abi_ctypes.py +++ b/Lib/test/test_stable_abi_ctypes.py @@ -198,6 +198,7 @@ SYMBOL_NAMES = ( "PyExc_AssertionError", "PyExc_AttributeError", "PyExc_BaseException", + "PyExc_BaseExceptionGroup", "PyExc_BlockingIOError", "PyExc_BrokenPipeError", "PyExc_BufferError", diff --git a/Misc/NEWS.d/next/Core and Builtins/2021-09-26-18-18-50.bpo-45292.aX5HVr.rst b/Misc/NEWS.d/next/Core and Builtins/2021-09-26-18-18-50.bpo-45292.aX5HVr.rst new file mode 100644 index 00000000000..ee48b6d5105 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2021-09-26-18-18-50.bpo-45292.aX5HVr.rst @@ -0,0 +1 @@ +Implement :pep:`654` Add :class:`ExceptionGroup` and :class:`BaseExceptionGroup`. diff --git a/Misc/stable_abi.txt b/Misc/stable_abi.txt index 23e5b96a0e8..9f5a85bdec4 100644 --- a/Misc/stable_abi.txt +++ b/Misc/stable_abi.txt @@ -619,6 +619,8 @@ data PyExc_AttributeError added 3.2 data PyExc_BaseException added 3.2 +data PyExc_BaseExceptionGroup + added 3.11 data PyExc_BufferError added 3.2 data PyExc_BytesWarning diff --git a/Objects/exceptions.c b/Objects/exceptions.c index a9ea42c9842..a5459da89a0 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -6,6 +6,7 @@ #define PY_SSIZE_T_CLEAN #include +#include #include "pycore_initconfig.h" #include "pycore_object.h" #include "structmember.h" // PyMemberDef @@ -540,7 +541,7 @@ StopIteration_clear(PyStopIterationObject *self) static void StopIteration_dealloc(PyStopIterationObject *self) { - _PyObject_GC_UNTRACK(self); + PyObject_GC_UnTrack(self); StopIteration_clear(self); Py_TYPE(self)->tp_free((PyObject *)self); } @@ -629,6 +630,516 @@ ComplexExtendsException(PyExc_BaseException, SystemExit, SystemExit, 0, 0, SystemExit_members, 0, 0, "Request to exit from the interpreter."); +/* + * BaseExceptionGroup extends BaseException + * ExceptionGroup extends BaseExceptionGroup and Exception + */ + + +static inline PyBaseExceptionGroupObject* +_PyBaseExceptionGroupObject_cast(PyObject *exc) +{ + assert(_PyBaseExceptionGroup_Check(exc)); + return (PyBaseExceptionGroupObject *)exc; +} + +static PyObject * +BaseExceptionGroup_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + struct _Py_exc_state *state = get_exc_state(); + PyTypeObject *PyExc_ExceptionGroup = + (PyTypeObject*)state->PyExc_ExceptionGroup; + + PyObject *message = NULL; + PyObject *exceptions = NULL; + + if (!PyArg_ParseTuple(args, "UO", &message, &exceptions)) { + return NULL; + } + + if (!PySequence_Check(exceptions)) { + PyErr_SetString( + PyExc_TypeError, + "second argument (exceptions) must be a sequence"); + return NULL; + } + + exceptions = PySequence_Tuple(exceptions); + if (!exceptions) { + return NULL; + } + + /* We are now holding a ref to the exceptions tuple */ + + Py_ssize_t numexcs = PyTuple_GET_SIZE(exceptions); + if (numexcs == 0) { + PyErr_SetString( + PyExc_ValueError, + "second argument (exceptions) must be a non-empty sequence"); + goto error; + } + + bool nested_base_exceptions = false; + for (Py_ssize_t i = 0; i < numexcs; i++) { + PyObject *exc = PyTuple_GET_ITEM(exceptions, i); + if (!exc) { + goto error; + } + if (!PyExceptionInstance_Check(exc)) { + PyErr_Format( + PyExc_ValueError, + "Item %d of second argument (exceptions) is not an exception", + i); + goto error; + } + int is_nonbase_exception = PyObject_IsInstance(exc, PyExc_Exception); + if (is_nonbase_exception < 0) { + goto error; + } + else if (is_nonbase_exception == 0) { + nested_base_exceptions = true; + } + } + + PyTypeObject *cls = type; + if (cls == PyExc_ExceptionGroup) { + if (nested_base_exceptions) { + PyErr_SetString(PyExc_TypeError, + "Cannot nest BaseExceptions in an ExceptionGroup"); + goto error; + } + } + else if (cls == (PyTypeObject*)PyExc_BaseExceptionGroup) { + if (!nested_base_exceptions) { + /* All nested exceptions are Exception subclasses, + * wrap them in an ExceptionGroup + */ + cls = PyExc_ExceptionGroup; + } + } + else { + /* Do nothing - we don't interfere with subclasses */ + } + + if (!cls) { + /* Don't crash during interpreter shutdown + * (PyExc_ExceptionGroup may have been cleared) + */ + cls = (PyTypeObject*)PyExc_BaseExceptionGroup; + } + PyBaseExceptionGroupObject *self = + _PyBaseExceptionGroupObject_cast(BaseException_new(cls, args, kwds)); + if (!self) { + goto error; + } + + self->msg = Py_NewRef(message); + self->excs = exceptions; + return (PyObject*)self; +error: + Py_DECREF(exceptions); + return NULL; +} + +static int +BaseExceptionGroup_init(PyBaseExceptionGroupObject *self, + PyObject *args, PyObject *kwds) +{ + if (!_PyArg_NoKeywords(Py_TYPE(self)->tp_name, kwds)) { + return -1; + } + if (BaseException_init((PyBaseExceptionObject *)self, args, kwds) == -1) { + return -1; + } + return 0; +} + +static int +BaseExceptionGroup_clear(PyBaseExceptionGroupObject *self) +{ + Py_CLEAR(self->msg); + Py_CLEAR(self->excs); + return BaseException_clear((PyBaseExceptionObject *)self); +} + +static void +BaseExceptionGroup_dealloc(PyBaseExceptionGroupObject *self) +{ + _PyObject_GC_UNTRACK(self); + BaseExceptionGroup_clear(self); + Py_TYPE(self)->tp_free((PyObject *)self); +} + +static int +BaseExceptionGroup_traverse(PyBaseExceptionGroupObject *self, + visitproc visit, void *arg) +{ + Py_VISIT(self->msg); + Py_VISIT(self->excs); + return BaseException_traverse((PyBaseExceptionObject *)self, visit, arg); +} + +static PyObject * +BaseExceptionGroup_str(PyBaseExceptionGroupObject *self) +{ + assert(self->msg); + assert(PyUnicode_Check(self->msg)); + return Py_NewRef(self->msg); +} + +static PyObject * +BaseExceptionGroup_derive(PyObject *self_, PyObject *args) +{ + PyBaseExceptionGroupObject *self = _PyBaseExceptionGroupObject_cast(self_); + PyObject *excs = NULL; + if (!PyArg_ParseTuple(args, "O", &excs)) { + return NULL; + } + PyObject *init_args = PyTuple_Pack(2, self->msg, excs); + if (!init_args) { + return NULL; + } + PyObject *eg = PyObject_CallObject( + PyExc_BaseExceptionGroup, init_args); + Py_DECREF(init_args); + return eg; +} + +static int +exceptiongroup_subset( + PyBaseExceptionGroupObject *_orig, PyObject *excs, PyObject **result) +{ + /* Sets *result to an ExceptionGroup wrapping excs with metadata from + * _orig. If excs is empty, sets *result to NULL. + * Returns 0 on success and -1 on error. + + * This function is used by split() to construct the match/rest parts, + * so excs is the matching or non-matching sub-sequence of orig->excs + * (this function does not verify that it is a subsequence). + */ + PyObject *orig = (PyObject *)_orig; + + *result = NULL; + Py_ssize_t num_excs = PySequence_Size(excs); + if (num_excs < 0) { + return -1; + } + else if (num_excs == 0) { + return 0; + } + + PyObject *eg = PyObject_CallMethod( + orig, "derive", "(O)", excs); + if (!eg) { + return -1; + } + + if (!_PyBaseExceptionGroup_Check(eg)) { + PyErr_SetString(PyExc_TypeError, + "derive must return an instance of BaseExceptionGroup"); + goto error; + } + + /* Now we hold a reference to the new eg */ + + PyObject *tb = PyException_GetTraceback(orig); + if (tb) { + int res = PyException_SetTraceback(eg, tb); + Py_DECREF(tb); + if (res == -1) { + goto error; + } + } + PyException_SetContext(eg, PyException_GetContext(orig)); + PyException_SetCause(eg, PyException_GetCause(orig)); + *result = eg; + return 0; +error: + Py_DECREF(eg); + return -1; +} + +typedef enum { + /* Exception type or tuple of thereof */ + EXCEPTION_GROUP_MATCH_BY_TYPE = 0, + /* A PyFunction returning True for matching exceptions */ + EXCEPTION_GROUP_MATCH_BY_PREDICATE = 1, + /* An instance or container thereof, checked with equality + * This matcher type is only used internally by the + * interpreter, it is not exposed to python code */ + EXCEPTION_GROUP_MATCH_INSTANCES = 2 +} _exceptiongroup_split_matcher_type; + +static int +get_matcher_type(PyObject *value, + _exceptiongroup_split_matcher_type *type) +{ + /* the python API supports only BY_TYPE and BY_PREDICATE */ + if (PyExceptionClass_Check(value) || + PyTuple_CheckExact(value)) { + *type = EXCEPTION_GROUP_MATCH_BY_TYPE; + return 0; + } + if (PyFunction_Check(value)) { + *type = EXCEPTION_GROUP_MATCH_BY_PREDICATE; + return 0; + } + PyErr_SetString( + PyExc_TypeError, + "expected a function, exception type or tuple of exception types"); + return -1; +} + +static int +exceptiongroup_split_check_match(PyObject *exc, + _exceptiongroup_split_matcher_type matcher_type, + PyObject *matcher_value) +{ + switch (matcher_type) { + case EXCEPTION_GROUP_MATCH_BY_TYPE: { + assert(PyExceptionClass_Check(matcher_value) || + PyTuple_CheckExact(matcher_value)); + return PyErr_GivenExceptionMatches(exc, matcher_value); + } + case EXCEPTION_GROUP_MATCH_BY_PREDICATE: { + assert(PyFunction_Check(matcher_value)); + PyObject *exc_matches = PyObject_CallOneArg(matcher_value, exc); + if (exc_matches == NULL) { + return -1; + } + int is_true = PyObject_IsTrue(exc_matches); + Py_DECREF(exc_matches); + return is_true; + } + case EXCEPTION_GROUP_MATCH_INSTANCES: { + if (PySequence_Check(matcher_value)) { + return PySequence_Contains(matcher_value, exc); + } + return matcher_value == exc; + } + } + return 0; +} + +typedef struct { + PyObject *match; + PyObject *rest; +} _exceptiongroup_split_result; + +static int +exceptiongroup_split_recursive(PyObject *exc, + _exceptiongroup_split_matcher_type matcher_type, + PyObject *matcher_value, + bool construct_rest, + _exceptiongroup_split_result *result) +{ + result->match = NULL; + result->rest = NULL; + + int is_match = exceptiongroup_split_check_match( + exc, matcher_type, matcher_value); + if (is_match < 0) { + return -1; + } + + if (is_match) { + /* Full match */ + result->match = Py_NewRef(exc); + return 0; + } + else if (!_PyBaseExceptionGroup_Check(exc)) { + /* Leaf exception and no match */ + if (construct_rest) { + result->rest = Py_NewRef(exc); + } + return 0; + } + + /* Partial match */ + + PyBaseExceptionGroupObject *eg = _PyBaseExceptionGroupObject_cast(exc); + assert(PyTuple_CheckExact(eg->excs)); + Py_ssize_t num_excs = PyTuple_Size(eg->excs); + if (num_excs < 0) { + return -1; + } + assert(num_excs > 0); /* checked in constructor, and excs is read-only */ + + int retval = -1; + PyObject *match_list = PyList_New(0); + if (!match_list) { + return -1; + } + + PyObject *rest_list = NULL; + if (construct_rest) { + rest_list = PyList_New(0); + if (!rest_list) { + goto done; + } + } + /* recursive calls */ + for (Py_ssize_t i = 0; i < num_excs; i++) { + PyObject *e = PyTuple_GET_ITEM(eg->excs, i); + _exceptiongroup_split_result rec_result; + if (Py_EnterRecursiveCall(" in exceptiongroup_split_recursive")) { + goto done; + } + if (exceptiongroup_split_recursive( + e, matcher_type, matcher_value, + construct_rest, &rec_result) == -1) { + assert(!rec_result.match); + assert(!rec_result.rest); + Py_LeaveRecursiveCall(); + goto done; + } + Py_LeaveRecursiveCall(); + if (rec_result.match) { + assert(PyList_CheckExact(match_list)); + if (PyList_Append(match_list, rec_result.match) == -1) { + Py_DECREF(rec_result.match); + goto done; + } + Py_DECREF(rec_result.match); + } + if (rec_result.rest) { + assert(construct_rest); + assert(PyList_CheckExact(rest_list)); + if (PyList_Append(rest_list, rec_result.rest) == -1) { + Py_DECREF(rec_result.rest); + goto done; + } + Py_DECREF(rec_result.rest); + } + } + + /* construct result */ + if (exceptiongroup_subset(eg, match_list, &result->match) == -1) { + goto done; + } + + if (construct_rest) { + assert(PyList_CheckExact(rest_list)); + if (exceptiongroup_subset(eg, rest_list, &result->rest) == -1) { + Py_CLEAR(result->match); + goto done; + } + } + retval = 0; +done: + Py_DECREF(match_list); + Py_XDECREF(rest_list); + if (retval == -1) { + Py_CLEAR(result->match); + Py_CLEAR(result->rest); + } + return retval; +} + +static PyObject * +BaseExceptionGroup_split(PyObject *self, PyObject *args) +{ + PyObject *matcher_value = NULL; + if (!PyArg_UnpackTuple(args, "split", 1, 1, &matcher_value)) { + return NULL; + } + + _exceptiongroup_split_matcher_type matcher_type; + if (get_matcher_type(matcher_value, &matcher_type) == -1) { + return NULL; + } + + _exceptiongroup_split_result split_result; + bool construct_rest = true; + if (exceptiongroup_split_recursive( + self, matcher_type, matcher_value, + construct_rest, &split_result) == -1) { + return NULL; + } + + PyObject *result = PyTuple_Pack( + 2, + split_result.match ? split_result.match : Py_None, + split_result.rest ? split_result.rest : Py_None); + + Py_XDECREF(split_result.match); + Py_XDECREF(split_result.rest); + return result; +} + +static PyObject * +BaseExceptionGroup_subgroup(PyObject *self, PyObject *args) +{ + PyObject *matcher_value = NULL; + if (!PyArg_UnpackTuple(args, "subgroup", 1, 1, &matcher_value)) { + return NULL; + } + + _exceptiongroup_split_matcher_type matcher_type; + if (get_matcher_type(matcher_value, &matcher_type) == -1) { + return NULL; + } + + _exceptiongroup_split_result split_result; + bool construct_rest = false; + if (exceptiongroup_split_recursive( + self, matcher_type, matcher_value, + construct_rest, &split_result) == -1) { + return NULL; + } + + PyObject *result = Py_NewRef( + split_result.match ? split_result.match : Py_None); + + Py_XDECREF(split_result.match); + assert(!split_result.rest); + return result; +} + +static PyMemberDef BaseExceptionGroup_members[] = { + {"message", T_OBJECT, offsetof(PyBaseExceptionGroupObject, msg), READONLY, + PyDoc_STR("exception message")}, + {"exceptions", T_OBJECT, offsetof(PyBaseExceptionGroupObject, excs), READONLY, + PyDoc_STR("nested exceptions")}, + {NULL} /* Sentinel */ +}; + +static PyMethodDef BaseExceptionGroup_methods[] = { + {"__class_getitem__", (PyCFunction)Py_GenericAlias, + METH_O|METH_CLASS, PyDoc_STR("See PEP 585")}, + {"derive", (PyCFunction)BaseExceptionGroup_derive, METH_VARARGS}, + {"split", (PyCFunction)BaseExceptionGroup_split, METH_VARARGS}, + {"subgroup", (PyCFunction)BaseExceptionGroup_subgroup, METH_VARARGS}, + {NULL} +}; + +ComplexExtendsException(PyExc_BaseException, BaseExceptionGroup, + BaseExceptionGroup, BaseExceptionGroup_new /* new */, + BaseExceptionGroup_methods, BaseExceptionGroup_members, + 0 /* getset */, BaseExceptionGroup_str, + "A combination of multiple unrelated exceptions."); + +/* + * ExceptionGroup extends BaseExceptionGroup, Exception + */ +static PyObject* +create_exception_group_class(void) { + struct _Py_exc_state *state = get_exc_state(); + + PyObject *bases = PyTuple_Pack( + 2, PyExc_BaseExceptionGroup, PyExc_Exception); + if (bases == NULL) { + return NULL; + } + + assert(!state->PyExc_ExceptionGroup); + state->PyExc_ExceptionGroup = PyErr_NewException( + "builtins.ExceptionGroup", bases, NULL); + + Py_DECREF(bases); + return state->PyExc_ExceptionGroup; +} + /* * KeyboardInterrupt extends BaseException */ @@ -2671,6 +3182,7 @@ _PyExc_Init(PyInterpreterState *interp) } while (0) PRE_INIT(BaseException); + PRE_INIT(BaseExceptionGroup); PRE_INIT(Exception); PRE_INIT(TypeError); PRE_INIT(StopAsyncIteration); @@ -2805,8 +3317,15 @@ _PyBuiltins_AddExceptions(PyObject *bltinmod) return _PyStatus_ERR("exceptions bootstrapping error."); } + PyObject *PyExc_ExceptionGroup = create_exception_group_class(); + if (!PyExc_ExceptionGroup) { + return _PyStatus_ERR("exceptions bootstrapping error."); + } + POST_INIT(BaseException); POST_INIT(Exception); + POST_INIT(BaseExceptionGroup); + POST_INIT(ExceptionGroup); POST_INIT(TypeError); POST_INIT(StopAsyncIteration); POST_INIT(StopIteration); @@ -2885,6 +3404,13 @@ _PyBuiltins_AddExceptions(PyObject *bltinmod) #undef INIT_ALIAS } +void +_PyExc_ClearExceptionGroupType(PyInterpreterState *interp) +{ + struct _Py_exc_state *state = &interp->exc_state; + Py_CLEAR(state->PyExc_ExceptionGroup); +} + void _PyExc_Fini(PyInterpreterState *interp) { diff --git a/PC/python3dll.c b/PC/python3dll.c index d9e6fd3e7ca..d2a87070de5 100755 --- a/PC/python3dll.c +++ b/PC/python3dll.c @@ -754,6 +754,7 @@ EXPORT_DATA(PyExc_ArithmeticError) EXPORT_DATA(PyExc_AssertionError) EXPORT_DATA(PyExc_AttributeError) EXPORT_DATA(PyExc_BaseException) +EXPORT_DATA(PyExc_BaseExceptionGroup) EXPORT_DATA(PyExc_BlockingIOError) EXPORT_DATA(PyExc_BrokenPipeError) EXPORT_DATA(PyExc_BufferError) diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index c5a209abae6..9ce845ca61d 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -1662,6 +1662,8 @@ finalize_interp_clear(PyThreadState *tstate) { int is_main_interp = _Py_IsMainInterpreter(tstate->interp); + _PyExc_ClearExceptionGroupType(tstate->interp); + /* Clear interpreter state and all thread states */ _PyInterpreterState_Clear(tstate);