Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions Doc/library/exceptions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,35 @@ The following exceptions are used mostly as base classes for other exceptions.

.. versionadded:: 3.11

.. method:: subgroup(condition)

Return an :class:`BaseExceptionGroup` or :class:`ExceptionGroup` that
contains this exception if it matches a *condition*, or ``None`` if
the result is empty.

The condition can be an exception type or tuple of exception types, in
which case the exception is checked for a match using the same check
that is used in an ``except`` clause.

The condition can also be a callable (other than a type object) that
accepts an exception as its single argument and returns true if the
exception should be matched.

.. seealso:: :meth:`BaseExceptionGroup.subgroup`

.. versionadded:: 3.14

.. method:: split(condition)

Like :meth:`subgroup`, but returns the pair ``(match, rest)`` where
``match`` is ``subgroup(condition)`` and ``rest`` is the remaining
non-matching part (either ``None`` or an exception group wrapping
this exception).

.. seealso:: :meth:`BaseExceptionGroup.split`

.. versionadded:: 3.14


.. exception:: Exception

Expand Down Expand Up @@ -975,6 +1004,8 @@ their subgroups based on the types of the contained exceptions.
including the top-level and any nested exception groups. If the condition is
true for such an exception group, it is included in the result in full.

.. seealso:: :meth:`BaseException.subgroup`

.. versionadded:: 3.13
``condition`` can be any callable which is not a type object.

Expand All @@ -984,6 +1015,8 @@ their subgroups based on the types of the contained exceptions.
is ``subgroup(condition)`` and ``rest`` is the remaining non-matching
part.

.. seealso:: :meth:`BaseException.split`

.. method:: derive(excs)

Returns an exception group with the same :attr:`message`, but which
Expand Down
6 changes: 6 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,12 @@ Other language changes
:mod:`copyable <copy>`.
(Contributed by Serhiy Storchaka in :gh:`125767`.)

* Add the :meth:`~BaseException.subgroup` and :meth:`~BaseException.split`
methods for matching and splitting leaf exceptions respectively. This is
roughly equivalent to wrapping the exception in an exception group and
calling the :exc:`BaseExceptionGroup` homonymous methods.
(Contributed by Bénédikt Tran in :gh:`125825`.)


New modules
===========
Expand Down
2 changes: 2 additions & 0 deletions Include/internal/pycore_global_objects_fini_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Include/internal/pycore_global_strings.h
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(source)
STRUCT_FOR_ID(source_traceback)
STRUCT_FOR_ID(spam)
STRUCT_FOR_ID(split)
STRUCT_FOR_ID(src)
STRUCT_FOR_ID(src_dir_fd)
STRUCT_FOR_ID(stacklevel)
Expand All @@ -720,6 +721,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(strict_mode)
STRUCT_FOR_ID(string)
STRUCT_FOR_ID(sub_key)
STRUCT_FOR_ID(subgroup)
STRUCT_FOR_ID(symmetric_difference_update)
STRUCT_FOR_ID(tabsize)
STRUCT_FOR_ID(tag)
Expand Down
2 changes: 2 additions & 0 deletions Include/internal/pycore_runtime_init_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions Include/internal/pycore_unicodeobject_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

136 changes: 96 additions & 40 deletions Lib/test/test_exception_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,32 +308,39 @@ class ExceptionGroupSubgroupTests(ExceptionGroupTestBase):
def setUp(self):
self.eg = create_simple_eg()
self.eg_template = [ValueError(1), TypeError(int), ValueError(2)]
self.exc = ValueError('a')

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)
for obj in (self.eg, self.exc):
for arg in (
"bad arg",
type('NewClass', (), {}),
OSError('instance not type'),
[OSError, TypeError],
(OSError, 42),
):
with self.subTest(obj_type=type(obj), arg=arg):
with self.assertRaises(TypeError):
obj.subgroup(arg)

with self.assertRaises(TypeError):
obj.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))
for exc_type in (
BaseException, Exception,
BaseExceptionGroup, ExceptionGroup,
):
with self.subTest(exc_type):
self.assertIs(self.eg, self.eg.subgroup(exc_type))

wrapped = self.exc.subgroup(ValueError)
self.assertEqual(wrapped.message, str(self.exc))
self.assertMatchesTemplate(wrapped, ExceptionGroup, [self.exc])

def test_basics_subgroup_by_type__no_match(self):
self.assertIsNone(self.eg.subgroup(OSError))
self.assertIsNone(self.exc.subgroup(OSError))

