Skip to content

improve raisesgroup code coverage #13275

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 7, 2025
Merged
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
3 changes: 2 additions & 1 deletion src/_pytest/_code/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,8 @@ def stringify_exception(
HTTPError = getattr(sys.modules.get("urllib.error", None), "HTTPError", ())
if sys.version_info < (3, 12) and isinstance(exc, HTTPError):
notes = []
else:
else: # pragma: no cover
# exception not related to above bug, reraise
raise
if not include_subexception_msg and isinstance(exc, BaseExceptionGroup):
message = exc.message
Expand Down
6 changes: 0 additions & 6 deletions src/_pytest/raises_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,6 @@ def _parse_exc(
self.is_baseexception = True
return cast(type[BaseExcT_1], origin_exc)
else:
# I kinda think this should be a TypeError...
raise ValueError(
f"Only `ExceptionGroup[Exception]` or `BaseExceptionGroup[BaseExeption]` "
f"are accepted as generic types but got `{exc}`. "
Expand Down Expand Up @@ -424,11 +423,6 @@ def __enter__(self) -> ExceptionInfo[BaseExcT_co_default]:
self.excinfo: ExceptionInfo[BaseExcT_co_default] = ExceptionInfo.for_later()
return self.excinfo

def expected_type(self) -> str:
if self.expected_exceptions == ():
return "BaseException"
return _exception_type_name(self.expected_exceptions)

# TODO: move common code into superclass
def __exit__(
self,
Expand Down
24 changes: 21 additions & 3 deletions testing/python/raises.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,19 @@ def test_raises_does_not_allow_none(self):
# so we can ignore Mypy telling us that None is invalid.
pytest.raises(expected_exception=None) # type: ignore

# it's unclear if this message is helpful, and if it is, should it trigger more
# liberally? Usually you'd get a TypeError here
def test_raises_false_and_arg(self):
with pytest.raises(
ValueError,
match=wrap_escape(
"Expected an exception type or a tuple of exception types, but got `False`. "
"Raising exceptions is already understood as failing the test, so you don't need "
"any special code to say 'this should never raise an exception'."
),
):
pytest.raises(False, int) # type: ignore[call-overload]

def test_raises_does_not_allow_empty_tuple(self):
with pytest.raises(
ValueError,
Expand Down Expand Up @@ -219,7 +232,7 @@ def __call__(self):
elif method == "with_group":
with pytest.RaisesGroup(ValueError, allow_unwrapped=True):
t()
else:
else: # pragma: no cover
raise AssertionError("bad parametrization")

# ensure both forms of pytest.raises don't leave exceptions in sys.exc_info()
Expand Down Expand Up @@ -337,10 +350,15 @@ def __class__(self):
def test_raises_context_manager_with_kwargs(self):
with pytest.raises(expected_exception=ValueError):
raise ValueError
with pytest.raises(TypeError) as excinfo:
with pytest.raises(
TypeError,
match=wrap_escape(
"Unexpected keyword arguments passed to pytest.raises: foo\n"
"Use context-manager form instead?"
),
):
with pytest.raises(OSError, foo="bar"): # type: ignore[call-overload]
pass
assert "Unexpected keyword arguments" in str(excinfo.value)

def test_expected_exception_is_not_a_baseexception(self) -> None:
with pytest.raises(TypeError) as excinfo:
Expand Down
29 changes: 20 additions & 9 deletions testing/python/raises_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -458,12 +458,16 @@ def my_check(e: object) -> bool: # pragma: no cover
assert RaisesGroup(ValueError, match="bar").matches(exc.value)


def test_RaisesGroup_matches() -> None:
def test_matches() -> None:
rg = RaisesGroup(ValueError)
assert not rg.matches(None)
assert not rg.matches(ValueError())
assert rg.matches(ExceptionGroup("", (ValueError(),)))

re = RaisesExc(ValueError)
assert not re.matches(None)
assert re.matches(ValueError())


def test_message() -> None:
def check_message(
Expand Down Expand Up @@ -884,11 +888,13 @@ def test_assert_message_nested() -> None:
)


# CI always runs with hypothesis, but this is not a critical test - it overlaps
# with several others
@pytest.mark.skipif(
"hypothesis" in sys.modules,
reason="hypothesis may have monkeypatched _check_repr",
)
def test_check_no_patched_repr() -> None:
def test_check_no_patched_repr() -> None: # pragma: no cover
# We make `_check_repr` monkeypatchable to avoid this very ugly and verbose
# repr. The other tests that use `check` make use of `_check_repr` so they'll
# continue passing in case it is patched - but we have this one test that
Expand Down Expand Up @@ -1288,15 +1294,20 @@ def test_parametrizing_conditional_raisesgroup(
def test_annotated_group() -> None:
# repr depends on if exceptiongroup backport is being used or not
t = repr(ExceptionGroup[ValueError])
fail_msg = wrap_escape(
f"Only `ExceptionGroup[Exception]` or `BaseExceptionGroup[BaseExeption]` are accepted as generic types but got `{t}`. As `raises` will catch all instances of the specified group regardless of the generic argument specific nested exceptions has to be checked with `RaisesGroup`."
)
msg = "Only `ExceptionGroup[Exception]` or `BaseExceptionGroup[BaseExeption]` are accepted as generic types but got `{}`. As `raises` will catch all instances of the specified group regardless of the generic argument specific nested exceptions has to be checked with `RaisesGroup`."

fail_msg = wrap_escape(msg.format(t))
with pytest.raises(ValueError, match=fail_msg):
with RaisesGroup(ExceptionGroup[ValueError]):
... # pragma: no cover
RaisesGroup(ExceptionGroup[ValueError])
with pytest.raises(ValueError, match=fail_msg):
with RaisesExc(ExceptionGroup[ValueError]):
... # pragma: no cover
RaisesExc(ExceptionGroup[ValueError])
with pytest.raises(
ValueError,
match=wrap_escape(msg.format(repr(BaseExceptionGroup[KeyboardInterrupt]))),
):
with RaisesExc(BaseExceptionGroup[KeyboardInterrupt]):
raise BaseExceptionGroup("", [KeyboardInterrupt()])

with RaisesGroup(ExceptionGroup[Exception]):
raise ExceptionGroup(
"", [ExceptionGroup("", [ValueError(), ValueError(), ValueError()])]
Expand Down