Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: agronholm/exceptiongroup
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 1.1.2
Choose a base ref
...
head repository: agronholm/exceptiongroup
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 1.1.3
Choose a head ref
  • 13 commits
  • 7 files changed
  • 3 contributors

Commits on Jul 3, 2023

  1. [pre-commit.ci] pre-commit autoupdate (#67)

    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    pre-commit-ci[bot] authored Jul 3, 2023
    Copy the full SHA
    fb8903a View commit details

Commits on Jul 5, 2023

  1. Copy the full SHA
    452ba09 View commit details

Commits on Jul 10, 2023

  1. [pre-commit.ci] pre-commit autoupdate (#68)

    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    pre-commit-ci[bot] authored Jul 10, 2023
    Copy the full SHA
    84b4134 View commit details

Commits on Jul 12, 2023

  1. Made catch() raise TypeError on async handler (#69)

    Co-authored-by: Alex Grönholm <alex.gronholm@nextday.fi>
    jakkdl and agronholm authored Jul 12, 2023
    Copy the full SHA
    1d604fb View commit details

Commits on Jul 18, 2023

  1. [pre-commit.ci] pre-commit autoupdate (#72)

    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    pre-commit-ci[bot] authored Jul 18, 2023
    Copy the full SHA
    c971039 View commit details

Commits on Jul 21, 2023

  1. Copy the full SHA
    14bf3ed View commit details

Commits on Jul 24, 2023

  1. [pre-commit.ci] pre-commit autoupdate (#73)

    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    pre-commit-ci[bot] authored Jul 24, 2023
    Copy the full SHA
    4640be7 View commit details

Commits on Jul 29, 2023

  1. Copy the full SHA
    fc578bc View commit details

Commits on Jul 31, 2023

  1. [pre-commit.ci] pre-commit autoupdate (#74)

    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    pre-commit-ci[bot] authored Jul 31, 2023
    Copy the full SHA
    0878b83 View commit details

Commits on Aug 8, 2023

  1. [pre-commit.ci] pre-commit autoupdate (#75)

    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    pre-commit-ci[bot] authored Aug 8, 2023
    Copy the full SHA
    0c94abe View commit details

Commits on Aug 9, 2023

  1. Copy the full SHA
    8b8791b View commit details

Commits on Aug 14, 2023

  1. Copy the full SHA
    516ade1 View commit details
  2. Added the release version

    agronholm committed Aug 14, 2023
    Copy the full SHA
    31d77ff View commit details
Showing with 215 additions and 44 deletions.
  1. +18 −4 .github/workflows/publish.yml
  2. +4 −4 .pre-commit-config.yaml
  3. +12 −0 CHANGES.rst
  4. +23 −5 src/exceptiongroup/_catch.py
  5. +72 −27 src/exceptiongroup/_exceptions.py
  6. +48 −2 tests/test_catch.py
  7. +38 −2 tests/test_catch_py311.py
22 changes: 18 additions & 4 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -9,8 +9,9 @@ on:
- "[0-9]+.[0-9]+.[0-9]+rc[0-9]+"

jobs:
publish:
build:
runs-on: ubuntu-latest
environment: release
steps:
- uses: actions/checkout@v3
- name: Set up Python
@@ -20,8 +21,21 @@ jobs:
- name: Install dependencies
run: pip install build
- name: Create packages
run: python -m build -s -w .
run: python -m build
- name: Archive packages
uses: actions/upload-artifact@v3
with:
name: dist
path: dist

publish:
needs: build
runs-on: ubuntu-latest
environment: release
permissions:
id-token: write
steps:
- name: Retrieve packages
uses: actions/download-artifact@v3
- name: Upload packages
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.pypi_token }}
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -16,20 +16,20 @@ repos:
- id: trailing-whitespace

- repo: https://github.com/asottile/pyupgrade
rev: v3.6.0
rev: v3.10.1
hooks:
- id: pyupgrade
args: ["--py37-plus", "--keep-runtime-typing"]

- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.272
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.0.282
hooks:
- id: ruff
args: [--fix, --show-fixes]
exclude: "tests/test_catch_py311.py"

- repo: https://github.com/psf/black
rev: 23.3.0
rev: 23.7.0
hooks:
- id: black
exclude: "tests/test_catch_py311.py"
12 changes: 12 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -3,6 +3,18 @@ Version history

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

**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
28 changes: 23 additions & 5 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
@@ -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

@@ -49,9 +59,20 @@ 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:
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
@@ -60,9 +81,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
99 changes: 72 additions & 27 deletions src/exceptiongroup/_exceptions.py
Original file line number Diff line number Diff line change
@@ -105,6 +105,12 @@ def exceptions(
) -> tuple[_BaseExceptionT_co | BaseExceptionGroup[_BaseExceptionT_co], ...]:
return tuple(self._exceptions)

@overload
def subgroup(
self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...]
) -> ExceptionGroup[_ExceptionT] | None:
...

@overload
def subgroup(
self, __condition: type[_BaseExceptionT] | tuple[type[_BaseExceptionT], ...]
@@ -113,16 +119,16 @@ def subgroup(

@overload
def subgroup(
self: Self, __condition: Callable[[_BaseExceptionT_co], bool]
) -> Self | None:
self, __condition: Callable[[_BaseExceptionT_co | Self], bool]
) -> BaseExceptionGroup[_BaseExceptionT_co] | None:
...

def subgroup(
self: Self,
self,
__condition: type[_BaseExceptionT]
| tuple[type[_BaseExceptionT], ...]
| Callable[[_BaseExceptionT_co], bool],
) -> BaseExceptionGroup[_BaseExceptionT] | Self | None:
| Callable[[_BaseExceptionT_co | Self], bool],
) -> BaseExceptionGroup[_BaseExceptionT] | None:
condition = get_condition_filter(__condition)
modified = False
if condition(self):
@@ -155,25 +161,49 @@ def subgroup(

@overload
def split(
self: Self,
__condition: type[_BaseExceptionT] | tuple[type[_BaseExceptionT], ...],
) -> tuple[BaseExceptionGroup[_BaseExceptionT] | None, Self | None]:
self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...]
) -> tuple[
ExceptionGroup[_ExceptionT] | None,
BaseExceptionGroup[_BaseExceptionT_co] | None,
]:
...

@overload
def split(
self: Self, __condition: Callable[[_BaseExceptionT_co], bool]
) -> tuple[Self | None, Self | None]:
self, __condition: type[_BaseExceptionT] | tuple[type[_BaseExceptionT], ...]
) -> tuple[
BaseExceptionGroup[_BaseExceptionT] | None,
BaseExceptionGroup[_BaseExceptionT_co] | None,
]:
...

@overload
def split(
self, __condition: Callable[[_BaseExceptionT_co | Self], bool]
) -> tuple[
BaseExceptionGroup[_BaseExceptionT_co] | None,
BaseExceptionGroup[_BaseExceptionT_co] | None,
]:
...

def split(
self: Self,
self,
__condition: type[_BaseExceptionT]
| tuple[type[_BaseExceptionT], ...]
| Callable[[_BaseExceptionT_co], bool],
) -> (
tuple[BaseExceptionGroup[_BaseExceptionT] | None, Self | None]
| tuple[Self | None, Self | None]
tuple[
ExceptionGroup[_ExceptionT] | None,
BaseExceptionGroup[_BaseExceptionT_co] | None,
]
| tuple[
BaseExceptionGroup[_BaseExceptionT] | None,
BaseExceptionGroup[_BaseExceptionT_co] | None,
]
| tuple[
BaseExceptionGroup[_BaseExceptionT_co] | None,
BaseExceptionGroup[_BaseExceptionT_co] | None,
]
):
condition = get_condition_filter(__condition)
if condition(self):
@@ -210,7 +240,19 @@ def split(

return matching_group, nonmatching_group

def derive(self: Self, __excs: Sequence[_BaseExceptionT_co]) -> Self:
@overload
def derive(self, __excs: Sequence[_ExceptionT]) -> ExceptionGroup[_ExceptionT]:
...

@overload
def derive(
self, __excs: Sequence[_BaseExceptionT]
) -> BaseExceptionGroup[_BaseExceptionT]:
...

def derive(
self, __excs: Sequence[_BaseExceptionT]
) -> BaseExceptionGroup[_BaseExceptionT]:
eg = BaseExceptionGroup(self.message, __excs)
if hasattr(self, "__notes__"):
# Create a new list so that add_note() only affects one exceptiongroup
@@ -246,37 +288,40 @@ def subgroup(

@overload
def subgroup(
self: Self, __condition: Callable[[_ExceptionT_co], bool]
) -> Self | None:
self, __condition: Callable[[_ExceptionT_co | Self], bool]
) -> ExceptionGroup[_ExceptionT_co] | None:
...

def subgroup(
self: Self,
self,
__condition: type[_ExceptionT]
| tuple[type[_ExceptionT], ...]
| Callable[[_ExceptionT_co], bool],
) -> ExceptionGroup[_ExceptionT] | Self | None:
) -> ExceptionGroup[_ExceptionT] | None:
return super().subgroup(__condition)

@overload # type: ignore[override]
@overload
def split(
self: Self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...]
) -> tuple[ExceptionGroup[_ExceptionT] | None, Self | None]:
self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...]
) -> tuple[
ExceptionGroup[_ExceptionT] | None, ExceptionGroup[_ExceptionT_co] | None
]:
...

