Files
RustPython/Lib/test/test_exception_group.py
T
Jeong, YunWon 7bc3ef6c0a ExceptionGroup fields, OSError use-init, and live f_lineno (#8738)
* Add ExceptionGroup fields and OSError use-init

BaseExceptionGroup stores msg, excs, and excs_str; args keep
the original sequence. OSError defers parse when a subclass
overrides only __init__. characters_written can be deleted.
Exception args/traceback/cause/context reject invalid
setattr/delattr. UnicodeError.object is a deletable field.
except* remainder projection copies traceback/context/cause.
SyntaxError.__init__ clears omitted location attributes.

Assisted-by: Grok:grok-4.6

* Fix OSError payload init and UnicodeError construction

new_payload_exception now runs slot_new so BlockingIOError
parses characters_written. new_exception falls back to
invoke_exception for extra-payload types. Mixed tab/space
UnexpectedIndentation is TabError. AttributeError pickle
omits obj.

Assisted-by: Grok:grok-4.6

* address review: classify TabError from error line

Assisted-by: Grok:grok-4.6

* Add new_simple_exception; fill SyntaxError and f_lineno

new_exception constructs extra-payload types; BaseException-sized
types use new_simple_exception. Non-UTF-8 SyntaxError sets location
and text. Codegen errors load text via ProgramText. Keep Missing
parentheses capitalization. f-string adjacent atoms report a missing
comma. f_lineno may be None; lasti is not restored on unwind.

Assisted-by: Grok:grok-4.6

* address review: skip f-string continuation keywords

Assisted-by: Grok:grok-4.6

* Fix clippy map_unwrap_or in Non-UTF-8 offset

Assisted-by: Grok:grok-4.6

* Rename encode error ctor and exception setters

new_unicode_encode_error_real is new_unicode_encode_error.
set___traceback__/__cause__/__context__ are pygetset setters;
Rust uses set_traceback/set_cause/set_context.

Assisted-by: Grok:grok-4.6

* Use set_context in native pickle

Assisted-by: Grok:grok-4.6

* Collapse nested if in asyncio traceback restore

Assisted-by: Grok:grok-4.6

* Use current instruction for f_lineno addr2line

lasti is stored as the next instruction index. addr2line of
that byte offset is past RETURN_VALUE, so f_lineno was None
and stdlib_threading_current_frames hung on leftover workers.

Assisted-by: Grok:grok-4.6

* Look up format args, dir class, and frame builtins

Consume printf args before rejecting unknown conversions.
object.__dir__ uses getattr of __class__/__dict__.
_PyEval_GetBuiltin looks up iter/__import__ on the frame.

Assisted-by: Grok:grok-4.6

* Fix builtin lookups after rebase and unxfail passing tests

Assisted-by: Grok:grok-4.6

* Restore RERAISE lasti and SyntaxError locations

RERAISE with oparg restores lasti from the exception table
before unwind, so leftover f_lineno matches the original raise.
Handler lookup still uses the raising instruction.

Indentation errors outrank print/exec missing-parentheses.
BOM-with-non-utf8, bytes literals, and global+nonlocal now
report the same offsets as the parser/symtable.

object.__dir__ keeps string keys; type.__type_params__ skips
the getset wrapper stored on builtin types.

Assisted-by: Grok:grok-4.6

* Unxfail syntax/import tests and bind jump locals

IMPORT_NAME passes fromlist None instead of an empty tuple.
Jumping f_lineno binds None into unbound localsplus slots.
str lstrip/rstrip copy subclasses to exact str when unchanged.

Assisted-by: Grok:grok-4.6

* Fix import_from callers and rename frame lineno

sqlite3.iterdump now passes fromlist as a PyObject.
FrameObject::lineno is the i32 helper; f_lineno is the getset.

Assisted-by: Grok:grok-4.6

* Pass sqlite fromlist without Into conversion

from_list.into() could not infer Into<PyObjectRef> at the
import_from call. The typed tuple already converts.

Assisted-by: Grok:grok-4.6
2026-09-20 14:22:58 +09:00

1049 lines
37 KiB
Python
Vendored

import collections
import types
import unittest
from test.support import skip_emscripten_stack_overflow, skip_wasi_stack_overflow, exceeds_recursion_limit
class TestExceptionGroupTypeHierarchy(unittest.TestCase):
def test_exception_group_types(self):
self.assertIsSubclass(ExceptionGroup, Exception)
self.assertIsSubclass(ExceptionGroup, BaseExceptionGroup)
self.assertIsSubclass(BaseExceptionGroup, BaseException)
def test_exception_is_not_generic_type(self):
with self.assertRaisesRegex(TypeError, 'Exception'):
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 = r'BaseExceptionGroup.__new__\(\) 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 = r'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 = r'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 = (r'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_non_base_exceptions(self):
class MyEG(ExceptionGroup):
pass
self.assertIs(
type(MyEG("eg", [ValueError(12), TypeError(42)])),
MyEG)
def test_EG_subclass_does_not_wrap_base_exceptions(self):
class MyEG(ExceptionGroup):
pass
msg = "Cannot nest BaseExceptions in 'MyEG'"
with self.assertRaisesRegex(TypeError, msg):
MyEG("eg", [ValueError(12), KeyboardInterrupt(42)])
def test_BEG_and_E_subclass_does_not_wrap_base_exceptions(self):
class MyEG(BaseExceptionGroup, ValueError):
pass
msg = "Cannot nest BaseExceptions in 'MyEG'"
with self.assertRaisesRegex(TypeError, msg):
MyEG("eg", [ValueError(12), KeyboardInterrupt(42)])
def test_EG_and_specific_subclass_can_wrap_any_nonbase_exception(self):
class MyEG(ExceptionGroup, ValueError):
pass
# The restriction is specific to Exception, not "the other base class"
MyEG("eg", [ValueError(12), Exception()])
def test_BEG_and_specific_subclass_can_wrap_any_nonbase_exception(self):
class MyEG(BaseExceptionGroup, ValueError):
pass
# The restriction is specific to Exception, not "the other base class"
MyEG("eg", [ValueError(12), Exception()])
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)
class StrAndReprTests(unittest.TestCase):
def test_ExceptionGroup(self):
eg = BaseExceptionGroup(
'flat', [ValueError(1), TypeError(2)])
self.assertEqual(str(eg), "flat (2 sub-exceptions)")
self.assertEqual(repr(eg),
"ExceptionGroup('flat', [ValueError(1), TypeError(2)])")
eg = BaseExceptionGroup(
'nested', [eg, ValueError(1), eg, TypeError(2)])
self.assertEqual(str(eg), "nested (4 sub-exceptions)")
self.assertEqual(repr(eg),
"ExceptionGroup('nested', "
"[ExceptionGroup('flat', "
"[ValueError(1), TypeError(2)]), "
"ValueError(1), "
"ExceptionGroup('flat', "
"[ValueError(1), TypeError(2)]), TypeError(2)])")
def test_BaseExceptionGroup(self):
eg = BaseExceptionGroup(
'flat', [ValueError(1), KeyboardInterrupt(2)])
self.assertEqual(str(eg), "flat (2 sub-exceptions)")
self.assertEqual(repr(eg),
"BaseExceptionGroup("
"'flat', "
"[ValueError(1), KeyboardInterrupt(2)])")
eg = BaseExceptionGroup(
'nested', [eg, ValueError(1), eg])
self.assertEqual(str(eg), "nested (3 sub-exceptions)")
self.assertEqual(repr(eg),
"BaseExceptionGroup('nested', "
"[BaseExceptionGroup('flat', "
"[ValueError(1), KeyboardInterrupt(2)]), "
"ValueError(1), "
"BaseExceptionGroup('flat', "
"[ValueError(1), KeyboardInterrupt(2)])])")
def test_custom_exception(self):
class MyEG(ExceptionGroup):
pass
eg = MyEG(
'flat', [ValueError(1), TypeError(2)])
self.assertEqual(str(eg), "flat (2 sub-exceptions)")
self.assertEqual(repr(eg), "MyEG('flat', [ValueError(1), TypeError(2)])")
eg = MyEG(
'nested', [eg, ValueError(1), eg, TypeError(2)])
self.assertEqual(str(eg), "nested (4 sub-exceptions)")
self.assertEqual(repr(eg), (
"MyEG('nested', "
"[MyEG('flat', [ValueError(1), TypeError(2)]), "
"ValueError(1), "
"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', deque([ValueError(1), TypeError(2)]))"
)
excs.clear()
# Ensure that clearing the exceptions sequence doesn't change the repr.
self.assertEqual(
repr(eg),
"ExceptionGroup('test', deque([ValueError(1), TypeError(2)]))"
)
def test_repr_raises(self):
class MySeq(collections.abc.Sequence):
def __init__(self, raises):
self.raises = raises
def __len__(self):
return 1
def __getitem__(self, index):
if index == 0:
return ValueError(1)
raise IndexError
def __repr__(self):
if self.raises:
raise self.raises
return None
seq = MySeq(None)
with self.assertRaisesRegex(
TypeError,
r"__repr__ returned non-string \(type NoneType\)"
):
ExceptionGroup("test", seq)
seq = MySeq(ValueError)
with self.assertRaises(ValueError):
BaseExceptionGroup("test", seq)
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 Predicate:
def __init__(self, func):
self.func = func
def __call__(self, e):
return self.func(e)
def method(self, e):
return self.func(e)
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):
class C:
pass
bad_args = ["bad arg",
C,
OSError('instance not type'),
[OSError, TypeError],
(OSError, 42),
]
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):
f = lambda e: True
for callable in [f, Predicate(f), Predicate(f).method]:
self.assertIs(self.eg, self.eg.subgroup(callable))
def test_basics_subgroup_by_predicate__no_match(self):
f = lambda e: False
for callable in [f, Predicate(f), Predicate(f).method]:
self.assertIsNone(self.eg.subgroup(callable))
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:
f = lambda e: isinstance(e, match_type)
for callable in [f, Predicate(f), Predicate(f).method]:
with self.subTest(callable=callable):
subeg = eg.subgroup(f)
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):
f = lambda e: True
for callable in [f, Predicate(f), Predicate(f).method]:
match, rest = self.eg.split(callable)
self.assertMatchesTemplate(match, ExceptionGroup, self.eg_template)
self.assertIsNone(rest)
def test_basics_split_by_predicate__no_match(self):
f = lambda e: False
for callable in [f, Predicate(f), Predicate(f).method]:
match, rest = self.eg.split(callable)
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:
f = lambda e: isinstance(e, match_type)
for callable in [f, Predicate(f), Predicate(f).method]:
match, rest = eg.split(callable)
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(exceeds_recursion_limit()):
e = ExceptionGroup('eg', [e])
return e
@skip_emscripten_stack_overflow()
@skip_wasi_stack_overflow()
def test_deep_split(self):
e = self.make_deep_eg()
with self.assertRaises(RecursionError):
e.split(TypeError)
@skip_emscripten_stack_overflow()
@skip_wasi_stack_overflow()
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, traceback and note 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__)
self.assertEqual(
getattr(eg, '__notes__', None),
getattr(part, '__notes__', None))
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:
e.add_note(f"the note: {id(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)])
def test_split_copies_notes(self):
# make sure each exception group after a split has its own __notes__ list
eg = ExceptionGroup("eg", [ValueError(1), TypeError(2)])
eg.add_note("note1")
eg.add_note("note2")
orig_notes = list(eg.__notes__)
match, rest = eg.split(TypeError)
self.assertEqual(eg.__notes__, orig_notes)
self.assertEqual(match.__notes__, orig_notes)
self.assertEqual(rest.__notes__, orig_notes)
self.assertIsNot(eg.__notes__, match.__notes__)
self.assertIsNot(eg.__notes__, rest.__notes__)
self.assertIsNot(match.__notes__, rest.__notes__)
eg.add_note("eg")
match.add_note("match")
rest.add_note("rest")
self.assertEqual(eg.__notes__, orig_notes + ["eg"])
self.assertEqual(match.__notes__, orig_notes + ["match"])
self.assertEqual(rest.__notes__, orig_notes + ["rest"])
def test_split_does_not_copy_non_sequence_notes(self):
# __notes__ should be a sequence, which is shallow copied.
# If it is not a sequence, the split parts don't get any notes.
eg = ExceptionGroup("eg", [ValueError(1), TypeError(2)])
eg.__notes__ = 123
match, rest = eg.split(TypeError)
self.assertNotHasAttr(match, '__notes__')
self.assertNotHasAttr(rest, '__notes__')
def test_drive_invalid_return_value(self):
class MyEg(ExceptionGroup):
def derive(self, excs):
return 42
eg = MyEg('eg', [TypeError(1), ValueError(2)])
msg = "derive must return an instance of BaseExceptionGroup"
with self.assertRaisesRegex(TypeError, msg):
eg.split(TypeError)
with self.assertRaisesRegex(TypeError, msg):
eg.subgroup(TypeError)
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()