bpo-45292: [PEP 654] add the ExceptionGroup and BaseExceptionGroup classes (GH-28569)

This commit is contained in:
Irit Katriel 2021-10-23 00:13:46 +01:00 committed by GitHub
parent 4bc5473a42
commit f30ad65dbf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1366 additions and 4 deletions

View File

@ -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,

View File

@ -14,6 +14,12 @@ typedef struct {
PyException_HEAD
} PyBaseExceptionObject;
typedef struct {
PyException_HEAD
PyObject *msg;
PyObject *excs;
} PyBaseExceptionGroupObject;
typedef struct {
PyException_HEAD
PyObject *msg;

View File

@ -205,6 +205,8 @@ struct _Py_exc_state {
PyObject *errnomap;
PyBaseExceptionObject *memerrors_freelist;
int memerrors_numfree;
// The ExceptionGroup type
PyObject *PyExc_ExceptionGroup;
};

View File

@ -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);

View File

@ -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

View File

@ -2,7 +2,9 @@ BaseException
├── SystemExit
├── KeyboardInterrupt
├── GeneratorExit
├── BaseExceptionGroup
└── Exception
├── ExceptionGroup [BaseExceptionGroup]
├── StopIteration
├── StopAsyncIteration
├── ArithmeticError

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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),

View File

@ -198,6 +198,7 @@ SYMBOL_NAMES = (
"PyExc_AssertionError",
"PyExc_AttributeError",
"PyExc_BaseException",
"PyExc_BaseExceptionGroup",
"PyExc_BlockingIOError",
"PyExc_BrokenPipeError",
"PyExc_BufferError",

View File

@ -0,0 +1 @@
Implement :pep:`654` Add :class:`ExceptionGroup` and :class:`BaseExceptionGroup`.

View File

@ -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

View File

@ -6,6 +6,7 @@
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <stdbool.h>
#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)
{

View File

@ -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)

View File

@ -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);