From 7bc3c15fa00e4be3ee7fed15a1bdb17f1a036fe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Mon, 24 Apr 2023 22:23:24 +0200 Subject: [PATCH 1/6] gh-103791: Make contextlib.suppress also act on exceptions within an ExceptionGroup --- Lib/contextlib.py | 11 ++++++++++- Lib/test/support/testcase.py | 27 +++++++++++++++++++++++++++ Lib/test/test_contextlib.py | 27 ++++++++++++++++++++++++++- Lib/test/test_except_star.py | 22 ++-------------------- 4 files changed, 65 insertions(+), 22 deletions(-) create mode 100644 Lib/test/support/testcase.py diff --git a/Lib/contextlib.py b/Lib/contextlib.py index 30d9ac25b2bbec..8b8958ecc0beaf 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 from excinst + return False class _BaseExitStack: diff --git a/Lib/test/support/testcase.py b/Lib/test/support/testcase.py new file mode 100644 index 00000000000000..4fd57952c33414 --- /dev/null +++ b/Lib/test/support/testcase.py @@ -0,0 +1,27 @@ +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) + print("E", exc.exceptions) + print("T", template.exceptions) + 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) From 08fa06c9a51af41fc25c1a1cf81ee2b790d0d927 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Mon, 24 Apr 2023 22:39:05 +0200 Subject: [PATCH 2/6] Remove unnecessary `print()` calls --- Lib/test/support/testcase.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/test/support/testcase.py b/Lib/test/support/testcase.py index 4fd57952c33414..10999acdb4d38b 100644 --- a/Lib/test/support/testcase.py +++ b/Lib/test/support/testcase.py @@ -20,8 +20,6 @@ def assertExceptionIsLike(self, exc, template): self.assertEqual(exc.args[0], template.args[0]) else: self.assertEqual(exc.message, template.message) - print("E", exc.exceptions) - print("T", template.exceptions) self.assertEqual(len(exc.exceptions), len(template.exceptions)) for e, t in zip(exc.exceptions, template.exceptions): self.assertExceptionIsLike(e, t) From be30df6a8026239eaa76eb2737476f11da31fb3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Mon, 24 Apr 2023 22:44:36 +0200 Subject: [PATCH 3/6] Fix docstring typo --- Lib/test/support/testcase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/support/testcase.py b/Lib/test/support/testcase.py index 10999acdb4d38b..1e4363b15783eb 100644 --- a/Lib/test/support/testcase.py +++ b/Lib/test/support/testcase.py @@ -4,7 +4,7 @@ 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`.} + `exc_obj.args`. """ if exc is None and template is None: return From acf95f6d3b25a62dd7933264d4dbeeb2fcab368f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Mon, 24 Apr 2023 23:02:11 +0200 Subject: [PATCH 4/6] Don't chain with the old exception: split already has the correct TB --- Lib/contextlib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/contextlib.py b/Lib/contextlib.py index 8b8958ecc0beaf..b5acbcb9e6d77c 100644 --- a/Lib/contextlib.py +++ b/Lib/contextlib.py @@ -449,7 +449,7 @@ def __exit__(self, exctype, excinst, exctb): match, rest = excinst.split(self._exceptions) if rest is None: return True - raise rest from excinst + raise rest return False From 503d9f843d9506986ea87a29e17486b80358ee5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Mon, 24 Apr 2023 23:13:08 +0200 Subject: [PATCH 5/6] Documentation and Blurb --- Doc/library/contextlib.rst | 8 ++++++++ .../2023-04-24-23-07-56.gh-issue-103791.bBPWdS.rst | 3 +++ 2 files changed, 11 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2023-04-24-23-07-56.gh-issue-103791.bBPWdS.rst diff --git a/Doc/library/contextlib.rst b/Doc/library/contextlib.rst index 1b55868c3aa62f..94ee71dfccddba 100644 --- a/Doc/library/contextlib.rst +++ b/Doc/library/contextlib.rst @@ -304,8 +304,16 @@ 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 others are left, the modified group is re-raised. + Otherwise, the exception group is empty and so nothing is 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/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..ffb0c8dd84886d --- /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, it +is now re-raised without the suppressed ones. From a43ffe46e7e6e527be9bd115bfe81d68087049ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Mon, 24 Apr 2023 23:50:35 +0200 Subject: [PATCH 6/6] Apply suggestions from code review Co-authored-by: Irit Katriel <1055913+iritkatriel@users.noreply.github.com> --- Doc/library/contextlib.rst | 3 +-- .../Library/2023-04-24-23-07-56.gh-issue-103791.bBPWdS.rst | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Doc/library/contextlib.rst b/Doc/library/contextlib.rst index 94ee71dfccddba..7cd081d1f54f43 100644 --- a/Doc/library/contextlib.rst +++ b/Doc/library/contextlib.rst @@ -306,8 +306,7 @@ Functions and classes provided: If the code within the :keyword:`!with` block raises an :exc:`ExceptionGroup`, suppressed exceptions are removed from the - group. If any others are left, the modified group is re-raised. - Otherwise, the exception group is empty and so nothing is raised. + group. If any exceptions in the group are not suppressed, a group containing them is re-raised. .. versionadded:: 3.4 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 index ffb0c8dd84886d..f00384cde9706e 100644 --- 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 @@ -1,3 +1,3 @@ :class:`contextlib.suppress` now supports suppressing exceptions raised as -part of an :exc:`ExceptionGroup`. If other exceptions exist on the group, it -is now re-raised without the suppressed ones. +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.