Skip to content

Commit af807d6

Browse files
committed
Merge branch 'main' into superopt_spec
* main: pythongh-87729: add LOAD_SUPER_ATTR instruction for faster super() (python#103497) pythongh-103791: Make contextlib.suppress also act on exceptions within an ExceptionGroup (python#103792)
2 parents 793a69a + 0dc8b50 commit af807d6

File tree

6 files changed

+73
-22
lines changed

6 files changed

+73
-22
lines changed

Doc/library/contextlib.rst

+7
Original file line numberDiff line numberDiff line change
@@ -304,8 +304,15 @@ Functions and classes provided:
304304

305305
This context manager is :ref:`reentrant <reentrant-cms>`.
306306

307+
If the code within the :keyword:`!with` block raises an
308+
:exc:`ExceptionGroup`, suppressed exceptions are removed from the
309+
group. If any exceptions in the group are not suppressed, a group containing them is re-raised.
310+
307311
.. versionadded:: 3.4
308312

313+
.. versionchanged:: 3.12
314+
``suppress`` now supports suppressing exceptions raised as
315+
part of an :exc:`ExceptionGroup`.
309316

310317
.. function:: redirect_stdout(new_target)
311318

Lib/contextlib.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,16 @@ def __exit__(self, exctype, excinst, exctb):
441441
# exactly reproduce the limitations of the CPython interpreter.
442442
#
443443
# See http://bugs.python.org/issue12029 for more details
444-
return exctype is not None and issubclass(exctype, self._exceptions)
444+
if exctype is None:
445+
return
446+
if issubclass(exctype, self._exceptions):
447+
return True
448+
if issubclass(exctype, ExceptionGroup):
449+
match, rest = excinst.split(self._exceptions)
450+
if rest is None:
451+
return True
452+
raise rest
453+
return False
445454

446455

447456
class _BaseExitStack:

Lib/test/support/testcase.py

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
class ExceptionIsLikeMixin:
2+
def assertExceptionIsLike(self, exc, template):
3+
"""
4+
Passes when the provided `exc` matches the structure of `template`.
5+
Individual exceptions don't have to be the same objects or even pass
6+
an equality test: they only need to be the same type and contain equal
7+
`exc_obj.args`.
8+
"""
9+
if exc is None and template is None:
10+
return
11+
12+
if template is None:
13+
self.fail(f"unexpected exception: {exc}")
14+
15+
if exc is None:
16+
self.fail(f"expected an exception like {template!r}, got None")
17+
18+
if not isinstance(exc, ExceptionGroup):
19+
self.assertEqual(exc.__class__, template.__class__)
20+
self.assertEqual(exc.args[0], template.args[0])
21+
else:
22+
self.assertEqual(exc.message, template.message)
23+
self.assertEqual(len(exc.exceptions), len(template.exceptions))
24+
for e, t in zip(exc.exceptions, template.exceptions):
25+
self.assertExceptionIsLike(e, t)

Lib/test/test_contextlib.py

+26-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from contextlib import * # Tests __all__
1111
from test import support
1212
from test.support import os_helper
13+
from test.support.testcase import ExceptionIsLikeMixin
1314
import weakref
1415

1516

@@ -1148,7 +1149,7 @@ class TestRedirectStderr(TestRedirectStream, unittest.TestCase):
11481149
orig_stream = "stderr"
11491150

11501151

1151-
class TestSuppress(unittest.TestCase):
1152+
class TestSuppress(ExceptionIsLikeMixin, unittest.TestCase):
11521153

11531154
@support.requires_docstrings
11541155
def test_instance_docs(self):
@@ -1202,6 +1203,30 @@ def test_cm_is_reentrant(self):
12021203
1/0
12031204
self.assertTrue(outer_continued)
12041205

1206+
def test_exception_groups(self):
1207+
eg_ve = lambda: ExceptionGroup(
1208+
"EG with ValueErrors only",
1209+
[ValueError("ve1"), ValueError("ve2"), ValueError("ve3")],
1210+
)
1211+
eg_all = lambda: ExceptionGroup(
1212+
"EG with many types of exceptions",
1213+
[ValueError("ve1"), KeyError("ke1"), ValueError("ve2"), KeyError("ke2")],
1214+
)
1215+
with suppress(ValueError):
1216+
raise eg_ve()
1217+
with suppress(ValueError, KeyError):
1218+
raise eg_all()
1219+
with self.assertRaises(ExceptionGroup) as eg1:
1220+
with suppress(ValueError):
1221+
raise eg_all()
1222+
self.assertExceptionIsLike(
1223+
eg1.exception,
1224+
ExceptionGroup(
1225+
"EG with many types of exceptions",
1226+
[KeyError("ke1"), KeyError("ke2")],
1227+
),
1228+
)
1229+
12051230

12061231
class TestChdir(unittest.TestCase):
12071232
def make_relative_path(self, *parts):

Lib/test/test_except_star.py

+2-20
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import sys
22
import unittest
33
import textwrap
4+
from test.support.testcase import ExceptionIsLikeMixin
45

56
class TestInvalidExceptStar(unittest.TestCase):
67
def test_mixed_except_and_except_star_is_syntax_error(self):
@@ -169,26 +170,7 @@ def f(x):
169170
self.assertIsInstance(exc, ExceptionGroup)
170171

171172

172-
class ExceptStarTest(unittest.TestCase):
173-
def assertExceptionIsLike(self, exc, template):
174-
if exc is None and template is None:
175-
return
176-
177-
if template is None:
178-
self.fail(f"unexpected exception: {exc}")
179-
180-
if exc is None:
181-
self.fail(f"expected an exception like {template!r}, got None")
182-
183-
if not isinstance(exc, ExceptionGroup):
184-
self.assertEqual(exc.__class__, template.__class__)
185-
self.assertEqual(exc.args[0], template.args[0])
186-
else:
187-
self.assertEqual(exc.message, template.message)
188-
self.assertEqual(len(exc.exceptions), len(template.exceptions))
189-
for e, t in zip(exc.exceptions, template.exceptions):
190-
self.assertExceptionIsLike(e, t)
191-
173+
class ExceptStarTest(ExceptionIsLikeMixin, unittest.TestCase):
192174
def assertMetadataEqual(self, e1, e2):
193175
if e1 is None or e2 is None:
194176
self.assertTrue(e1 is None and e2 is None)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:class:`contextlib.suppress` now supports suppressing exceptions raised as
2+
part of an :exc:`ExceptionGroup`. If other exceptions exist on the group, they
3+
are re-raised in a group that does not contain the suppressed exceptions.

0 commit comments

Comments
 (0)