Merge pull request #11959 from eerovaher/warn-message-type
Allow using `warnings.warn()` with a `Warning`
This commit is contained in:
		
						commit
						6ef0cf150a
					
				
							
								
								
									
										1
									
								
								AUTHORS
								
								
								
								
							
							
						
						
									
										1
									
								
								AUTHORS
								
								
								
								
							| 
						 | 
					@ -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
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
            raise TypeError(
 | 
					            # to filter those warnings. See
 | 
				
			||||||
                f"Warning message must be str, got {msg!r} (type {type(msg).__name__})"
 | 
					            # 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(
 | 
				
			||||||
 | 
					                    f"Warning must be str or Warning, got {msg!r} (type {type(msg).__name__})"
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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):
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue