diff --git a/CHANGES.rst b/CHANGES.rst index 8b415ef..b75db4d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,8 @@ This library adheres to `Semantic Versioning 2.0 `_. - Added special monkeypatching if `Apport `_ has overridden ``sys.excepthook`` so it will format exception groups correctly (PR by John Litborn) +- Added a backport of ``contextlib.suppress()`` from Python 3.12.1 which also handles + suppressing exceptions inside exception groups - Fixed bare ``raise`` in a handler reraising the original naked exception rather than an exception group which is what is raised when you do a ``raise`` in an ``except*`` handler diff --git a/README.rst b/README.rst index 48178d9..d5203fd 100644 --- a/README.rst +++ b/README.rst @@ -26,6 +26,8 @@ It contains the following: * ``traceback.format_exception_only()`` * ``traceback.print_exception()`` * ``traceback.print_exc()`` +* A backported version of ``contextlib.suppress()`` from Python 3.12.1 which also + handles suppressing exceptions inside exception groups If this package is imported on Python 3.11 or later, the built-in implementations of the exception group classes are used instead, ``TracebackException`` is not monkey patched @@ -84,6 +86,18 @@ would be written with this backport like this: **NOTE**: Just like with ``except*``, you cannot handle ``BaseExceptionGroup`` or ``ExceptionGroup`` with ``catch()``. +Suppressing exceptions +====================== + +This library contains a backport of the ``contextlib.suppress()`` context manager from +Python 3.12.1. It allows you to selectively ignore certain exceptions, even when they're +inside exception groups:: + + from exceptiongroup import suppress + + with suppress(RuntimeError): + raise ExceptionGroup("", [RuntimeError("boo")]) + Notes on monkey patching ======================== diff --git a/src/exceptiongroup/__init__.py b/src/exceptiongroup/__init__.py index 0e7e02b..d8e36b2 100644 --- a/src/exceptiongroup/__init__.py +++ b/src/exceptiongroup/__init__.py @@ -6,6 +6,7 @@ "format_exception_only", "print_exception", "print_exc", + "suppress", ] import os @@ -38,3 +39,8 @@ BaseExceptionGroup = BaseExceptionGroup ExceptionGroup = ExceptionGroup + +if sys.version_info < (3, 12, 1): + from ._suppress import suppress +else: + from contextlib import suppress diff --git a/src/exceptiongroup/_suppress.py b/src/exceptiongroup/_suppress.py new file mode 100644 index 0000000..6741563 --- /dev/null +++ b/src/exceptiongroup/_suppress.py @@ -0,0 +1,40 @@ +import sys +from contextlib import AbstractContextManager + +if sys.version_info < (3, 11): + from ._exceptions import BaseExceptionGroup + + +class suppress(AbstractContextManager): + """Backport of :class:`contextlib.suppress` from Python 3.12.1.""" + + def __init__(self, *exceptions): + self._exceptions = exceptions + + def __enter__(self): + pass + + def __exit__(self, exctype, excinst, exctb): + # Unlike isinstance and issubclass, CPython exception handling + # currently only looks at the concrete type hierarchy (ignoring + # the instance and subclass checking hooks). While Guido considers + # that a bug rather than a feature, it's a fairly hard one to fix + # due to various internal implementation details. suppress provides + # the simpler issubclass based semantics, rather than trying to + # exactly reproduce the limitations of the CPython interpreter. + # + # See http://bugs.python.org/issue12029 for more details + if exctype is None: + return + + if issubclass(exctype, self._exceptions): + return True + + if issubclass(exctype, BaseExceptionGroup): + match, rest = excinst.split(self._exceptions) + if rest is None: + return True + + raise rest + + return False diff --git a/tests/test_suppress.py b/tests/test_suppress.py new file mode 100644 index 0000000..289bb33 --- /dev/null +++ b/tests/test_suppress.py @@ -0,0 +1,16 @@ +import sys + +import pytest + +from exceptiongroup import suppress + +if sys.version_info < (3, 11): + from exceptiongroup import BaseExceptionGroup, ExceptionGroup + + +def test_suppress_exception(): + with pytest.raises(ExceptionGroup) as exc, suppress(SystemExit): + raise BaseExceptionGroup("", [SystemExit(1), RuntimeError("boo")]) + + assert len(exc.value.exceptions) == 1 + assert isinstance(exc.value.exceptions[0], RuntimeError)