Skip to content

Commit d8ca5a1

Browse files
authored
gh-105730: support more callables in ExceptionGroup.split() and subgroup() (#106035)
1 parent 1d33d53 commit d8ca5a1

File tree

4 files changed

+59
-26
lines changed

4 files changed

+59
-26
lines changed

Doc/library/exceptions.rst

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -912,10 +912,11 @@ their subgroups based on the types of the contained exceptions.
912912
Returns an exception group that contains only the exceptions from the
913913
current group that match *condition*, or ``None`` if the result is empty.
914914

915-
The condition can be either a function that accepts an exception and returns
916-
true for those that should be in the subgroup, or it can be an exception type
917-
or a tuple of exception types, which is used to check for a match using the
918-
same check that is used in an ``except`` clause.
915+
The condition can be an exception type or tuple of exception types, in which
916+
case each exception is checked for a match using the same check that is used
917+
in an ``except`` clause. The condition can also be a callable (other than
918+
a type object) that accepts an exception as its single argument and returns
919+
true for the exceptions that should be in the subgroup.
919920

920921
The nesting structure of the current exception is preserved in the result,
921922
as are the values of its :attr:`message`, :attr:`__traceback__`,
@@ -926,6 +927,9 @@ their subgroups based on the types of the contained exceptions.
926927
including the top-level and any nested exception groups. If the condition is
927928
true for such an exception group, it is included in the result in full.
928929

930+
.. versionadded:: 3.13
931+
``condition`` can be any callable which is not a type object.
932+
929933
.. method:: split(condition)
930934

931935
Like :meth:`subgroup`, but returns the pair ``(match, rest)`` where ``match``

Lib/test/test_exception_group.py

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -294,17 +294,31 @@ def assertMatchesTemplate(self, exc, exc_type, template):
294294
self.assertEqual(type(exc), type(template))
295295
self.assertEqual(exc.args, template.args)
296296

297+
class Predicate:
298+
def __init__(self, func):
299+
self.func = func
300+
301+
def __call__(self, e):
302+
return self.func(e)
303+
304+
def method(self, e):
305+
return self.func(e)
297306

298307
class ExceptionGroupSubgroupTests(ExceptionGroupTestBase):
299308
def setUp(self):
300309
self.eg = create_simple_eg()
301310
self.eg_template = [ValueError(1), TypeError(int), ValueError(2)]
302311

303312
def test_basics_subgroup_split__bad_arg_type(self):
313+
class C:
314+
pass
315+
304316
bad_args = ["bad arg",
317+
C,
305318
OSError('instance not type'),
306319
[OSError, TypeError],
307-
(OSError, 42)]
320+
(OSError, 42),
321+
]
308322
for arg in bad_args:
309323
with self.assertRaises(TypeError):
310324
self.eg.subgroup(arg)
@@ -336,10 +350,14 @@ def test_basics_subgroup_by_type__match(self):
336350
self.assertMatchesTemplate(subeg, ExceptionGroup, template)
337351

338352
def test_basics_subgroup_by_predicate__passthrough(self):
339-
self.assertIs(self.eg, self.eg.subgroup(lambda e: True))
353+
f = lambda e: True
354+
for callable in [f, Predicate(f), Predicate(f).method]:
355+
self.assertIs(self.eg, self.eg.subgroup(callable))
340356

341357
def test_basics_subgroup_by_predicate__no_match(self):
342-
self.assertIsNone(self.eg.subgroup(lambda e: False))
358+
f = lambda e: False
359+
for callable in [f, Predicate(f), Predicate(f).method]:
360+
self.assertIsNone(self.eg.subgroup(callable))
343361

344362
def test_basics_subgroup_by_predicate__match(self):
345363
eg = self.eg
@@ -350,9 +368,12 @@ def test_basics_subgroup_by_predicate__match(self):
350368
((ValueError, TypeError), self.eg_template)]
351369

352370
for match_type, template in testcases:
353-
subeg = eg.subgroup(lambda e: isinstance(e, match_type))
354-
self.assertEqual(subeg.message, eg.message)
355-
self.assertMatchesTemplate(subeg, ExceptionGroup, template)
371+
f = lambda e: isinstance(e, match_type)
372+
for callable in [f, Predicate(f), Predicate(f).method]:
373+
with self.subTest(callable=callable):
374+
subeg = eg.subgroup(f)
375+
self.assertEqual(subeg.message, eg.message)
376+
self.assertMatchesTemplate(subeg, ExceptionGroup, template)
356377

