Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
*.egg-info
*.dist-info
*.pyc
build
dist
__pycache__
.coverage
.pytest_cache/
.mypy_cache/
.ruff_cache/
.eggs/
.tox
.idea
.cache
.local
venv*/
/src/exceptiongroup/_version.py
24 changes: 24 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: check-added-large-files
- id: check-case-conflict
- id: check-merge-conflict
- id: check-symlinks
- id: check-toml
- id: check-yaml
- id: debug-statements
exclude: "tests/test_catch_py311.py"
- id: end-of-file-fixer
- id: mixed-line-ending
args: ["--fix=lf"]
- id: trailing-whitespace

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.5.0
hooks:
- id: ruff
args: [--fix, --show-fixes]
exclude: "tests/test_catch_py311.py"
- id: ruff-format
36 changes: 36 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,42 @@ Version history

This library adheres to `Semantic Versioning 2.0 <http://semver.org/>`_.

**1.2.2**

- Removed an ``assert`` in ``exceptiongroup._formatting`` that caused compatibility
issues with Sentry (`#123 <https://github.com/agronholm/exceptiongroup/issues/123>`_)

**1.2.1**

- Updated the copying of ``__notes__`` to match CPython behavior (PR by CF Bolz-Tereick)
- Corrected the type annotation of the exception handler callback to accept a
``BaseExceptionGroup`` instead of ``BaseException``
- Fixed type errors on Python < 3.10 and the type annotation of ``suppress()``
(PR by John Litborn)

**1.2.0**

