Skip to content

Commit

Permalink
add changelog. helpful suggestion if check would pass on sub-exceptio…
Browse files Browse the repository at this point in the history
…n. export raises_group as a convenience alias.
  • Loading branch information
jakkdl committed Jan 29, 2025
1 parent 37aa8e5 commit aeb5b28
Show file tree
Hide file tree
Showing 4 changed files with 35 additions and 3 deletions.
1 change: 1 addition & 0 deletions changelog/11671.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added `RaisesGroup` (also available as `raises_group`) and `Matcher`, as an equivalent to `raises` for expecting `ExceptionGroup`. It includes the ability to specity multiple different expected exceptions, the structure of nested exception groups, and/or closely emulating `except_star`.
19 changes: 17 additions & 2 deletions src/_pytest/_raises_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -696,8 +696,23 @@ def matches(
return False

# Only run `self.check` once we know `exc_val` is of the correct type.
# TODO: if this fails, we should say the *group* did not match
return self._check_check(exc_val)
if not self._check_check(exc_val):
reason = cast(str, self._fail_reason) + f" on the {type(exc_val).__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(Matcher({expected.__name__}, check=<...>))"
)
else:
self._fail_reason = reason
return False

return True

@staticmethod
def _check_expected(
Expand Down
2 changes: 2 additions & 0 deletions src/pytest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from _pytest._code import ExceptionInfo
from _pytest._raises_group import Matcher
from _pytest._raises_group import RaisesGroup
from _pytest._raises_group import RaisesGroup as raises_group
from _pytest.assertion import register_assert_rewrite
from _pytest.cacheprovider import Cache
from _pytest.capture import CaptureFixture
Expand Down Expand Up @@ -166,6 +167,7 @@
"mark",
"param",
"raises",
"raises_group",
"register_assert_rewrite",
"set_trace",
"skip",
Expand Down
16 changes: 15 additions & 1 deletion testing/python/raises_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,11 +397,25 @@ def is_exc(e: ExceptionGroup[ValueError]) -> bool:
raise exc

with (
fails_raises_group(f"check {is_exc_repr} did not return True"),
fails_raises_group(
f"check {is_exc_repr} did not return True on the ExceptionGroup"
),
RaisesGroup(ValueError, check=is_exc),
):
raise ExceptionGroup("", (ValueError(),))

def is_value_error(e: BaseException) -> bool:
return isinstance(e, ValueError)

# helpful suggestion if the user thinks the check is for the sub-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(Matcher(ValueError, check=<...>))"
),
RaisesGroup(ValueError, check=is_value_error),
):
raise ExceptionGroup("", (ValueError(),))


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

0 comments on commit aeb5b28

Please sign in to comment.