357378

358379
class ExceptionGroupSplitTests(ExceptionGroupTestBase):
@@ -399,14 +420,18 @@ def test_basics_split_by_type__match(self):
399420
self.assertIsNone(rest)
400421

401422
def test_basics_split_by_predicate__passthrough(self):
402-
match, rest = self.eg.split(lambda e: True)
403-
self.assertMatchesTemplate(match, ExceptionGroup, self.eg_template)
404-
self.assertIsNone(rest)
423+
f = lambda e: True
424+
for callable in [f, Predicate(f), Predicate(f).method]:
425+
match, rest = self.eg.split(callable)
426+
self.assertMatchesTemplate(match, ExceptionGroup, self.eg_template)
427+
self.assertIsNone(rest)
405428

406429
def test_basics_split_by_predicate__no_match(self):
407-
match, rest = self.eg.split(lambda e: False)
408-
self.assertIsNone(match)
409-
self.assertMatchesTemplate(rest, ExceptionGroup, self.eg_template)
430+
f = lambda e: False
431+
for callable in [f, Predicate(f), Predicate(f).method]:
432+
match, rest = self.eg.split(callable)
433+
self.assertIsNone(match)
434+
self.assertMatchesTemplate(rest, ExceptionGroup, self.eg_template)
410435

411436
def test_basics_split_by_predicate__match(self):
412437
eg = self.eg
@@ -420,14 +445,16 @@ def test_basics_split_by_predicate__match(self):
420445
]
421446

422447
for match_type, match_template, rest_template in testcases:
423-
match, rest = eg.split(lambda e: isinstance(e, match_type))
424-
self.assertEqual(match.message, eg.message)
425-
self.assertMatchesTemplate(
426-
match, ExceptionGroup, match_template)
427-
if rest_template is not None:
428-
self.assertEqual(rest.message, eg.message)
448+
f = lambda e: isinstance(e, match_type)
449+
for callable in [f, Predicate(f), Predicate(f).method]:
450+
match, rest = eg.split(callable)
451+
self.assertEqual(match.message, eg.message)
429452
self.assertMatchesTemplate(
430-
rest, ExceptionGroup, rest_template)
453+
match, ExceptionGroup, match_template)
454+
if rest_template is not None:
455+
self.assertEqual(rest.message, eg.message)
456+
self.assertMatchesTemplate(
457+
rest, ExceptionGroup, rest_template)
431458

432459

433460
class DeepRecursionInSplitAndSubgroup(unittest.TestCase):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Allow any callable other than type objects as the condition predicate in
2+
:meth:`BaseExceptionGroup.split` and :meth:`BaseExceptionGroup.subgroup`.

Objects/exceptions.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -992,7 +992,7 @@ get_matcher_type(PyObject *value,
992992
{
993993
assert(value);
994994

995-
if (PyFunction_Check(value)) {
995+
if (PyCallable_Check(value) && !PyType_Check(value)) {
996996
*type = EXCEPTION_GROUP_MATCH_BY_PREDICATE;
997997
return 0;
998998
}
@@ -1016,7 +1016,7 @@ get_matcher_type(PyObject *value,
10161016
error:
10171017
PyErr_SetString(
10181018
PyExc_TypeError,
1019-
"expected a function, exception type or tuple of exception types");
1019+
"expected an exception type, a tuple of exception types, or a callable (other than a class)");
10201020
return -1;
10211021
}
10221022

@@ -1032,7 +1032,7 @@ exceptiongroup_split_check_match(PyObject *exc,
10321032
return PyErr_GivenExceptionMatches(exc, matcher_value);
10331033
}
10341034
case EXCEPTION_GROUP_MATCH_BY_PREDICATE: {
1035-
assert(PyFunction_Check(matcher_value));
1035+
assert(PyCallable_Check(matcher_value) && !PyType_Check(matcher_value));
10361036
PyObject *exc_matches = PyObject_CallOneArg(matcher_value, exc);
10371037
if (exc_matches == NULL) {
10381038
return -1;

0 commit comments

Comments
 (0)