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
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,7 @@ Tareq Alayan
Tatiana Ovary
Ted Xiao
Terje Runde
theman6660
Thomas Grainger
Thomas Hisch
Tianyu Dongfang
Expand Down
1 change: 1 addition & 0 deletions changelog/14324.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix :class:`pytest.RaisesGroup` calling its ``check`` callback on a contained exception while constructing a failure message. The callback is now called only on the exception group.
14 changes: 1 addition & 13 deletions src/_pytest/raises.py
Original file line number Diff line number Diff line change
Expand Up @@ -1193,21 +1193,9 @@ def matches(

# Only run `self.check` once we know `exception` is of the correct type.
if not self._check_check(exception):
reason = (
self._fail_reason = (
cast(str, self._fail_reason) + f" on the {type(exception).__name__}"
)
if (
len(actual_exceptions) == len(self.expected_exceptions) == 1
and isinstance(expected := self.expected_exceptions[0], type)
# we explicitly break typing here :)
and self._check_check(actual_exceptions[0]) # type: ignore[arg-type]
):
self._fail_reason = reason + (
f", but did return True for the expected {self._repr_expected(expected)}."
f" You might want RaisesGroup(RaisesExc({expected.__name__}, check=<...>))"
)
else:
self._fail_reason = reason
return False

return True
Expand Down
36 changes: 34 additions & 2 deletions testing/python/raises_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,16 +426,48 @@ def is_exc(e: ExceptionGroup[ValueError]) -> bool:
def is_value_error(e: BaseException) -> bool:
return isinstance(e, ValueError)

# helpful suggestion if the user thinks the check is for the sub-exception
# A failed check must not be retried on the contained exception.
with (
fails_raises_group(
f"check {is_value_error} did not return True on the ExceptionGroup, but did return True for the expected ValueError. You might want RaisesGroup(RaisesExc(ValueError, check=<...>))"
f"check {is_value_error} did not return True on the ExceptionGroup"
),
RaisesGroup(ValueError, check=is_value_error),
):
raise ExceptionGroup("", (ValueError(),))


@pytest.mark.parametrize("accepted", [False, True])
@pytest.mark.parametrize("exception_type", [ValueError, KeyboardInterrupt])
def test_check_only_receives_group(
accepted: bool, exception_type: type[BaseException]
) -> None:
group = BaseExceptionGroup("message", [exception_type()])
calls: list[BaseExceptionGroup[BaseException]] = []

def check(exc: BaseExceptionGroup[BaseException]) -> bool:
calls.append(exc)
return len(exc.exceptions) == 1 and accepted

matcher = RaisesGroup(exception_type, check=check)
assert matcher.matches(group) is accepted
assert calls == [group]
if not accepted:
assert matcher.fail_reason == (
f"check {repr_callable(check)} did not return True on the {type(group).__name__}"
)


def test_check_exception_propagates() -> None:
error = RuntimeError("check failed")

def check(exc: ExceptionGroup[ValueError]) -> bool:
raise error

with pytest.raises(RuntimeError) as excinfo:
RaisesGroup(ValueError, check=check).matches(ExceptionGroup("", [ValueError()]))
assert excinfo.value is error


def test_unwrapped_match_check() -> None:
def my_check(e: object) -> bool: # pragma: no cover
return True
Expand Down
Loading