- Added special monkeypatching if `Apport <https://github.com/canonical/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

**1.1.3**

- ``catch()`` now raises a ``TypeError`` if passed an async exception handler instead of
just giving a ``RuntimeWarning`` about the coroutine never being awaited. (#66, PR by
John Litborn)
- Fixed plain ``raise`` statement in an exception handler callback to work like a
``raise`` in an ``except*`` block
- Fixed new exception group not being chained to the original exception when raising an
exception group from exceptions raised in handler callbacks
- Fixed type annotations of the ``derive()``, ``subgroup()`` and ``split()`` methods to
match the ones in typeshed

**1.1.2**

- Changed handling of exceptions in exception group handler callbacks to not wrap a
Expand Down
26 changes: 21 additions & 5 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -52,7 +54,7 @@ containing more matching exceptions.

Thus, the following Python 3.11+ code:

.. code-block:: python3
.. code-block:: python

try:
...
Expand All @@ -64,15 +66,15 @@ Thus, the following Python 3.11+ code:

would be written with this backport like this:

.. code-block:: python3
.. code-block:: python

from exceptiongroup import ExceptionGroup, catch
from exceptiongroup import BaseExceptionGroup, catch

def value_key_err_handler(excgroup: ExceptionGroup) -> None:
def value_key_err_handler(excgroup: BaseExceptionGroup) -> None:
for exc in excgroup.exceptions:
print('Caught exception:', type(exc))

def runtime_err_handler(exc: ExceptionGroup) -> None:
def runtime_err_handler(exc: BaseExceptionGroup) -> None:
print('Caught runtime error')

with catch({
Expand All @@ -84,6 +86,20 @@ 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:

.. code-block:: python

from exceptiongroup import suppress

with suppress(RuntimeError):
raise ExceptionGroup("", [RuntimeError("boo")])

Notes on monkey patching
========================

Expand Down
30 changes: 30 additions & 0 deletions debian/changelog
Original file line number Diff line number Diff line change
@@ -1,3 +1,33 @@
python-exceptiongroup (1.2.2-1) unstable; urgency=medium

* Team upload.
* New upstream release.
* Standards-Version: 4.7.0 (no changes required).

-- Colin Watson <[email protected]> Mon, 15 Jul 2024 11:14:53 +0100

python-exceptiongroup (1.2.1-1) unstable; urgency=medium

* Team upload.
* New upstream release:
- Fix test failure on Python 3.12.3 (closes: #1069817).

-- Colin Watson <[email protected]> Fri, 26 Apr 2024 13:42:11 +0100

python-exceptiongroup (1.2.0-1) unstable; urgency=medium

* Team upload.
* New upstream version 1.2.0

-- Timo Röhling <[email protected]> Tue, 28 Nov 2023 22:14:41 +0100

python-exceptiongroup (1.1.3-1) unstable; urgency=medium

* Team upload.
* New upstream version 1.1.3

-- Timo Röhling <[email protected]> Mon, 21 Aug 2023 11:33:24 +0200

python-exceptiongroup (1.1.2-1) unstable; urgency=medium

* Team upload.
Expand Down
2 changes: 1 addition & 1 deletion debian/control
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Build-Depends:
python3-all,
python3-flit-scm,
python3-pytest <!nocheck>,
Standards-Version: 4.6.2
Standards-Version: 4.7.0
Testsuite: autopkgtest-pkg-pybuild
Vcs-Git: https://salsa.debian.org/python-team/packages/python-exceptiongroup.git
Vcs-Browser: https://salsa.debian.org/python-team/packages/python-exceptiongroup
Expand Down
46 changes: 33 additions & 13 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ test = [

[tool.flit.sdist]
include = [
"CHANGES.rst",
"tests",
]
exclude = [
Expand All @@ -44,18 +45,21 @@ version_scheme = "post-release"
local_scheme = "dirty-tag"
write_to = "src/exceptiongroup/_version.py"

[tool.ruff]
line-length = 88
select = [
"E", "F", "W", # default flake-8
[tool.ruff.lint]
extend-select = [
"I", # isort
"ISC", # flake8-implicit-str-concat
"PGH", # pygrep-hooks
"RUF100", # unused noqa (yesqa)
"UP", # pyupgrade
"W", # pycodestyle warnings
]
target-version = "py37"

[tool.ruff.isort]
[tool.ruff.lint.pyupgrade]
# Preserve types, even if a file imports `from __future__ import annotations`.
keep-runtime-typing = true

[tool.ruff.lint.isort]
known-first-party = ["exceptiongroup"]

[tool.pytest.ini_options]
Expand All @@ -67,25 +71,41 @@ source = ["exceptiongroup"]
relative_files = true

[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:"
exclude_also = [
"if TYPE_CHECKING:",
"@overload",
]

[tool.pyright]
# for type tests, the code itself isn't type checked in CI
reportUnnecessaryTypeIgnoreComment = true

[tool.mypy]
# for type tests, the code itself isn't type checked in CI
warn_unused_ignores = true

[tool.tox]
legacy_tox_ini = """
[tox]
envlist = py37, py38, py39, py310, py311, py312, pypy3
envlist = py37, py38, py39, py310, py311, py312, py313, pypy3
labels =
typing = py{310,311,312}-typing
skip_missing_interpreters = true
minversion = 4.0

[testenv]
extras = test
commands = python -m pytest {posargs}
package = editable
usedevelop = true

[testenv:pyright]
deps = pyright
commands = pyright --verifytypes exceptiongroup
[testenv:{py37-,py38-,py39-,py310-,py311-,py312-,}typing]
deps =
pyright
mypy
commands =
pyright --verifytypes exceptiongroup
pyright tests/check_types.py
mypy tests/check_types.py
usedevelop = true
"""
6 changes: 6 additions & 0 deletions src/exceptiongroup/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"format_exception_only",
"print_exception",
"print_exc",
"suppress",
]

import os
Expand Down Expand Up @@ -38,3 +39,8 @@

BaseExceptionGroup = BaseExceptionGroup
ExceptionGroup = ExceptionGroup

if sys.version_info < (3, 12, 1):
from ._suppress import suppress
else:
from contextlib import suppress
35 changes: 28 additions & 7 deletions src/exceptiongroup/_catch.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import inspect
import sys
from collections.abc import Callable, Iterable, Mapping
from contextlib import AbstractContextManager
Expand All @@ -10,7 +11,7 @@
from ._exceptions import BaseExceptionGroup

if TYPE_CHECKING:
_Handler = Callable[[BaseException], Any]
_Handler = Callable[[BaseExceptionGroup[Any]], Any]


class _Catcher:
Expand All @@ -33,7 +34,16 @@ def __exit__(
elif unhandled is None:
return True
else:
raise unhandled from None
if isinstance(exc, BaseExceptionGroup):
try:
raise unhandled from exc.__cause__
except BaseExceptionGroup:
# Change __context__ to __cause__ because Python 3.11 does this
# too
unhandled.__context__ = exc.__cause__
raise

raise unhandled from exc

return False

Expand All @@ -49,9 +59,23 @@ def handle_exception(self, exc: BaseException) -> BaseException | None:
matched, excgroup = excgroup.split(exc_types)
if matched:
try:
handler(matched)
try:
raise matched
except BaseExceptionGroup:
result = handler(matched)
except BaseExceptionGroup as new_exc:
if new_exc is matched:
new_exceptions.append(new_exc)
else:
new_exceptions.extend(new_exc.exceptions)
except BaseException as new_exc:
new_exceptions.append(new_exc)
else:
if inspect.iscoroutine(result):
raise TypeError(
f"Error trying to handle {matched!r} with {handler!r}. "
"Exception handler must be a sync function."
) from exc

if not excgroup:
break
Expand All @@ -60,9 +84,6 @@ def handle_exception(self, exc: BaseException) -> BaseException | None:
if len(new_exceptions) == 1:
return new_exceptions[0]

if excgroup:
new_exceptions.append(excgroup)

return BaseExceptionGroup("", new_exceptions)
elif (
excgroup and len(excgroup.exceptions) == 1 and excgroup.exceptions[0] is exc
Expand All @@ -73,7 +94,7 @@ def handle_exception(self, exc: BaseException) -> BaseException | None:


def catch(
__handlers: Mapping[type[BaseException] | Iterable[type[BaseException]], _Handler]
__handlers: Mapping[type[BaseException] | Iterable[type[BaseException]], _Handler],
) -> AbstractContextManager[None]:
if not isinstance(__handlers, Mapping):
raise TypeError("the argument must be a mapping")
Expand Down
Loading
Loading