Merge pull request #8677 from olgarithms/warns-no-arg-catches-any

This commit is contained in:
Zac Hatfield-Dodds 2021-05-19 17:47:39 +10:00 committed by GitHub
commit c198a7a67e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 52 additions and 12 deletions

View File

@ -231,6 +231,7 @@ Nicholas Murphy
Niclas Olofsson Niclas Olofsson
Nicolas Delaby Nicolas Delaby
Nikolay Kondratyev Nikolay Kondratyev
Olga Matoula
Oleg Pidsadnyi Oleg Pidsadnyi
Oleg Sushchenko Oleg Sushchenko
Oliver Bestwalter Oliver Bestwalter

View File

@ -0,0 +1,4 @@
Reducing confusion from `pytest.warns(None)` by:
- Allowing no arguments to be passed in order to catch any exception (no argument defaults to `Warning`).
- Emit a deprecation warning if passed `None`.

View File

@ -332,11 +332,11 @@ You can record raised warnings either using func:`pytest.warns` or with
the ``recwarn`` fixture. the ``recwarn`` fixture.
To record with func:`pytest.warns` without asserting anything about the warnings, To record with func:`pytest.warns` without asserting anything about the warnings,
pass ``None`` as the expected warning type: pass no arguments as the expected warning type and it will default to a generic Warning:
.. code-block:: python .. code-block:: python
with pytest.warns(None) as record: with pytest.warns() as record:
warnings.warn("user", UserWarning) warnings.warn("user", UserWarning)
warnings.warn("runtime", RuntimeWarning) warnings.warn("runtime", RuntimeWarning)

View File

@ -101,6 +101,12 @@ HOOK_LEGACY_PATH_ARG = UnformattedWarning(
"see https://docs.pytest.org/en/latest/deprecations.html" "see https://docs.pytest.org/en/latest/deprecations.html"
"#py-path-local-arguments-for-hooks-replaced-with-pathlib-path", "#py-path-local-arguments-for-hooks-replaced-with-pathlib-path",
) )
WARNS_NONE_ARG = PytestDeprecationWarning(
"Passing None to catch any warning has been deprecated, pass no arguments instead:\n"
" Replace pytest.warns(None) by simply pytest.warns()."
)
# You want to make some `__init__` or function "private". # You want to make some `__init__` or function "private".
# #
# def my_private_function(some, args): # def my_private_function(some, args):

View File

