From 8b2f83772d3fbfbbef65c37673b8768ba67b71c5 Mon Sep 17 00:00:00 2001 From: Olga Matoula Date: Sat, 15 May 2021 15:15:43 +0100 Subject: [PATCH 1/7] Catch any warning on warns with no arg passed --- AUTHORS | 1 + changelog/8645.improvement.rst | 4 ++++ doc/en/how-to/capture-warnings.rst | 4 ++-- src/_pytest/deprecated.py | 5 +++++ src/_pytest/recwarn.py | 8 +++++--- testing/deprecated_test.py | 8 ++++++++ testing/test_recwarn.py | 13 ++++++++++++- testing/test_tmpdir.py | 13 ++++++++----- 8 files changed, 45 insertions(+), 11 deletions(-) create mode 100644 changelog/8645.improvement.rst diff --git a/AUTHORS b/AUTHORS index 5f300c9d4..b822d469a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -231,6 +231,7 @@ Nicholas Murphy Niclas Olofsson Nicolas Delaby Nikolay Kondratyev +Olga Matoula Oleg Pidsadnyi Oleg Sushchenko Oliver Bestwalter diff --git a/changelog/8645.improvement.rst b/changelog/8645.improvement.rst new file mode 100644 index 000000000..b3a68b0b8 --- /dev/null +++ b/changelog/8645.improvement.rst @@ -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`. diff --git a/doc/en/how-to/capture-warnings.rst b/doc/en/how-to/capture-warnings.rst index 1bafaeeb9..28e071c45 100644 --- a/doc/en/how-to/capture-warnings.rst +++ b/doc/en/how-to/capture-warnings.rst @@ -332,11 +332,11 @@ You can record raised warnings either using func:`pytest.warns` or with the ``recwarn`` fixture. 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 - with pytest.warns(None) as record: + with pytest.warns() as record: warnings.warn("user", UserWarning) warnings.warn("runtime", RuntimeWarning) diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 7a09d5163..9f4b71bdc 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -101,6 +101,11 @@ HOOK_LEGACY_PATH_ARG = UnformattedWarning( "see https://docs.pytest.org/en/latest/deprecations.html" "#py-path-local-arguments-for-hooks-replaced-with-pathlib-path", ) + +WARNS_NONE_ARG = PytestDeprecationWarning( + "Please pass an explicit Warning type or tuple of Warning types." +) + # You want to make some `__init__` or function "private". # # def my_private_function(some, args): diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index bc3a2835e..852b4c191 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -17,6 +17,7 @@ from typing import Union from _pytest.compat import final from _pytest.deprecated import check_ispytest +from _pytest.deprecated import WARNS_NONE_ARG from _pytest.fixtures import fixture from _pytest.outcomes import fail @@ -83,7 +84,7 @@ def deprecated_call( @overload def warns( - expected_warning: Optional[Union[Type[Warning], Tuple[Type[Warning], ...]]], + expected_warning: Optional[Union[Type[Warning], Tuple[Type[Warning], ...]]] = ..., *, match: Optional[Union[str, Pattern[str]]] = ..., ) -> "WarningsChecker": @@ -101,7 +102,7 @@ def warns( def warns( - expected_warning: Optional[Union[Type[Warning], Tuple[Type[Warning], ...]]], + expected_warning: Optional[Union[Type[Warning], Tuple[Type[Warning], ...]]] = Warning, *args: Any, match: Optional[Union[str, Pattern[str]]] = None, **kwargs: Any, @@ -232,7 +233,7 @@ class WarningsChecker(WarningsRecorder): self, expected_warning: Optional[ Union[Type[Warning], Tuple[Type[Warning], ...]] - ] = None, + ] = Warning, match_expr: Optional[Union[str, Pattern[str]]] = None, *, _ispytest: bool = False, @@ -242,6 +243,7 @@ class WarningsChecker(WarningsRecorder): msg = "exceptions must be derived from Warning, not %s" if expected_warning is None: + warnings.warn(WARNS_NONE_ARG, stacklevel=4) expected_warning_tup = None elif isinstance(expected_warning, tuple): for exc in expected_warning: diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 1d012adf2..86650877e 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -178,3 +178,11 @@ def test_hookproxy_warnings_for_fspath(tmp_path, hooktype, request): assert l1 < record.lineno < l2 hooks.pytest_ignore_collect(config=request.config, fspath=tmp_path) + + +def test_warns_none_is_deprecated(): + with pytest.warns( + PytestDeprecationWarning, + match="Please pass an explicit Warning type or tuple of Warning types.", + ): + pytest.warns(None) diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index 91efe6d23..c73ab8a11 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -298,7 +298,7 @@ class TestWarns: assert str(record[0].message) == "user" def test_record_only(self) -> None: - with pytest.warns(None) as record: + with pytest.warns() as record: warnings.warn("user", UserWarning) warnings.warn("runtime", RuntimeWarning) @@ -306,6 +306,17 @@ class TestWarns: assert str(record[0].message) == "user" assert str(record[1].message) == "runtime" + def test_record_only_none_deprecated_warn(self) -> None: + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + with pytest.warns(None) as record: + 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: with pytest.warns(Warning) as record: warnings.warn("user", UserWarning) diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 40e75663c..fe4b8b8cd 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -1,6 +1,7 @@ import os import stat import sys +import warnings from pathlib import Path from typing import Callable from typing import cast @@ -400,11 +401,13 @@ class TestRmRf: assert fn.is_file() # ignored function - with pytest.warns(None) as warninfo: - exc_info4 = (None, PermissionError(), None) - 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] + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + with pytest.warns(None) as warninfo: + exc_info4 = (None, PermissionError(), None) + 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) on_rm_rf_error(os.unlink, str(fn), exc_info5, start_path=tmp_path) From 6ae71a2c2b7bdadf4f16c0eac960838315b51198 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 15 May 2021 17:51:54 +0000 Subject: [PATCH 2/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/_pytest/recwarn.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 852b4c191..715627209 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -102,7 +102,9 @@ def warns( def warns( - expected_warning: Optional[Union[Type[Warning], Tuple[Type[Warning], ...]]] = Warning, + expected_warning: Optional[ + Union[Type[Warning], Tuple[Type[Warning], ...]] + ] = Warning, *args: Any, match: Optional[Union[str, Pattern[str]]] = None, **kwargs: Any, From dbe66d97b49d94b131fa9ccbcf549f6376f88d9c Mon Sep 17 00:00:00 2001 From: Olga Matoula Date: Sun, 16 May 2021 12:07:39 +0100 Subject: [PATCH 3/7] Add better warning msg for deprecated warns(None) --- src/_pytest/deprecated.py | 3 ++- testing/deprecated_test.py | 5 +++-- testing/test_recwarn.py | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 9f4b71bdc..99907d128 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -103,7 +103,8 @@ HOOK_LEGACY_PATH_ARG = UnformattedWarning( ) WARNS_NONE_ARG = PytestDeprecationWarning( - "Please pass an explicit Warning type or tuple of Warning types." + "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". diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 86650877e..0974cf6c6 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -183,6 +183,7 @@ def test_hookproxy_warnings_for_fspath(tmp_path, hooktype, request): def test_warns_none_is_deprecated(): with pytest.warns( PytestDeprecationWarning, - match="Please pass an explicit Warning type or tuple of Warning types.", + match=r"Passing None to catch any warning has been deprecated, pass no arguments instead:\n Replace pytest.warns\(None\) by simply pytest.warns\(\).", ): - pytest.warns(None) + with pytest.warns(None): + pass diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index c73ab8a11..963f3aef2 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -307,6 +307,7 @@ class TestWarns: 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: From 24ad886b158a37cf9caf72a48eef1704361e1abf Mon Sep 17 00:00:00 2001 From: Olga Matoula Date: Sun, 16 May 2021 12:10:32 +0100 Subject: [PATCH 4/7] Remove the option to pass None in warns() --- src/_pytest/recwarn.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 715627209..4b61db496 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -84,7 +84,7 @@ def deprecated_call( @overload 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]]] = ..., ) -> "WarningsChecker": @@ -93,7 +93,7 @@ def warns( @overload def warns( - expected_warning: Optional[Union[Type[Warning], Tuple[Type[Warning], ...]]], + expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]], func: Callable[..., T], *args: Any, **kwargs: Any, @@ -102,9 +102,7 @@ def warns( def warns( - expected_warning: Optional[ - Union[Type[Warning], Tuple[Type[Warning], ...]] - ] = Warning, + expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]] = Warning, *args: Any, match: Optional[Union[str, Pattern[str]]] = None, **kwargs: Any, From 2414d23c7803b131d6c8016f5c34579148bc70e4 Mon Sep 17 00:00:00 2001 From: Olga Matoula Date: Sun, 16 May 2021 13:44:56 +0100 Subject: [PATCH 5/7] Remove default arg from overloaded warns --- src/_pytest/recwarn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 4b61db496..4feaa3bbb 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -84,7 +84,7 @@ def deprecated_call( @overload def warns( - expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]] = ..., + expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]], *, match: Optional[Union[str, Pattern[str]]] = ..., ) -> "WarningsChecker": From dd8ad3fa9c9862037e244f8460d0e726f7678779 Mon Sep 17 00:00:00 2001 From: Olga Matoula Date: Mon, 17 May 2021 09:23:08 +0100 Subject: [PATCH 6/7] Split warns matching string in multiple lines --- testing/deprecated_test.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 0974cf6c6..027a773b8 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -183,7 +183,10 @@ def test_hookproxy_warnings_for_fspath(tmp_path, hooktype, request): def test_warns_none_is_deprecated(): with pytest.warns( PytestDeprecationWarning, - match=r"Passing None to catch any warning has been deprecated, pass no arguments instead:\n Replace pytest.warns\(None\) by simply pytest.warns\(\).", + 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): pass From 3f414d7bbe82e6d80a7e54c4798d676c8207dc77 Mon Sep 17 00:00:00 2001 From: Olga Matoula Date: Mon, 17 May 2021 09:50:59 +0100 Subject: [PATCH 7/7] Ignore depredcated warns(None) overload errors from mypy --- src/_pytest/recwarn.py | 2 +- testing/deprecated_test.py | 2 +- testing/test_recwarn.py | 2 +- testing/test_tmpdir.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 4feaa3bbb..4b61db496 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -84,7 +84,7 @@ def deprecated_call( @overload def warns( - expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]], + expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]] = ..., *, match: Optional[Union[str, Pattern[str]]] = ..., ) -> "WarningsChecker": diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 027a773b8..479f1f26d 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -188,5 +188,5 @@ def test_warns_none_is_deprecated(): "Replace pytest.warns(None) by simply pytest.warns()." ), ): - with pytest.warns(None): + with pytest.warns(None): # type: ignore[call-overload] pass diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index 963f3aef2..40a2e23fa 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -310,7 +310,7 @@ class TestWarns: # 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: + with pytest.warns(None) as record: # type: ignore[call-overload] warnings.warn("user", UserWarning) warnings.warn("runtime", RuntimeWarning) diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index fe4b8b8cd..4dff9dff0 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -403,7 +403,7 @@ class TestRmRf: # ignored function with warnings.catch_warnings(): warnings.simplefilter("ignore") - with pytest.warns(None) as warninfo: + with pytest.warns(None) as warninfo: # type: ignore[call-overload] exc_info4 = (None, PermissionError(), None) on_rm_rf_error(os.open, str(fn), exc_info4, start_path=tmp_path) assert fn.is_file()