Merge pull request #11959 from eerovaher/warn-message-type

Allow using `warnings.warn()` with a `Warning`
This commit is contained in:
Ran Benita 2024-02-16 14:00:37 +02:00 committed by GitHub
commit 6ef0cf150a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 49 additions and 20 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

@ -314,7 +314,7 @@ class WarningsChecker(WarningsRecorder):
): ):
return return
def found_str(): def found_str() -> str:
return pformat([record.message for record in self], indent=2) return pformat([record.message for record in self], indent=2)
try: try:
@ -341,14 +341,30 @@ class WarningsChecker(WarningsRecorder):
module=w.__module__, module=w.__module__,
source=w.source, source=w.source,
) )
# Check warnings has valid argument type (#10865).
wrn: warnings.WarningMessage
for wrn in self:
self._validate_message(wrn)
@staticmethod # Currently in Python it is possible to pass other types than an
def _validate_message(wrn: Any) -> None: # `str` message when creating `Warning` instances, however this
if not isinstance(msg := wrn.message.args[0], str): # causes an exception when :func:`warnings.filterwarnings` is used
# to filter those warnings. See
# https://github.com/python/cpython/issues/103577 for a discussion.
# While this can be considered a bug in CPython, we put guards in
# pytest as the error message produced without this check in place
# is confusing (#10865).
for w in self:
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):
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):