@overload
def split(
self: Self, __condition: Callable[[_ExceptionT_co], bool]
) -> tuple[Self | None, Self | None]:
self, __condition: Callable[[_ExceptionT_co | Self], bool]
) -> tuple[
ExceptionGroup[_ExceptionT_co] | None, ExceptionGroup[_ExceptionT_co] | None
]:
...

def split(
self: Self,
__condition: type[_ExceptionT]
| tuple[type[_ExceptionT], ...]
| Callable[[_ExceptionT_co], bool],
) -> (
tuple[ExceptionGroup[_ExceptionT] | None, Self | None]
| tuple[Self | None, Self | None]
):
) -> tuple[
ExceptionGroup[_ExceptionT_co] | None, ExceptionGroup[_ExceptionT_co] | None
]:
return super().split(__condition)
50 changes: 48 additions & 2 deletions tests/test_catch.py
Original file line number Diff line number Diff line change
@@ -148,9 +148,41 @@ def test_catch_handler_raises():
def handler(exc):
raise RuntimeError("new")

with pytest.raises(RuntimeError, match="new"):
with pytest.raises(RuntimeError, match="new") as exc:
with catch({(ValueError, ValueError): handler}):
raise ExceptionGroup("booboo", [ValueError("bar")])
excgrp = ExceptionGroup("booboo", [ValueError("bar")])
raise excgrp

