diff --git a/Doc/library/contextlib.rst b/Doc/library/contextlib.rst index 1b55868c3aa62f..7cd081d1f54f43 100644 --- a/Doc/library/contextlib.rst +++ b/Doc/library/contextlib.rst @@ -304,8 +304,15 @@ Functions and classes provided: This context manager is :ref:`reentrant `. + If the code within the :keyword:`!with` block raises an + :exc:`ExceptionGroup`, suppressed exceptions are removed from the + group. If any exceptions in the group are not suppressed, a group containing them is re-raised. + .. versionadded:: 3.4 + .. versionchanged:: 3.12 + ``suppress`` now supports suppressing exceptions raised as + part of an :exc:`ExceptionGroup`. .. function:: redirect_stdout(new_target) diff --git a/Lib/contextlib.py b/Lib/contextlib.py index 30d9ac25b2bbec..b5acbcb9e6d77c 100644 --- a/Lib/contextlib.py +++ b/Lib/contextlib.py @@ -441,7 +441,16 @@ def __exit__(self, exctype, excinst, exctb): # exactly reproduce the limitations of the CPython interpreter. # # See http://bugs.python.org/issue12029 for more details - return exctype is not None and issubclass(exctype, self._exceptions) + if exctype is None: + return + if issubclass(exctype, self._exceptions): + return True + if issubclass(exctype, ExceptionGroup): + match, rest = excinst.split(self._exceptions) + if rest is None: + return True + raise rest + return False class _BaseExitStack: diff --git a/Lib/test/support/testcase.py b/Lib/test/support/testcase.py new file mode 100644 index 00000000000000..1e4363b15783eb --- /dev/null +++ b/Lib/test/support/testcase.py @@ -0,0 +1,25 @@ +class ExceptionIsLikeMixin: + def assertExceptionIsLike(self, exc, template): + """ + Passes when the provided `exc` matches the structure of `template`. + Individual exceptions don't have to be the same objects or even pass + an equality test: they only need to be the same type and contain equal + `exc_obj.args`. + """ + if exc is None and template is None: + return + + if template is None: + self.fail(f"unexpected exception: {exc}") + + if exc is None: + self.fail(f"expected an exception like {template!r}, got None") + + if not isinstance(exc, ExceptionGroup): + self.assertEqual(exc.__class__, template.__class__) + self.assertEqual(exc.args[0], template.args[0]) + else: + self.assertEqual(exc.message, template.message) + self.assertEqual(len(exc.exceptions), len(template.exceptions)) + for e, t in zip(exc.exceptions, template.exceptions): + self.assertExceptionIsLike(e, t) diff --git a/Lib/test/test_contextlib.py b/Lib/test/test_contextlib.py index ec06785b5667a6..0f8351ab8108a6 100644 --- a/Lib/test/test_contextlib.py +++ b/Lib/test/test_contextlib.py @@ -10,6 +10,7 @@ from contextlib import * # Tests __all__ from test import support from test.support import os_helper +from test.support.testcase import ExceptionIsLikeMixin import weakref @@ -1148,7 +1149,7 @@ class TestRedirectStderr(TestRedirectStream, unittest.TestCase): orig_stream = "stderr" -class TestSuppress(unittest.TestCase): +class TestSuppress(ExceptionIsLikeMixin, unittest.TestCase): @support.requires_docstrings def test_instance_docs(self): @@ -1202,6 +1203,30 @@ def test_cm_is_reentrant(self): 1/0 self.assertTrue(outer_continued) + def test_exception_groups(self): + eg_ve = lambda: ExceptionGroup( + "EG with ValueErrors only", + [ValueError("ve1"), ValueError("ve2"), ValueError("ve3")], + ) + eg_all = lambda: ExceptionGroup( + "EG with many types of exceptions", + [ValueError("ve1"), KeyError("ke1"), ValueError("ve2"), KeyError("ke2")], + ) + with suppress(ValueError): + raise eg_ve() + with suppress(ValueError, KeyError): + raise eg_all() + with self.assertRaises(ExceptionGroup) as eg1: + with suppress(ValueError): + raise eg_all() + self.assertExceptionIsLike( + eg1.exception, + ExceptionGroup( + "EG with many types of exceptions", + [KeyError("ke1"), KeyError("ke2")], + ), + ) + class TestChdir(unittest.TestCase): def make_relative_path(self, *parts): diff --git a/Lib/test/test_except_star.py b/Lib/test/test_except_star.py index c5167c5bba38af..bc66f90b9cad45 100644 --- a/Lib/test/test_except_star.py +++ b/Lib/test/test_except_star.py @@ -1,6 +1,7 @@ import sys import unittest import textwrap +from test.support.testcase import ExceptionIsLikeMixin class TestInvalidExceptStar(unittest.TestCase): def test_mixed_except_and_except_star_is_syntax_error(self): @@ -169,26 +170,7 @@ def f(x): self.assertIsInstance(exc, ExceptionGroup) -class ExceptStarTest(unittest.TestCase): - def assertExceptionIsLike(self, exc, template): - if exc is None and template is None: - return - - if template is None: - self.fail(f"unexpected exception: {exc}") - - if exc is None: - self.fail(f"expected an exception like {template!r}, got None") - - if not isinstance(exc, ExceptionGroup): - self.assertEqual(exc.__class__, template.__class__) - self.assertEqual(exc.args[0], template.args[0]) - else: - self.assertEqual(exc.message, template.message) - self.assertEqual(len(exc.exceptions), len(template.exceptions)) - for e, t in zip(exc.exceptions, template.exceptions): - self.assertExceptionIsLike(e, t) - +class ExceptStarTest(ExceptionIsLikeMixin, unittest.TestCase): def assertMetadataEqual(self, e1, e2): if e1 is None or e2 is None: self.assertTrue(e1 is None and e2 is None) diff --git a/Misc/NEWS.d/next/Library/2023-04-24-23-07-56.gh-issue-103791.bBPWdS.rst b/Misc/NEWS.d/next/Library/2023-04-24-23-07-56.gh-issue-103791.bBPWdS.rst new file mode 100644 index 00000000000000..f00384cde9706e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-04-24-23-07-56.gh-issue-103791.bBPWdS.rst @@ -0,0 +1,3 @@ +:class:`contextlib.suppress` now supports suppressing exceptions raised as +part of an :exc:`ExceptionGroup`. If other exceptions exist on the group, they +are re-raised in a group that does not contain the suppressed exceptions. pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy