Allow using `warnings.warn()` with Warnings

Test:

`warnings.warn()` expects that its first argument is a `str` or a
`Warning`, but since 9454fc38d3
`pytest.warns()` no longer allows `Warning` instances unless the first
argument the `Warning` was initialized with is a `str`. Furthermore, if
the `Warning` was created without arguments then `pytest.warns()` raises
an unexpected `IndexError`. The new tests reveal the problem.

Fix:

`pytest.warns()` now allows using `warnings.warn()` with a `Warning`
instance, as is required by Python, with one exception. If the warning
used is a `UserWarning` that was created by passing it arguments and the
first argument was not a `str` then `pytest.raises()` still considers
that an error. This is because if an invalid type was used in
`warnings.warn()` then Python creates a `UserWarning` anyways and it
becomes impossible for `pytest` to figure out if that was done
automatically or not.

[ran: rebased on previous commit]
This commit is contained in:
Eero Vaher 2024-02-09 21:13:41 +01:00 committed by Ran Benita
parent dcf9da92be
commit 0475b1c925
4 changed files with 35 additions and 11 deletions

View File

@ -127,6 +127,7 @@ Edison Gustavo Muenz
Edoardo Batini Edoardo Batini
Edson Tadeu M. Manoel Edson Tadeu M. Manoel
Eduardo Schettino Eduardo Schettino
Eero Vaher
Eli Boyarski Eli Boyarski
Elizaveta Shashkova Elizaveta Shashkova
Éloi Rivard Éloi Rivard

View File

@ -1,2 +1,3 @@
:func:`pytest.warns` now validates that warning object's ``message`` is of type `str` -- currently in Python it is possible to pass other types than `str` when creating `Warning` instances, however this causes an exception when :func:`warnings.filterwarnings` is used to filter those warnings. See `CPython #103577 <https://github.com/python/cpython/issues/103577>`__ for a discussion. :func:`pytest.warns` now validates that :func:`warnings.warn` was called with a `str` or a `Warning`.
Currently in Python it is possible to use other types, however this causes an exception when :func:`warnings.filterwarnings` is used to filter those warnings (see `CPython #103577 <https://github.com/python/cpython/issues/103577>`__ for a discussion).
While this can be considered a bug in CPython, we decided to put guards in pytest as the error message produced without this check in place is confusing. While this can be considered a bug in CPython, we decided to put guards in pytest as the error message produced without this check in place is confusing.

View File

@ -351,9 +351,20 @@ class WarningsChecker(WarningsRecorder):
# pytest as the error message produced without this check in place # pytest as the error message produced without this check in place
# is confusing (#10865). # is confusing (#10865).
for w in self: for w in self:
msg = w.message.args[0] # type: ignore[union-attr] if type(w.message) is not UserWarning:
# If the warning was of an incorrect type then `warnings.warn()`
# creates a UserWarning. Any other warning must have been specified
# explicitly.
continue
if not w.message.args:
# UserWarning() without arguments must have been specified explicitly.
continue
msg = w.message.args[0]
if isinstance(msg, str): if isinstance(msg, str):
continue continue
# It's possible that UserWarning was explicitly specified, and
# its first argument was not a string. But that case can't be
# distinguished from an invalid type.
raise TypeError( raise TypeError(
f"Warning message must be str, got {msg!r} (type {type(msg).__name__})" f"Warning must be str or Warning, got {msg!r} (type {type(msg).__name__})"
) )

View File

@ -3,6 +3,7 @@ import sys
from typing import List from typing import List
from typing import Optional from typing import Optional
from typing import Type from typing import Type
from typing import Union
import warnings import warnings
import pytest import pytest
@ -546,24 +547,34 @@ class TestWarns:
result.assert_outcomes() result.assert_outcomes()
def test_raise_type_error_on_non_string_warning() -> None: def test_raise_type_error_on_invalid_warning() -> None:
"""Check pytest.warns validates warning messages are strings (#10865).""" """Check pytest.warns validates warning messages are strings (#10865) or
with pytest.raises(TypeError, match="Warning message must be str"): Warning instances (#11959)."""
with pytest.raises(TypeError, match="Warning must be str or Warning"):
with pytest.warns(UserWarning): with pytest.warns(UserWarning):
warnings.warn(1) # type: ignore warnings.warn(1) # type: ignore
def test_no_raise_type_error_on_string_warning() -> None: @pytest.mark.parametrize(
"""Check pytest.warns validates warning messages are strings (#10865).""" "message",
with pytest.warns(UserWarning): [
warnings.warn("Warning") pytest.param("Warning", id="str"),
pytest.param(UserWarning(), id="UserWarning"),
pytest.param(Warning(), id="Warning"),
],
)
def test_no_raise_type_error_on_valid_warning(message: Union[str, Warning]) -> None:
"""Check pytest.warns validates warning messages are strings (#10865) or
Warning instances (#11959)."""
with pytest.warns(Warning):
warnings.warn(message)
@pytest.mark.skipif( @pytest.mark.skipif(
hasattr(sys, "pypy_version_info"), hasattr(sys, "pypy_version_info"),
reason="Not for pypy", reason="Not for pypy",
) )
def test_raise_type_error_on_non_string_warning_cpython() -> None: def test_raise_type_error_on_invalid_warning_message_cpython() -> None:
# Check that we get the same behavior with the stdlib, at least if filtering # Check that we get the same behavior with the stdlib, at least if filtering
# (see https://github.com/python/cpython/issues/103577 for details) # (see https://github.com/python/cpython/issues/103577 for details)
with pytest.raises(TypeError): with pytest.raises(TypeError):