Skip to content

Commit 0475b1c

Browse files
eerovaherbluetech
authored andcommitted
Allow using warnings.warn() with Warnings
Test: `warnings.warn()` expects that its first argument is a `str` or a `Warning`, but since 9454fc3 `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]
1 parent dcf9da9 commit 0475b1c

File tree

4 files changed

+35
-11
lines changed

4 files changed

+35
-11
lines changed

AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ Edison Gustavo Muenz
127127
Edoardo Batini
128128
Edson Tadeu M. Manoel
129129
Eduardo Schettino
130+
Eero Vaher
130131
Eli Boyarski
131132
Elizaveta Shashkova
132133
Éloi Rivard

changelog/10865.improvement.rst

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1-
: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.
1+
:func:`pytest.warns` now validates that :func:`warnings.warn` was called with a `str` or a `Warning`.
2+
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).
23
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.

src/_pytest/recwarn.py

+13-2
Original file line numberDiff line numberDiff line change
@@ -351,9 +351,20 @@ def found_str() -> str:
351351
# pytest as the error message produced without this check in place
352352
# is confusing (#10865).
353353
for w in self:
354-
msg = w.message.args[0] # type: ignore[union-attr]
354+
if type(w.message) is not UserWarning:
355+
# If the warning was of an incorrect type then `warnings.warn()`
356+
# creates a UserWarning. Any other warning must have been specified
357+
# explicitly.
358+
continue
359+
if not w.message.args:
360+
# UserWarning() without arguments must have been specified explicitly.
361+
continue
362+
msg = w.message.args[0]
355363
if isinstance(msg, str):
356364
continue
365+
# It's possible that UserWarning was explicitly specified, and
366+
# its first argument was not a string. But that case can't be
367+
# distinguished from an invalid type.
357368
raise TypeError(
358-
f"Warning message must be str, got {msg!r} (type {type(msg).__name__})"
369+
f"Warning must be str or Warning, got {msg!r} (type {type(msg).__name__})"
359370
)

testing/test_recwarn.py

+19-8
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import List
44
from typing import Optional
55
from typing import Type
6+
from typing import Union
67
import warnings
78

89
import pytest
@@ -546,24 +547,34 @@ def test_it():
546547
result.assert_outcomes()
547548

548549

549-
def test_raise_type_error_on_non_string_warning() -> None:
550-
"""Check pytest.warns validates warning messages are strings (#10865)."""
551-
with pytest.raises(TypeError, match="Warning message must be str"):
550+
def test_raise_type_error_on_invalid_warning() -> None:
551+
"""Check pytest.warns validates warning messages are strings (#10865) or
552+
Warning instances (#11959)."""
553+
with pytest.raises(TypeError, match="Warning must be str or Warning"):
552554
with pytest.warns(UserWarning):
553555
warnings.warn(1) # type: ignore
554556

555557

556-
def test_no_raise_type_error_on_string_warning() -> None:
557-
"""Check pytest.warns validates warning messages are strings (#10865)."""
558-
with pytest.warns(UserWarning):
559-
warnings.warn("Warning")
558+
@pytest.mark.parametrize(
559+
"message",
560+
[
561+
pytest.param("Warning", id="str"),
562+
pytest.param(UserWarning(), id="UserWarning"),
563+
pytest.param(Warning(), id="Warning"),
564+
],
565+
)
566+
def test_no_raise_type_error_on_valid_warning(message: Union[str, Warning]) -> None:
567+
"""Check pytest.warns validates warning messages are strings (#10865) or
568+
Warning instances (#11959)."""
569+
with pytest.warns(Warning):
570+
warnings.warn(message)
560571

561572

562573
@pytest.mark.skipif(
563574
hasattr(sys, "pypy_version_info"),
564575
reason="Not for pypy",
565576
)
566-
def test_raise_type_error_on_non_string_warning_cpython() -> None:
577+
def test_raise_type_error_on_invalid_warning_message_cpython() -> None:
567578
# Check that we get the same behavior with the stdlib, at least if filtering
568579
# (see https://github.com/python/cpython/issues/103577 for details)
569580
with pytest.raises(TypeError):

0 commit comments

Comments
 (0)