@ -17,6 +17,7 @@ from typing import Union
from _pytest.compat import final from _pytest.compat import final
from _pytest.deprecated import check_ispytest from _pytest.deprecated import check_ispytest
from _pytest.deprecated import WARNS_NONE_ARG
from _pytest.fixtures import fixture from _pytest.fixtures import fixture
from _pytest.outcomes import fail from _pytest.outcomes import fail
@ -83,7 +84,7 @@ def deprecated_call(
@overload @overload
def warns( def warns(
expected_warning: Optional[Union[Type[Warning], Tuple[Type[Warning], ...]]], expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]] = ...,
*, *,
match: Optional[Union[str, Pattern[str]]] = ..., match: Optional[Union[str, Pattern[str]]] = ...,
) -> "WarningsChecker": ) -> "WarningsChecker":
@ -92,7 +93,7 @@ def warns(
@overload @overload
def warns( def warns(
expected_warning: Optional[Union[Type[Warning], Tuple[Type[Warning], ...]]], expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]],
func: Callable[..., T], func: Callable[..., T],
*args: Any, *args: Any,
**kwargs: Any, **kwargs: Any,
@ -101,7 +102,7 @@ def warns(
def warns( def warns(
expected_warning: Optional[Union[Type[Warning], Tuple[Type[Warning], ...]]], expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]] = Warning,
*args: Any, *args: Any,
match: Optional[Union[str, Pattern[str]]] = None, match: Optional[Union[str, Pattern[str]]] = None,
**kwargs: Any, **kwargs: Any,
@ -232,7 +233,7 @@ class WarningsChecker(WarningsRecorder):
self, self,
expected_warning: Optional[ expected_warning: Optional[
Union[Type[Warning], Tuple[Type[Warning], ...]] Union[Type[Warning], Tuple[Type[Warning], ...]]
] = None, ] = Warning,
match_expr: Optional[Union[str, Pattern[str]]] = None, match_expr: Optional[Union[str, Pattern[str]]] = None,
*, *,
_ispytest: bool = False, _ispytest: bool = False,
@ -242,6 +243,7 @@ class WarningsChecker(WarningsRecorder):
msg = "exceptions must be derived from Warning, not %s" msg = "exceptions must be derived from Warning, not %s"
if expected_warning is None: if expected_warning is None:
warnings.warn(WARNS_NONE_ARG, stacklevel=4)
expected_warning_tup = None expected_warning_tup = None
elif isinstance(expected_warning, tuple): elif isinstance(expected_warning, tuple):
for exc in expected_warning: for exc in expected_warning:

View File

@ -178,3 +178,15 @@ def test_hookproxy_warnings_for_fspath(tmp_path, hooktype, request):
assert l1 < record.lineno < l2 assert l1 < record.lineno < l2
hooks.pytest_ignore_collect(config=request.config, fspath=tmp_path) hooks.pytest_ignore_collect(config=request.config, fspath=tmp_path)
def test_warns_none_is_deprecated():
with pytest.warns(
PytestDeprecationWarning,
match=re.escape(
"Passing None to catch any warning has been deprecated, pass no arguments instead:\n "
"Replace pytest.warns(None) by simply pytest.warns()."
),
):
with pytest.warns(None): # type: ignore[call-overload]
pass

View File

@ -298,7 +298,7 @@ class TestWarns:
assert str(record[0].message) == "user" assert str(record[0].message) == "user"
def test_record_only(self) -> None: def test_record_only(self) -> None:
with pytest.warns(None) as record: with pytest.warns() as record:
warnings.warn("user", UserWarning) warnings.warn("user", UserWarning)
warnings.warn("runtime", RuntimeWarning) warnings.warn("runtime", RuntimeWarning)
@ -306,6 +306,18 @@ class TestWarns:
assert str(record[0].message) == "user" assert str(record[0].message) == "user"
assert str(record[1].message) == "runtime" assert str(record[1].message) == "runtime"
def test_record_only_none_deprecated_warn(self) -> None:
# This should become an error when WARNS_NONE_ARG is removed in Pytest 7.0
with warnings.catch_warnings():
warnings.simplefilter("ignore")
with pytest.warns(None) as record: # type: ignore[call-overload]
warnings.warn("user", UserWarning)
warnings.warn("runtime", RuntimeWarning)
assert len(record) == 2
assert str(record[0].message) == "user"
assert str(record[1].message) == "runtime"
def test_record_by_subclass(self) -> None: def test_record_by_subclass(self) -> None:
with pytest.warns(Warning) as record: with pytest.warns(Warning) as record:
warnings.warn("user", UserWarning) warnings.warn("user", UserWarning)

View File

@ -1,6 +1,7 @@
import os import os
import stat import stat
import sys import sys
import warnings
from pathlib import Path from pathlib import Path
from typing import Callable from typing import Callable
from typing import cast from typing import cast
@ -400,11 +401,13 @@ class TestRmRf:
assert fn.is_file() assert fn.is_file()
# ignored function # ignored function
with pytest.warns(None) as warninfo: with warnings.catch_warnings():
exc_info4 = (None, PermissionError(), None) warnings.simplefilter("ignore")
on_rm_rf_error(os.open, str(fn), exc_info4, start_path=tmp_path) with pytest.warns(None) as warninfo: # type: ignore[call-overload]
assert fn.is_file() exc_info4 = (None, PermissionError(), None)
assert not [x.message for x in warninfo] on_rm_rf_error(os.open, str(fn), exc_info4, start_path=tmp_path)
assert fn.is_file()
assert not [x.message for x in warninfo]
exc_info5 = (None, PermissionError(), None) exc_info5 = (None, PermissionError(), None)
on_rm_rf_error(os.unlink, str(fn), exc_info5, start_path=tmp_path) on_rm_rf_error(os.unlink, str(fn), exc_info5, start_path=tmp_path)