context = exc.value.__context__
assert isinstance(context, ExceptionGroup)
assert str(context) == "booboo (1 sub-exception)"
assert len(context.exceptions) == 1
assert isinstance(context.exceptions[0], ValueError)
assert exc.value.__cause__ is None


def test_bare_raise_in_handler():
"""Test that a bare "raise" "middle" ecxeption group gets discarded."""

def handler(exc):
raise

with pytest.raises(ExceptionGroup) as excgrp:
with catch({(ValueError,): handler, (RuntimeError,): lambda eg: None}):
try:
first_exc = RuntimeError("first")
raise first_exc
except RuntimeError as exc:
middle_exc = ExceptionGroup(
"bad", [ValueError(), ValueError(), TypeError()]
)
raise middle_exc from exc

assert len(excgrp.value.exceptions) == 2
assert all(isinstance(exc, ValueError) for exc in excgrp.value.exceptions)
assert excgrp.value is not middle_exc
assert excgrp.value.__cause__ is first_exc
assert excgrp.value.__context__ is first_exc


def test_catch_subclass():
@@ -162,3 +194,17 @@ def test_catch_subclass():
assert isinstance(lookup_errors[0], ExceptionGroup)
exceptions = lookup_errors[0].exceptions
assert isinstance(exceptions[0], KeyError)


def test_async_handler(request):
async def handler(eg):
pass

def delegate(eg):
coro = handler(eg)
request.addfinalizer(coro.close)
return coro

with pytest.raises(TypeError, match="Exception handler must be a sync function."):
with catch({TypeError: delegate}):
raise ExceptionGroup("message", [TypeError("uh-oh")])
Loading