def test_basics_subgroup_by_type__match(self):
eg = self.eg
Expand All @@ -349,15 +356,24 @@ def test_basics_subgroup_by_type__match(self):
self.assertEqual(subeg.message, eg.message)
self.assertMatchesTemplate(subeg, ExceptionGroup, template)

wrapped = self.exc.subgroup(ValueError)
self.assertEqual(wrapped.message, str(self.exc))
self.assertMatchesTemplate(wrapped, ExceptionGroup, [self.exc])

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

wrapped = self.exc.subgroup(callable)
self.assertEqual(wrapped.message, str(self.exc))
self.assertMatchesTemplate(wrapped, ExceptionGroup, [self.exc])

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))
self.assertIsNone(self.exc.subgroup(callable))

def test_basics_subgroup_by_predicate__match(self):
eg = self.eg
Expand All @@ -371,40 +387,61 @@ def test_basics_subgroup_by_predicate__match(self):
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)
subeg = eg.subgroup(callable)
self.assertEqual(subeg.message, eg.message)
self.assertMatchesTemplate(subeg, ExceptionGroup, template)

f = lambda e: isinstance(e, ValueError)
for callable in [f, Predicate(f), Predicate(f).method]:
group = self.exc.subgroup(callable)
self.assertEqual(group.message, str(self.exc))
self.assertMatchesTemplate(group, ExceptionGroup, [self.exc])


class ExceptionGroupSplitTests(ExceptionGroupTestBase):
def setUp(self):
self.eg = create_simple_eg()
self.eg_template = [ValueError(1), TypeError(int), ValueError(2)]

self.exc = ValueError('a')

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)
for exc_type in (
BaseException, Exception,
BaseExceptionGroup, ExceptionGroup,
):
with self.subTest(exc_type):
match, rest = self.eg.split(exc_type)
self.assertMatchesTemplate(match, ExceptionGroup,
self.eg_template)
self.assertIsNone(rest)

match, rest = self.exc.split(exc_type)
self.assertEqual(match.message, str(self.exc))
self.assertMatchesTemplate(match, ExceptionGroup, [self.exc])
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)
self.assertMatchesTemplate(rest, ExceptionGroup, self.eg_template)

match, rest = self.exc.split(OSError)
self.assertIsNone(match)
self.assertMatchesTemplate(rest, ExceptionGroup, [self.exc])

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)]),
# (exc_or_eg, matcher, match_template, rest_template)
(ValueError, [ValueError(1), ValueError(2)], [TypeError(int)]),
(TypeError, [TypeError(int)], [ValueError(1), ValueError(2)]),
((ValueError, TypeError), self.eg_template, None),
(
(OSError, ValueError),
[ValueError(1), ValueError(2)],
[TypeError(int)],
),
]

for match_type, match_template, rest_template in testcases:
Expand All @@ -419,29 +456,41 @@ def test_basics_split_by_type__match(self):
else:
self.assertIsNone(rest)

match, rest = self.exc.split(ValueError)
self.assertEqual(match.message, str(self.exc))
self.assertMatchesTemplate(match, ExceptionGroup, [self.exc])
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)

match, rest = self.exc.split(callable)
self.assertEqual(match.message, str(self.exc))
self.assertMatchesTemplate(match, ExceptionGroup, [self.exc])
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)

match, rest = self.exc.split(callable)
self.assertIsNone(match)
self.assertMatchesTemplate(rest, ExceptionGroup, [self.exc])

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),
(ValueError, [ValueError(1), ValueError(2)], [TypeError(int)]),
(TypeError, [TypeError(int)], [ValueError(1), ValueError(2)]),
((ValueError, TypeError), self.eg_template, None),
]

for match_type, match_template, rest_template in testcases:
Expand All @@ -456,6 +505,13 @@ def test_basics_split_by_predicate__match(self):
self.assertMatchesTemplate(
rest, ExceptionGroup, rest_template)

f = lambda e: isinstance(e, ValueError)
for callable in [f, Predicate(f), Predicate(f).method]:
match, rest = self.exc.split(callable)
self.assertEqual(match.message, str(self.exc))
self.assertMatchesTemplate(match, ExceptionGroup, [self.exc])
self.assertIsNone(rest)


class DeepRecursionInSplitAndSubgroup(unittest.TestCase):
def make_deep_eg(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Add :meth:`~BaseException.split` and :meth:`~BaseException.subgroup` to
allow splitting and matching simple exceptions as if they were
:ref:`exception groups <lib-exception-groups>` of one exception. Patch by
Bénédikt Tran.
Loading
Loading