2021-10-22 20:13:46 -03:00
|
|
|
import collections.abc
|
|
|
|
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):
|
2022-03-05 09:59:24 -04:00
|
|
|
with self.assertRaisesRegex(TypeError, 'Exception'):
|
2021-10-22 20:13:46 -03:00
|
|
|
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):
|
2022-01-25 19:52:43 -04:00
|
|
|
MSG = r'BaseExceptionGroup.__new__\(\) takes exactly 2 arguments'
|
2021-10-22 20:13:46 -03:00
|
|
|
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):
|
2021-11-16 07:58:21 -04:00
|
|
|
MSG = r'second argument \(exceptions\) must be a sequence'
|
2021-10-22 20:13:46 -03:00
|
|
|
with self.assertRaisesRegex(TypeError, MSG):
|
|
|
|
ExceptionGroup('errors not sequence', {ValueError(42)})
|
|
|
|
with self.assertRaisesRegex(TypeError, MSG):
|
|
|
|
ExceptionGroup("eg", None)
|
|
|
|
|
2021-11-16 07:58:21 -04:00
|
|
|
MSG = r'second argument \(exceptions\) must be a non-empty sequence'
|
2021-10-22 20:13:46 -03:00
|
|
|
with self.assertRaisesRegex(ValueError, MSG):
|
|
|
|
ExceptionGroup("eg", [])
|
|
|
|
|
|
|
|
def test_bad_EG_construction__nested_non_exceptions(self):
|
2021-11-16 07:58:21 -04:00
|
|
|
MSG = (r'Item [0-9]+ of second argument \(exceptions\)'
|
2021-10-22 20:13:46 -03:00
|
|
|
' 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)
|
|
|
|
|
|
|
|
|
2022-02-22 14:28:58 -04:00
|
|
|
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)])"))
|
|
|
|
|
|
|
|
|
2021-10-22 20:13:46 -03:00
|
|
|
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'),
|
2021-12-14 12:48:15 -04:00
|
|
|
[OSError, TypeError],
|
|
|
|
(OSError, 42)]
|
2021-10-22 20:13:46 -03:00
|
|
|
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)
|
|
|
|
|
2021-12-21 06:12:26 -04:00
|
|
|
# message, cause and context, traceback and note equal to eg
|
2021-10-22 20:13:46 -03:00
|
|
|
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__)
|
2022-04-16 15:59:52 -03:00
|
|
|
self.assertEqual(
|
|
|
|
getattr(eg, '__notes__', None),
|
|
|
|
getattr(part, '__notes__', None))
|
2021-10-22 20:13:46 -03:00
|
|
|
|
|
|
|
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:
|
2022-04-16 15:59:52 -03:00
|
|
|
e.add_note(f"the note: {id(e)}")
|
2021-10-22 20:13:46 -03:00
|
|
|
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)])
|
|
|
|
|
2022-04-16 15:59:52 -03:00
|
|
|
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.assertFalse(hasattr(match, '__notes__'))
|
|
|
|
self.assertFalse(hasattr(rest, '__notes__'))
|
|
|
|
|
2022-07-08 15:01:47 -03:00
|
|
|
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)
|
|
|
|
|
2021-10-22 20:13:46 -03:00
|
|
|
|
|
|
|
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()
|