From 126896f69d065b46efbf55e6e2b30865ab4569c0 Mon Sep 17 00:00:00 2001 From: Virgil Dupras Date: Mon, 6 Aug 2018 16:17:24 -0400 Subject: [PATCH 01/62] Add option to disable plugin auto-loading If `PYTEST_DISABLE_PLUGIN_AUTOLOAD` is set, disable auto-loading of plugins through setuptools entrypoints. Only plugins that have been explicitly specified are loaded. ref #3784. --- AUTHORS | 1 + changelog/3784.feature.rst | 1 + doc/en/reference.rst | 5 +++++ src/_pytest/config/__init__.py | 9 ++++++++- src/_pytest/helpconfig.py | 1 + testing/test_config.py | 20 ++++++++++++++++++++ 6 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 changelog/3784.feature.rst diff --git a/AUTHORS b/AUTHORS index 49440194e5c..3b0448371e0 100644 --- a/AUTHORS +++ b/AUTHORS @@ -207,6 +207,7 @@ Tzu-ping Chung Vasily Kuznetsov Victor Uriarte Vidar T. Fauske +Virgil Dupras Vitaly Lashmanov Vlad Dragos Wil Cooley diff --git a/changelog/3784.feature.rst b/changelog/3784.feature.rst new file mode 100644 index 00000000000..87b6cafb6a1 --- /dev/null +++ b/changelog/3784.feature.rst @@ -0,0 +1 @@ +Add option to disable plugin auto-loading. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 86d92cf07bf..6e235e17634 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -866,6 +866,11 @@ Contains comma-separated list of modules that should be loaded as plugins: export PYTEST_PLUGINS=mymodule.plugin,xdist +PYTEST_DISABLE_PLUGIN_AUTOLOAD +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When set, disables plugin auto-loading through setuptools entrypoints. Only explicitly specified plugins will be +loaded. PYTEST_CURRENT_TEST ~~~~~~~~~~~~~~~~~~~ diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 421d124e9a9..3b2d622c357 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -701,6 +701,10 @@ def _mark_plugins_for_rewrite(self, hook): self.pluginmanager.rewrite_hook = hook + if os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"): + # We don't autoload from setuptools entry points, no need to continue. + return + # 'RECORD' available for plugins installed normally (pip install) # 'SOURCES.txt' available for plugins installed in dev mode (pip install -e) # for installed plugins 'SOURCES.txt' returns an empty list, and vice-versa @@ -726,7 +730,10 @@ def _preparse(self, args, addopts=True): self._checkversion() self._consider_importhook(args) self.pluginmanager.consider_preparse(args) - self.pluginmanager.load_setuptools_entrypoints("pytest11") + if not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"): + # Don't autoload from setuptools entry point. Only explicitly specified + # plugins are going to be loaded. + self.pluginmanager.load_setuptools_entrypoints("pytest11") self.pluginmanager.consider_env() self.known_args_namespace = ns = self._parser.parse_known_args( args, namespace=copy.copy(self.option) diff --git a/src/_pytest/helpconfig.py b/src/_pytest/helpconfig.py index 12c3339c647..85f071e9e3e 100644 --- a/src/_pytest/helpconfig.py +++ b/src/_pytest/helpconfig.py @@ -156,6 +156,7 @@ def showhelp(config): vars = [ ("PYTEST_ADDOPTS", "extra command line options"), ("PYTEST_PLUGINS", "comma-separated plugins to load during startup"), + ("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "set to disable plugin auto-loading"), ("PYTEST_DEBUG", "set to enable debug tracing of pytest's internals"), ] for name, help in vars: diff --git a/testing/test_config.py b/testing/test_config.py index b507bb8e823..d58bda255cf 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -605,6 +605,26 @@ def load(self): ) +@pytest.mark.parametrize( + "parse_args,should_load", [(("-p", "mytestplugin"), True), ((), False)] +) +def test_disable_plugin_autoload(testdir, monkeypatch, parse_args, should_load): + pkg_resources = pytest.importorskip("pkg_resources") + + def my_iter(name): + raise AssertionError("Should not be called") + + class PseudoPlugin(object): + x = 42 + + monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1") + monkeypatch.setattr(pkg_resources, "iter_entry_points", my_iter) + monkeypatch.setitem(sys.modules, "mytestplugin", PseudoPlugin()) + config = testdir.parseconfig(*parse_args) + has_loaded = config.pluginmanager.get_plugin("mytestplugin") is not None + assert has_loaded == should_load + + def test_cmdline_processargs_simple(testdir): testdir.makeconftest( """ From 539a22c750e2232ca6f0664976a4940e65c48ef5 Mon Sep 17 00:00:00 2001 From: wim glenn Date: Mon, 20 Aug 2018 01:10:44 -0500 Subject: [PATCH 02/62] Added support for xfailed and xpassed outcomes to the ``pytester.RunResult.assert_outcomes`` signature. --- AUTHORS | 1 + changelog/3837.feature.rst | 1 + src/_pytest/pytester.py | 18 +++++++++++--- testing/test_pytester.py | 51 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 changelog/3837.feature.rst diff --git a/AUTHORS b/AUTHORS index 9c3cb6a1266..4f7f3fc8b93 100644 --- a/AUTHORS +++ b/AUTHORS @@ -213,6 +213,7 @@ Vitaly Lashmanov Vlad Dragos Wil Cooley William Lee +Wim Glenn Wouter van Ackooy Xuan Luong Xuecong Liao diff --git a/changelog/3837.feature.rst b/changelog/3837.feature.rst new file mode 100644 index 00000000000..707c3b0da30 --- /dev/null +++ b/changelog/3837.feature.rst @@ -0,0 +1 @@ +Added support for 'xfailed' and 'xpassed' outcomes to the ``pytester.RunResult.assert_outcomes`` signature. diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 5b42b81eedf..58801be736f 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -406,7 +406,9 @@ def parseoutcomes(self): return d raise ValueError("Pytest terminal report not found") - def assert_outcomes(self, passed=0, skipped=0, failed=0, error=0): + def assert_outcomes( + self, passed=0, skipped=0, failed=0, error=0, xpassed=0, xfailed=0 + ): """Assert that the specified outcomes appear with the respective numbers (0 means it didn't occur) in the text output from a test run. @@ -417,10 +419,18 @@ def assert_outcomes(self, passed=0, skipped=0, failed=0, error=0): "skipped": d.get("skipped", 0), "failed": d.get("failed", 0), "error": d.get("error", 0), + "xpassed": d.get("xpassed", 0), + "xfailed": d.get("xfailed", 0), } - assert obtained == dict( - passed=passed, skipped=skipped, failed=failed, error=error - ) + expected = { + "passed": passed, + "skipped": skipped, + "failed": failed, + "error": error, + "xpassed": xpassed, + "xfailed": xfailed, + } + assert obtained == expected class CwdSnapshot(object): diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 86dc35796b3..8d612826271 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -83,6 +83,57 @@ def test_hello(testdir): result.assert_outcomes(passed=1) +def test_runresult_assertion_on_xfail(testdir): + testdir.makepyfile( + """ + import pytest + + pytest_plugins = "pytester" + + @pytest.mark.xfail + def test_potato(): + assert False + """ + ) + result = testdir.runpytest() + result.assert_outcomes(xfailed=1) + assert result.ret == 0 + + +def test_runresult_assertion_on_xpassed(testdir): + testdir.makepyfile( + """ + import pytest + + pytest_plugins = "pytester" + + @pytest.mark.xfail + def test_potato(): + assert True + """ + ) + result = testdir.runpytest() + result.assert_outcomes(xpassed=1) + assert result.ret == 0 + + +def test_xpassed_with_strict_is_considered_a_failure(testdir): + testdir.makepyfile( + """ + import pytest + + pytest_plugins = "pytester" + + @pytest.mark.xfail(strict=True) + def test_potato(): + assert True + """ + ) + result = testdir.runpytest() + result.assert_outcomes(failed=1) + assert result.ret != 0 + + def make_holder(): class apiclass(object): def pytest_xyz(self, arg): From 93f783228c3dfebd77a53e1d2e5fa81b49ae107a Mon Sep 17 00:00:00 2001 From: Jeffrey Rackauckas Date: Thu, 23 Aug 2018 22:56:25 -0700 Subject: [PATCH 03/62] Add the progress_display_mode ini option --- changelog/3829.feature.rst | 1 + src/_pytest/terminal.py | 22 ++++++++++++++++++---- testing/test_terminal.py | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 changelog/3829.feature.rst diff --git a/changelog/3829.feature.rst b/changelog/3829.feature.rst new file mode 100644 index 00000000000..3d9b6436593 --- /dev/null +++ b/changelog/3829.feature.rst @@ -0,0 +1 @@ +Added the `progress_display_mode` ini option to enable displaying the progress as a count instead of a percentage. \ No newline at end of file diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 7dd2edd6f9d..480a81f6e66 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -143,6 +143,12 @@ def pytest_addoption(parser): default="progress", ) + parser.addini( + "progress_display_mode", + help="Controls how to show the test progress (percentage|count)", + default="percentage", + ) + def pytest_configure(config): reporter = TerminalReporter(config, sys.stdout) @@ -426,10 +432,18 @@ def _get_progress_information_message(self): if self.config.getoption("capture") == "no": return "" collected = self._session.testscollected - if collected: - progress = len(self._progress_nodeids_reported) * 100 // collected - return " [{:3d}%]".format(progress) - return " [100%]" + if self.config.getini("progress_display_mode") == "count": + if collected: + progress = self._progress_nodeids_reported + counter_format = "{{:{}d}}".format(len(str(collected))) + format_string = "[ {} / {{}} ]".format(counter_format) + return format_string.format(len(progress), collected) + return " [ {} / {} ]".format(collected, collected) + else: + if collected: + progress = len(self._progress_nodeids_reported) * 100 // collected + return " [{:3d}%]".format(progress) + return " [100%]" def _write_progress_information_filling_space(self): msg = self._get_progress_information_message() diff --git a/testing/test_terminal.py b/testing/test_terminal.py index a9da27980c0..b102d1c339d 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -1143,6 +1143,21 @@ def test_normal(self, many_tests_files, testdir): ] ) + def test_count(self, many_tests_files, testdir): + testdir.makeini( + """ + [pytest] + progress_display_mode = count + """) + output = testdir.runpytest() + output.stdout.re_match_lines( + [ + r"test_bar.py \.{10} \s+ \[ 10 / 20 \]", + r"test_foo.py \.{5} \s+ \[ 15 / 20 \]", + r"test_foobar.py \.{5} \s+ \[ 20 / 20 \]", + ] + ) + def test_verbose(self, many_tests_files, testdir): output = testdir.runpytest("-v") output.stdout.re_match_lines( @@ -1153,11 +1168,31 @@ def test_verbose(self, many_tests_files, testdir): ] ) + def test_verbose_count(self, many_tests_files, testdir): + testdir.makeini( + """ + [pytest] + progress_display_mode = count + """) + output = testdir.runpytest("-v") + output.stdout.re_match_lines( + [ + r"test_bar.py::test_bar\[0\] PASSED \s+ \[ 1 / 20 \]", + r"test_foo.py::test_foo\[4\] PASSED \s+ \[ 15 / 20 \]", + r"test_foobar.py::test_foobar\[4\] PASSED \s+ \[ 20 / 20 \]", + ] + ) + def test_xdist_normal(self, many_tests_files, testdir): pytest.importorskip("xdist") output = testdir.runpytest("-n2") output.stdout.re_match_lines([r"\.{20} \s+ \[100%\]"]) + def test_xdist_normal(self, many_tests_files, testdir): + pytest.importorskip("xdist") + output = testdir.runpytest("-n2") + output.stdout.re_match_lines([r"\.{20} \s+ \[ 20 / 20 \]"]) + def test_xdist_verbose(self, many_tests_files, testdir): pytest.importorskip("xdist") output = testdir.runpytest("-n2", "-v") From 5fefc48f3332c7143d6f1ce5d4e70dc933100319 Mon Sep 17 00:00:00 2001 From: Jeffrey Rackauckas Date: Thu, 23 Aug 2018 23:00:02 -0700 Subject: [PATCH 04/62] Fixing pre-commit hooks --- testing/test_terminal.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index b102d1c339d..64d909772ec 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -1148,7 +1148,8 @@ def test_count(self, many_tests_files, testdir): """ [pytest] progress_display_mode = count - """) + """ + ) output = testdir.runpytest() output.stdout.re_match_lines( [ @@ -1173,7 +1174,8 @@ def test_verbose_count(self, many_tests_files, testdir): """ [pytest] progress_display_mode = count - """) + """ + ) output = testdir.runpytest("-v") output.stdout.re_match_lines( [ @@ -1188,7 +1190,7 @@ def test_xdist_normal(self, many_tests_files, testdir): output = testdir.runpytest("-n2") output.stdout.re_match_lines([r"\.{20} \s+ \[100%\]"]) - def test_xdist_normal(self, many_tests_files, testdir): + def test_xdist_normal_count(self, many_tests_files, testdir): pytest.importorskip("xdist") output = testdir.runpytest("-n2") output.stdout.re_match_lines([r"\.{20} \s+ \[ 20 / 20 \]"]) From 5e260c4d3442505c8b3cbe2fc9e8571f872e7310 Mon Sep 17 00:00:00 2001 From: Jeffrey Rackauckas Date: Sat, 25 Aug 2018 21:50:19 -0700 Subject: [PATCH 05/62] Fixing changelog file. --- changelog/3829.feature.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/3829.feature.rst b/changelog/3829.feature.rst index 3d9b6436593..06354e0c8fb 100644 --- a/changelog/3829.feature.rst +++ b/changelog/3829.feature.rst @@ -1 +1 @@ -Added the `progress_display_mode` ini option to enable displaying the progress as a count instead of a percentage. \ No newline at end of file +Added the `progress_display_mode` ini option to enable displaying the progress as a count instead of a percentage. From dda5e5ea328db9b80dbb098a925f2117c486010a Mon Sep 17 00:00:00 2001 From: Jeffrey Rackauckas Date: Sat, 25 Aug 2018 21:55:00 -0700 Subject: [PATCH 06/62] Fixing backticks in changelog file. --- changelog/3829.feature.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/3829.feature.rst b/changelog/3829.feature.rst index 06354e0c8fb..a6a24c47b8b 100644 --- a/changelog/3829.feature.rst +++ b/changelog/3829.feature.rst @@ -1 +1 @@ -Added the `progress_display_mode` ini option to enable displaying the progress as a count instead of a percentage. +Added the ``progress_display_mode`` ini option to enable displaying the progress as a count instead of a percentage. From 325319dc3b27febe63b6e2ea59bf6e4923207529 Mon Sep 17 00:00:00 2001 From: Jeffrey Rackauckas Date: Sat, 25 Aug 2018 22:18:29 -0700 Subject: [PATCH 07/62] Fixing xdist test to properly configure an ini file. --- testing/test_terminal.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 64d909772ec..c0240c58f8e 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -1188,10 +1188,17 @@ def test_verbose_count(self, many_tests_files, testdir): def test_xdist_normal(self, many_tests_files, testdir): pytest.importorskip("xdist") output = testdir.runpytest("-n2") + sys.stderr.write(output.stdout) output.stdout.re_match_lines([r"\.{20} \s+ \[100%\]"]) def test_xdist_normal_count(self, many_tests_files, testdir): pytest.importorskip("xdist") + testdir.makeini( + """ + [pytest] + progress_display_mode = count + """ + ) output = testdir.runpytest("-n2") output.stdout.re_match_lines([r"\.{20} \s+ \[ 20 / 20 \]"]) From 2a917a582ed21e679e310dc37b964439d2ee7bad Mon Sep 17 00:00:00 2001 From: Jeffrey Rackauckas Date: Sat, 25 Aug 2018 22:21:50 -0700 Subject: [PATCH 08/62] Removing accidental change to test --- testing/test_terminal.py | 1 - 1 file changed, 1 deletion(-) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index c0240c58f8e..5ab82444aa1 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -1188,7 +1188,6 @@ def test_verbose_count(self, many_tests_files, testdir): def test_xdist_normal(self, many_tests_files, testdir): pytest.importorskip("xdist") output = testdir.runpytest("-n2") - sys.stderr.write(output.stdout) output.stdout.re_match_lines([r"\.{20} \s+ \[100%\]"]) def test_xdist_normal_count(self, many_tests_files, testdir): From 8f4685e02479c051db62ee2cf00c074b5e7ce026 Mon Sep 17 00:00:00 2001 From: Jeffrey Rackauckas Date: Sun, 26 Aug 2018 19:21:00 -0700 Subject: [PATCH 09/62] Move count display style to be part of console_output_style, fixed test progress for count console output style. --- doc/en/reference.rst | 2 +- src/_pytest/terminal.py | 21 +++++++++++++++------ testing/test_terminal.py | 6 +++--- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index fe9e87042f9..65953e52f17 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -916,7 +916,7 @@ passed multiple times. The expected format is ``name=value``. For example:: * ``classic``: classic pytest output. * ``progress``: like classic pytest output, but with a progress indicator. - + * ``count``: like progress, but shows progress as the number of tests completed instead of a percent. The default is ``progress``, but you can fallback to ``classic`` if you prefer or the new mode is causing unexpected problems: diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 480a81f6e66..056ab01a8a4 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -260,7 +260,10 @@ def _determine_show_progress_info(self): # do not show progress if we are showing fixture setup/teardown if self.config.getoption("setupshow"): return False - return self.config.getini("console_output_style") == "progress" + return ( + self.config.getini("console_output_style") == "progress" + or self.config.getini("console_output_style") == "count" + ) def hasopt(self, char): char = {"xfailed": "x", "skipped": "s"}.get(char, char) @@ -410,6 +413,14 @@ def pytest_runtest_logreport(self, report): self.currentfspath = -2 def pytest_runtest_logfinish(self, nodeid): + if self.config.getini("console_output_style") == "count": + num_tests = self._session.testscollected + _PROGRESS_LENGTH = len( + " [ {} / {} ]".format(str(num_tests), str(num_tests)) + ) + else: + _PROGRESS_LENGTH = len(" [100%]") + if self.verbosity <= 0 and self._show_progress_info: self._progress_nodeids_reported.add(nodeid) last_item = ( @@ -419,24 +430,22 @@ def pytest_runtest_logfinish(self, nodeid): self._write_progress_information_filling_space() else: past_edge = ( - self._tw.chars_on_current_line + self._PROGRESS_LENGTH + 1 + self._tw.chars_on_current_line + _PROGRESS_LENGTH + 1 >= self._screen_width ) if past_edge: msg = self._get_progress_information_message() self._tw.write(msg + "\n", cyan=True) - _PROGRESS_LENGTH = len(" [100%]") - def _get_progress_information_message(self): if self.config.getoption("capture") == "no": return "" collected = self._session.testscollected - if self.config.getini("progress_display_mode") == "count": + if self.config.getini("console_output_style") == "count": if collected: progress = self._progress_nodeids_reported counter_format = "{{:{}d}}".format(len(str(collected))) - format_string = "[ {} / {{}} ]".format(counter_format) + format_string = " [ {} / {{}} ]".format(counter_format) return format_string.format(len(progress), collected) return " [ {} / {} ]".format(collected, collected) else: diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 5ab82444aa1..0ce357b6810 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -1147,7 +1147,7 @@ def test_count(self, many_tests_files, testdir): testdir.makeini( """ [pytest] - progress_display_mode = count + console_output_style = count """ ) output = testdir.runpytest() @@ -1173,7 +1173,7 @@ def test_verbose_count(self, many_tests_files, testdir): testdir.makeini( """ [pytest] - progress_display_mode = count + console_output_style = count """ ) output = testdir.runpytest("-v") @@ -1195,7 +1195,7 @@ def test_xdist_normal_count(self, many_tests_files, testdir): testdir.makeini( """ [pytest] - progress_display_mode = count + console_output_style = count """ ) output = testdir.runpytest("-n2") From 23295e1e987cdf5c926e94d75b55dada945d65be Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 27 Aug 2018 20:21:08 -0300 Subject: [PATCH 10/62] Fix docs linting --- doc/en/reference.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 65953e52f17..9b0c0bc71f3 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -917,6 +917,7 @@ passed multiple times. The expected format is ``name=value``. For example:: * ``classic``: classic pytest output. * ``progress``: like classic pytest output, but with a progress indicator. * ``count``: like progress, but shows progress as the number of tests completed instead of a percent. + The default is ``progress``, but you can fallback to ``classic`` if you prefer or the new mode is causing unexpected problems: From 4b94760c8e32400cce807eef132e40499e871a50 Mon Sep 17 00:00:00 2001 From: Jeffrey Rackauckas Date: Mon, 27 Aug 2018 20:23:17 -0700 Subject: [PATCH 11/62] Removed spacing in count display. --- changelog/3829.feature.rst | 2 +- src/_pytest/terminal.py | 21 +++++---------------- testing/test_terminal.py | 14 +++++++------- 3 files changed, 13 insertions(+), 24 deletions(-) diff --git a/changelog/3829.feature.rst b/changelog/3829.feature.rst index a6a24c47b8b..d3bfdb8e67e 100644 --- a/changelog/3829.feature.rst +++ b/changelog/3829.feature.rst @@ -1 +1 @@ -Added the ``progress_display_mode`` ini option to enable displaying the progress as a count instead of a percentage. +Added the ``count`` option to ``console_output_style`` to enable displaying the progress as a count instead of a percentage. diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 056ab01a8a4..1251b1479ed 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -143,12 +143,6 @@ def pytest_addoption(parser): default="progress", ) - parser.addini( - "progress_display_mode", - help="Controls how to show the test progress (percentage|count)", - default="percentage", - ) - def pytest_configure(config): reporter = TerminalReporter(config, sys.stdout) @@ -260,10 +254,7 @@ def _determine_show_progress_info(self): # do not show progress if we are showing fixture setup/teardown if self.config.getoption("setupshow"): return False - return ( - self.config.getini("console_output_style") == "progress" - or self.config.getini("console_output_style") == "count" - ) + return self.config.getini("console_output_style") in ("progress", "count") def hasopt(self, char): char = {"xfailed": "x", "skipped": "s"}.get(char, char) @@ -415,11 +406,9 @@ def pytest_runtest_logreport(self, report): def pytest_runtest_logfinish(self, nodeid): if self.config.getini("console_output_style") == "count": num_tests = self._session.testscollected - _PROGRESS_LENGTH = len( - " [ {} / {} ]".format(str(num_tests), str(num_tests)) - ) + progress_length = len(" [{}/{}]".format(str(num_tests), str(num_tests))) else: - _PROGRESS_LENGTH = len(" [100%]") + progress_length = len(" [100%]") if self.verbosity <= 0 and self._show_progress_info: self._progress_nodeids_reported.add(nodeid) @@ -430,7 +419,7 @@ def pytest_runtest_logfinish(self, nodeid): self._write_progress_information_filling_space() else: past_edge = ( - self._tw.chars_on_current_line + _PROGRESS_LENGTH + 1 + self._tw.chars_on_current_line + progress_length + 1 >= self._screen_width ) if past_edge: @@ -445,7 +434,7 @@ def _get_progress_information_message(self): if collected: progress = self._progress_nodeids_reported counter_format = "{{:{}d}}".format(len(str(collected))) - format_string = " [ {} / {{}} ]".format(counter_format) + format_string = " [{}/{{}}]".format(counter_format) return format_string.format(len(progress), collected) return " [ {} / {} ]".format(collected, collected) else: diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 0ce357b6810..1a1d20c95c0 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -1153,9 +1153,9 @@ def test_count(self, many_tests_files, testdir): output = testdir.runpytest() output.stdout.re_match_lines( [ - r"test_bar.py \.{10} \s+ \[ 10 / 20 \]", - r"test_foo.py \.{5} \s+ \[ 15 / 20 \]", - r"test_foobar.py \.{5} \s+ \[ 20 / 20 \]", + r"test_bar.py \.{10} \s+ \[10/20\]", + r"test_foo.py \.{5} \s+ \[15/20\]", + r"test_foobar.py \.{5} \s+ \[20/20\]", ] ) @@ -1179,9 +1179,9 @@ def test_verbose_count(self, many_tests_files, testdir): output = testdir.runpytest("-v") output.stdout.re_match_lines( [ - r"test_bar.py::test_bar\[0\] PASSED \s+ \[ 1 / 20 \]", - r"test_foo.py::test_foo\[4\] PASSED \s+ \[ 15 / 20 \]", - r"test_foobar.py::test_foobar\[4\] PASSED \s+ \[ 20 / 20 \]", + r"test_bar.py::test_bar\[0\] PASSED \s+ \[ 1/20\]", + r"test_foo.py::test_foo\[4\] PASSED \s+ \[15/20\]", + r"test_foobar.py::test_foobar\[4\] PASSED \s+ \[20/20\]", ] ) @@ -1199,7 +1199,7 @@ def test_xdist_normal_count(self, many_tests_files, testdir): """ ) output = testdir.runpytest("-n2") - output.stdout.re_match_lines([r"\.{20} \s+ \[ 20 / 20 \]"]) + output.stdout.re_match_lines([r"\.{20} \s+ \[20/20\]"]) def test_xdist_verbose(self, many_tests_files, testdir): pytest.importorskip("xdist") From 3035b2724db117264ce49a336b1aed5c201025ef Mon Sep 17 00:00:00 2001 From: dhirensr Date: Thu, 30 Aug 2018 16:01:42 +0530 Subject: [PATCH 12/62] T3853:removed needless message printed with --failed-first,--last-failed and no failed tests --- AUTHORS | 1 + src/_pytest/cacheprovider.py | 15 ++++++--------- testing/test_cacheprovider.py | 19 ++++++++++++------- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/AUTHORS b/AUTHORS index 6bf45b27b85..cb207eb7129 100644 --- a/AUTHORS +++ b/AUTHORS @@ -47,6 +47,7 @@ Christian Theunert Christian Tismer Christopher Gilling Cyrus Maden +Dhiren Serai Daniel Grana Daniel Hahler Daniel Nuri diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index dc72512b8a7..f601d9bec08 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -134,15 +134,12 @@ def __init__(self, config): def pytest_report_collectionfinish(self): if self.active: if not self._previously_failed_count: - mode = "run {} (no recorded failures)".format( - self._no_failures_behavior - ) - else: - noun = "failure" if self._previously_failed_count == 1 else "failures" - suffix = " first" if self.config.getoption("failedfirst") else "" - mode = "rerun previous {count} {noun}{suffix}".format( - count=self._previously_failed_count, suffix=suffix, noun=noun - ) + return None + noun = "failure" if self._previously_failed_count == 1 else "failures" + suffix = " first" if self.config.getoption("failedfirst") else "" + mode = "rerun previous {count} {noun}{suffix}".format( + count=self._previously_failed_count, suffix=suffix, noun=noun + ) return "run-last-failure: %s" % mode def pytest_runtest_logreport(self, report): diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index cfeb4a0cf39..ba3d6f87ada 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -414,13 +414,7 @@ def test_b2(): ) result = testdir.runpytest(test_a, "--lf") - result.stdout.fnmatch_lines( - [ - "collected 2 items", - "run-last-failure: run all (no recorded failures)", - "*2 passed in*", - ] - ) + result.stdout.fnmatch_lines(["collected 2 items", "*2 passed in*"]) result = testdir.runpytest(test_b, "--lf") result.stdout.fnmatch_lines( @@ -617,6 +611,17 @@ def test(): assert self.get_cached_last_failed(testdir) == [] assert result.ret == 0 + @pytest.mark.parametrize("quiet", [True, False]) + @pytest.mark.parametrize("opt", ["--ff", "--lf"]) + def test_lf_and_ff_prints_no_needless_message(self, quiet, opt, testdir): + # Issue 3853 + testdir.makepyfile("def test(): pass") + args = [opt] + if quiet: + args.append("-q") + result = testdir.runpytest(*args) + assert "run all" not in result.stdout.str() + def get_cached_last_failed(self, testdir): config = testdir.parseconfigure() return sorted(config.cache.get("cache/lastfailed", {})) From ade01b1f5b197bff47248bacc5325b0c05cf2ab2 Mon Sep 17 00:00:00 2001 From: dhirensr Date: Thu, 30 Aug 2018 16:03:18 +0530 Subject: [PATCH 13/62] T3853:Added changelog file --- changelog/3853.feature.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelog/3853.feature.rst diff --git a/changelog/3853.feature.rst b/changelog/3853.feature.rst new file mode 100644 index 00000000000..5bf739ba49b --- /dev/null +++ b/changelog/3853.feature.rst @@ -0,0 +1,2 @@ +Removed the Needless message printed with --failed-first,--last-failed and no failed tests. +Also added the test in test_cacheprovider.py file and removed 1 option from test which was explictily checking the needless message (run-last-failure: run all (no recorded failures)). From 0183d46275ca9e3c14fe523ac22c5e26e97ea98a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 30 Aug 2018 19:44:04 -0300 Subject: [PATCH 14/62] Improve CHANGELOG a bit --- changelog/3853.feature.rst | 2 -- changelog/3853.trivial.rst | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 changelog/3853.feature.rst create mode 100644 changelog/3853.trivial.rst diff --git a/changelog/3853.feature.rst b/changelog/3853.feature.rst deleted file mode 100644 index 5bf739ba49b..00000000000 --- a/changelog/3853.feature.rst +++ /dev/null @@ -1,2 +0,0 @@ -Removed the Needless message printed with --failed-first,--last-failed and no failed tests. -Also added the test in test_cacheprovider.py file and removed 1 option from test which was explictily checking the needless message (run-last-failure: run all (no recorded failures)). diff --git a/changelog/3853.trivial.rst b/changelog/3853.trivial.rst new file mode 100644 index 00000000000..fe8a4afb0da --- /dev/null +++ b/changelog/3853.trivial.rst @@ -0,0 +1,2 @@ +Removed ``"run all (no recorded failures)"`` message printed with ``--failed-first`` and ``--last-failed`` when there are no failed tests. + From 84a033fd97381301adcde2548d1ea169424f67ed Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 30 Aug 2018 19:48:47 -0300 Subject: [PATCH 15/62] Remove extra newline --- changelog/3853.trivial.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/changelog/3853.trivial.rst b/changelog/3853.trivial.rst index fe8a4afb0da..252d4604349 100644 --- a/changelog/3853.trivial.rst +++ b/changelog/3853.trivial.rst @@ -1,2 +1 @@ Removed ``"run all (no recorded failures)"`` message printed with ``--failed-first`` and ``--last-failed`` when there are no failed tests. - From 2256f2f04d9d073ca1c3e53deb6e6d6d6966c68d Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 31 Aug 2018 08:01:55 -0300 Subject: [PATCH 16/62] Remove test which is no longer required and improve test_lf_and_ff_prints_no_needless_message * test_lf_and_ff_obey_verbosity is no longer necessary because test_lf_and_ff_prints_no_needless_message already checks if the proper messages are displayed when -q is used. * Improve test_lf_and_ff_prints_no_needless_message so we also check that the correct message is displayed when there are failures to run --- testing/test_cacheprovider.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index 86bd65810d0..23ec7359906 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -553,19 +553,6 @@ def test_lastfailed_creates_cache_when_needed(self, testdir): testdir.runpytest("-q", "--lf") assert os.path.exists(".pytest_cache/v/cache/lastfailed") - @pytest.mark.parametrize("quiet", [True, False]) - @pytest.mark.parametrize("opt", ["--ff", "--lf"]) - def test_lf_and_ff_obey_verbosity(self, quiet, opt, testdir): - testdir.makepyfile("def test(): pass") - args = [opt] - if quiet: - args.append("-q") - result = testdir.runpytest(*args) - if quiet: - assert "run all" not in result.stdout.str() - else: - assert "run all" in result.stdout.str() - def test_xfail_not_considered_failure(self, testdir): testdir.makepyfile( """ @@ -628,13 +615,19 @@ def test(): @pytest.mark.parametrize("opt", ["--ff", "--lf"]) def test_lf_and_ff_prints_no_needless_message(self, quiet, opt, testdir): # Issue 3853 - testdir.makepyfile("def test(): pass") + testdir.makepyfile("def test(): assert 0") args = [opt] if quiet: args.append("-q") result = testdir.runpytest(*args) assert "run all" not in result.stdout.str() + result = testdir.runpytest(*args) + if quiet: + assert "run all" not in result.stdout.str() + else: + assert "rerun previous" in result.stdout.str() + def get_cached_last_failed(self, testdir): config = testdir.parseconfigure() return sorted(config.cache.get("cache/lastfailed", {})) From b01704cce13bbb65e60b809ccb9fd58b6a8821ee Mon Sep 17 00:00:00 2001 From: CrazyMerlyn Date: Sun, 2 Sep 2018 20:42:47 +0000 Subject: [PATCH 17/62] Fix exit code for command line errors Fixes #3913 --- AUTHORS | 1 + changelog/3913.bugfix.rst | 1 + src/_pytest/config/argparsing.py | 15 +++++++++++++++ testing/acceptance_test.py | 5 +++++ 4 files changed, 22 insertions(+) create mode 100644 changelog/3913.bugfix.rst diff --git a/AUTHORS b/AUTHORS index cb207eb7129..144eb19db23 100644 --- a/AUTHORS +++ b/AUTHORS @@ -46,6 +46,7 @@ Christian Boelsen Christian Theunert Christian Tismer Christopher Gilling +CrazyMerlyn Cyrus Maden Dhiren Serai Daniel Grana diff --git a/changelog/3913.bugfix.rst b/changelog/3913.bugfix.rst new file mode 100644 index 00000000000..33ed4ce28f0 --- /dev/null +++ b/changelog/3913.bugfix.rst @@ -0,0 +1 @@ +Pytest now returns with correct exit code (EXIT_USAGEERROR, 4) when called with unknown arguments. diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index 3a2a11af44a..784b2b21233 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -2,8 +2,13 @@ import warnings import argparse +from gettext import gettext as _ +import sys as _sys + import py +from ..main import EXIT_USAGEERROR + FILE_OR_DIR = "file_or_dir" @@ -329,6 +334,16 @@ def __init__(self, parser, extra_info=None): # an usage error to provide more contextual information to the user self.extra_info = extra_info + def error(self, message): + """error(message: string) + + Prints a usage message incorporating the message to stderr and + exits. + Overrides the method in parent class to change exit code""" + self.print_usage(_sys.stderr) + args = {"prog": self.prog, "message": message} + self.exit(EXIT_USAGEERROR, _("%(prog)s: error: %(message)s\n") % args) + def parse_args(self, args=None, namespace=None): """allow splitting of positional arguments""" args, argv = self.parse_known_args(args, namespace) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 428ac464c0b..8a9585be2c0 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1061,3 +1061,8 @@ def test_fixture_mock_integration(testdir): p = testdir.copy_example("acceptance/fixture_mock_integration.py") result = testdir.runpytest(p) result.stdout.fnmatch_lines("*1 passed*") + + +def test_usage_error_code(testdir): + result = testdir.runpytest("-unknown-option-") + assert result.ret == EXIT_USAGEERROR From 10f21b423a5676311974d0870af841ebe344d340 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 25 Aug 2018 20:31:18 -0300 Subject: [PATCH 18/62] Remove assert for "reprec" because this is no longer set on the pluginmanager It seems this has no effect since `pluggy` was developed as a separate library. --- src/_pytest/pytester.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 5c412047cff..f8824446849 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -525,7 +525,6 @@ def preserve_module(name): def make_hook_recorder(self, pluginmanager): """Create a new :py:class:`HookRecorder` for a PluginManager.""" - assert not hasattr(pluginmanager, "reprec") pluginmanager.reprec = reprec = HookRecorder(pluginmanager) self.request.addfinalizer(reprec.finish_recording) return reprec From ffd47ceefcda22ef178e14c2f90698417a75fe33 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 25 Aug 2018 20:41:16 -0300 Subject: [PATCH 19/62] Implement new pytest_warning_captured hook --- src/_pytest/hookspec.py | 19 ++++++++++++ src/_pytest/warnings.py | 62 ++++++++++++++++++++-------------------- testing/test_warnings.py | 19 ++++++++++++ 3 files changed, 69 insertions(+), 31 deletions(-) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index e2969110aeb..246a59d5965 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -535,6 +535,25 @@ def pytest_logwarning(message, code, nodeid, fslocation): """ +@hookspec(historic=True) +def pytest_warning_captured(warning, when, item): + """ + Process a warning captured by the internal pytest plugin. + + :param warnings.WarningMessage warning: + The captured warning. This is the same object produced by :py:func:`warnings.catch_warnings`, and contains + the same attributes as :py:func:`warnings.showwarning`. + + :param str when: + Indicates when the warning was captured. Possible values: + * ``"collect"``: during test collection. + * ``"runtest"``: during test execution. + + :param pytest.Item|None item: + The item being executed if ``when == "runtest"``, else ``None``. + """ + + # ------------------------------------------------------------------------- # doctest hooks # ------------------------------------------------------------------------- diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 3a93f92f3c4..7600840ea04 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -58,7 +58,7 @@ def pytest_configure(config): @contextmanager -def catch_warnings_for_item(item): +def deprecated_catch_warnings_for_item(item): """ catches the warnings generated during setup/call/teardown execution of the given item and after it is done posts them as warnings to this @@ -80,40 +80,40 @@ def catch_warnings_for_item(item): yield for warning in log: - warn_msg = warning.message - unicode_warning = False - - if compat._PY2 and any( - isinstance(m, compat.UNICODE_TYPES) for m in warn_msg.args - ): - new_args = [] - for m in warn_msg.args: - new_args.append( - compat.ascii_escaped(m) - if isinstance(m, compat.UNICODE_TYPES) - else m - ) - unicode_warning = list(warn_msg.args) != new_args - warn_msg.args = new_args - - msg = warnings.formatwarning( - warn_msg, - warning.category, - warning.filename, - warning.lineno, - warning.line, + item.ihook.pytest_warning_captured.call_historic( + kwargs=dict(warning=warning, when="runtest", item=item) ) - item.warn("unused", msg) + deprecated_emit_warning(item, warning) - if unicode_warning: - warnings.warn( - "Warning is using unicode non convertible to ascii, " - "converting to a safe representation:\n %s" % msg, - UnicodeWarning, - ) + +def deprecated_emit_warning(item, warning): + """ + Emits the deprecated ``pytest_logwarning`` for the given warning and item. + """ + warn_msg = warning.message + unicode_warning = False + if compat._PY2 and any(isinstance(m, compat.UNICODE_TYPES) for m in warn_msg.args): + new_args = [] + for m in warn_msg.args: + new_args.append( + compat.ascii_escaped(m) if isinstance(m, compat.UNICODE_TYPES) else m + ) + unicode_warning = list(warn_msg.args) != new_args + warn_msg.args = new_args + + msg = warnings.formatwarning( + warn_msg, warning.category, warning.filename, warning.lineno, warning.line + ) + item.warn("unused", msg) + if unicode_warning: + warnings.warn( + "Warning is using unicode non convertible to ascii, " + "converting to a safe representation:\n %s" % msg, + UnicodeWarning, + ) @pytest.hookimpl(hookwrapper=True) def pytest_runtest_protocol(item): - with catch_warnings_for_item(item): + with deprecated_catch_warnings_for_item(item): yield diff --git a/testing/test_warnings.py b/testing/test_warnings.py index a26fb459776..d0e5bbe4bd6 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -302,3 +302,22 @@ def test_func(): ) result = testdir.runpytest("--strict") assert result.ret == 0 + + +@pytest.mark.filterwarnings("always") +def test_warning_captured_hook(testdir, pyfile_with_warnings): + + collected = [] + + class WarningCollector: + def pytest_warning_captured(self, warning, when, item): + collected.append((warning.category, when, item.name)) + + result = testdir.runpytest(plugins=[WarningCollector()]) + result.stdout.fnmatch_lines(["*1 passed*"]) + + expected = [ + (UserWarning, "runtest", "test_func"), + (RuntimeWarning, "runtest", "test_func"), + ] + assert collected == expected From 3fcc4cdbd54f135c2031f019a7503cba90dd5dd9 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 25 Aug 2018 22:15:22 -0300 Subject: [PATCH 20/62] Make terminal capture pytest_warning_capture pytest_logwarning is no longer emitted by the warnings plugin, only ever emitted from .warn() functions in config and item --- src/_pytest/hookspec.py | 6 +++--- src/_pytest/nodes.py | 16 ++++++++++++--- src/_pytest/terminal.py | 42 +++++++++++++++++++++++++--------------- src/_pytest/warnings.py | 26 ++++++++++++++----------- testing/test_warnings.py | 4 ++-- 5 files changed, 59 insertions(+), 35 deletions(-) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 246a59d5965..001f59b86bd 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -536,13 +536,13 @@ def pytest_logwarning(message, code, nodeid, fslocation): @hookspec(historic=True) -def pytest_warning_captured(warning, when, item): +def pytest_warning_captured(warning_message, when, item): """ Process a warning captured by the internal pytest plugin. - :param warnings.WarningMessage warning: + :param warnings.WarningMessage warning_message: The captured warning. This is the same object produced by :py:func:`warnings.catch_warnings`, and contains - the same attributes as :py:func:`warnings.showwarning`. + the same attributes as the parameters of :py:func:`warnings.showwarning`. :param str when: Indicates when the warning was captured. Possible values: diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 49c30e90354..c8b0f64b752 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -138,9 +138,7 @@ def warn(self, code, message): """ generate a warning with the given code and message for this item. """ assert isinstance(code, str) - fslocation = getattr(self, "location", None) - if fslocation is None: - fslocation = getattr(self, "fspath", None) + fslocation = get_fslocation_from_item(self) self.ihook.pytest_logwarning.call_historic( kwargs=dict( code=code, message=message, nodeid=self.nodeid, fslocation=fslocation @@ -310,6 +308,18 @@ def _repr_failure_py(self, excinfo, style=None): repr_failure = _repr_failure_py +def get_fslocation_from_item(item): + """Tries to extract the actual location from an item, depending on available attributes: + + * "fslocation": a pair (path, lineno) + * "fspath": just a path + """ + fslocation = getattr(item, "location", None) + if fslocation is None: + fslocation = getattr(item, "fspath", None) + return fslocation + + class Collector(Node): """ Collector instances create children through collect() and thus iteratively build a tree. diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index cc83959fd84..14549ddf772 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -9,6 +9,7 @@ import sys import time +import attr import pluggy import py import six @@ -184,23 +185,20 @@ def pytest_report_teststatus(report): return report.outcome, letter, report.outcome.upper() +@attr.s class WarningReport(object): """ Simple structure to hold warnings information captured by ``pytest_logwarning``. + + :ivar str message: user friendly message about the warning + :ivar str|None nodeid: node id that generated the warning (see ``get_location``). + :ivar tuple|py.path.local fslocation: + file system location of the source of the warning (see ``get_location``). """ - def __init__(self, code, message, nodeid=None, fslocation=None): - """ - :param code: unused - :param str message: user friendly message about the warning - :param str|None nodeid: node id that generated the warning (see ``get_location``). - :param tuple|py.path.local fslocation: - file system location of the source of the warning (see ``get_location``). - """ - self.code = code - self.message = message - self.nodeid = nodeid - self.fslocation = fslocation + message = attr.ib() + nodeid = attr.ib(default=None) + fslocation = attr.ib(default=None) def get_location(self, config): """ @@ -327,13 +325,25 @@ def pytest_internalerror(self, excrepr): self.write_line("INTERNALERROR> " + line) return 1 - def pytest_logwarning(self, code, fslocation, message, nodeid): + def pytest_logwarning(self, fslocation, message, nodeid): warnings = self.stats.setdefault("warnings", []) - warning = WarningReport( - code=code, fslocation=fslocation, message=message, nodeid=nodeid - ) + warning = WarningReport(fslocation=fslocation, message=message, nodeid=nodeid) warnings.append(warning) + def pytest_warning_captured(self, warning_message, item): + from _pytest.nodes import get_fslocation_from_item + from _pytest.warnings import warning_record_to_str + + warnings = self.stats.setdefault("warnings", []) + + fslocation = get_fslocation_from_item(item) + message = warning_record_to_str(warning_message) + + warning_report = WarningReport( + fslocation=fslocation, message=message, nodeid=item.nodeid + ) + warnings.append(warning_report) + def pytest_plugin_registered(self, plugin): if self.config.option.traceconfig: msg = "PLUGIN registered: %s" % (plugin,) diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 7600840ea04..7c772f7c496 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -58,7 +58,7 @@ def pytest_configure(config): @contextmanager -def deprecated_catch_warnings_for_item(item): +def catch_warnings_for_item(item): """ catches the warnings generated during setup/call/teardown execution of the given item and after it is done posts them as warnings to this @@ -79,18 +79,18 @@ def deprecated_catch_warnings_for_item(item): yield - for warning in log: + for warning_message in log: item.ihook.pytest_warning_captured.call_historic( - kwargs=dict(warning=warning, when="runtest", item=item) + kwargs=dict(warning_message=warning_message, when="runtest", item=item) ) - deprecated_emit_warning(item, warning) -def deprecated_emit_warning(item, warning): - """ - Emits the deprecated ``pytest_logwarning`` for the given warning and item. +def warning_record_to_str(warning_message): + """Convert a warnings.WarningMessage to a string, taking in account a lot of unicode shenaningans in Python 2. + + When Python 2 support is tropped this function can be greatly simplified. """ - warn_msg = warning.message + warn_msg = warning_message.message unicode_warning = False if compat._PY2 and any(isinstance(m, compat.UNICODE_TYPES) for m in warn_msg.args): new_args = [] @@ -102,18 +102,22 @@ def deprecated_emit_warning(item, warning): warn_msg.args = new_args msg = warnings.formatwarning( - warn_msg, warning.category, warning.filename, warning.lineno, warning.line + warn_msg, + warning_message.category, + warning_message.filename, + warning_message.lineno, + warning_message.line, ) - item.warn("unused", msg) if unicode_warning: warnings.warn( "Warning is using unicode non convertible to ascii, " "converting to a safe representation:\n %s" % msg, UnicodeWarning, ) + return msg @pytest.hookimpl(hookwrapper=True) def pytest_runtest_protocol(item): - with deprecated_catch_warnings_for_item(item): + with catch_warnings_for_item(item): yield diff --git a/testing/test_warnings.py b/testing/test_warnings.py index d0e5bbe4bd6..efb974905f7 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -310,8 +310,8 @@ def test_warning_captured_hook(testdir, pyfile_with_warnings): collected = [] class WarningCollector: - def pytest_warning_captured(self, warning, when, item): - collected.append((warning.category, when, item.name)) + def pytest_warning_captured(self, warning_message, when, item): + collected.append((warning_message.category, when, item.name)) result = testdir.runpytest(plugins=[WarningCollector()]) result.stdout.fnmatch_lines(["*1 passed*"]) From 51e32cf7cc2906f24330081bc097cd80ba0acf14 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 26 Aug 2018 10:33:17 -0300 Subject: [PATCH 21/62] Remove Python 2.6 specific warning --- src/_pytest/python.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index f175394a8be..a4c3c32528d 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -349,11 +349,6 @@ def istestfunction(self, obj, name): if isinstance(obj, staticmethod): # static methods need to be unwrapped obj = safe_getattr(obj, "__func__", False) - if obj is False: - # Python 2.6 wraps in a different way that we won't try to handle - msg = "cannot collect static method %r because it is not a function" - self.warn(code="C2", message=msg % name) - return False return ( safe_getattr(obj, "__call__", False) and fixtures.getfixturemarker(obj) is None From 1a9d913ee1f9d1a56448f659649c96214f6d9645 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 26 Aug 2018 11:09:54 -0300 Subject: [PATCH 22/62] Capture and display warnings during collection Fix #3251 --- changelog/3251.feture.rst | 1 + src/_pytest/terminal.py | 6 ++++-- src/_pytest/warnings.py | 32 +++++++++++++++++++++----------- testing/test_warnings.py | 25 +++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 13 deletions(-) create mode 100644 changelog/3251.feture.rst diff --git a/changelog/3251.feture.rst b/changelog/3251.feture.rst new file mode 100644 index 00000000000..3ade3093dd1 --- /dev/null +++ b/changelog/3251.feture.rst @@ -0,0 +1 @@ +Warnings are now captured and displayed during test collection. diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 14549ddf772..41b0e755cd0 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -339,8 +339,9 @@ def pytest_warning_captured(self, warning_message, item): fslocation = get_fslocation_from_item(item) message = warning_record_to_str(warning_message) + nodeid = item.nodeid if item is not None else "" warning_report = WarningReport( - fslocation=fslocation, message=message, nodeid=item.nodeid + fslocation=fslocation, message=message, nodeid=nodeid ) warnings.append(warning_report) @@ -707,7 +708,8 @@ def summary_warnings(self): self.write_sep("=", "warnings summary", yellow=True, bold=False) for location, warning_records in grouped: - self._tw.line(str(location) if location else "") + if location: + self._tw.line(str(location)) for w in warning_records: lines = w.message.splitlines() indented = "\n".join(" " + x for x in lines) diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 7c772f7c496..6562d11a3fd 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -58,14 +58,16 @@ def pytest_configure(config): @contextmanager -def catch_warnings_for_item(item): +def catch_warnings_for_item(config, ihook, item): """ - catches the warnings generated during setup/call/teardown execution - of the given item and after it is done posts them as warnings to this - item. + Context manager that catches warnings generated in the contained execution block. + + ``item`` can be None if we are not in the context of an item execution. + + Each warning captured triggers the ``pytest_warning_captured`` hook. """ - args = item.config.getoption("pythonwarnings") or [] - inifilters = item.config.getini("filterwarnings") + args = config.getoption("pythonwarnings") or [] + inifilters = config.getini("filterwarnings") with warnings.catch_warnings(record=True) as log: for arg in args: warnings._setoption(arg) @@ -73,14 +75,15 @@ def catch_warnings_for_item(item): for arg in inifilters: _setoption(warnings, arg) - for mark in item.iter_markers(name="filterwarnings"): - for arg in mark.args: - warnings._setoption(arg) + if item is not None: + for mark in item.iter_markers(name="filterwarnings"): + for arg in mark.args: + warnings._setoption(arg) yield for warning_message in log: - item.ihook.pytest_warning_captured.call_historic( + ihook.pytest_warning_captured.call_historic( kwargs=dict(warning_message=warning_message, when="runtest", item=item) ) @@ -119,5 +122,12 @@ def warning_record_to_str(warning_message): @pytest.hookimpl(hookwrapper=True) def pytest_runtest_protocol(item): - with catch_warnings_for_item(item): + with catch_warnings_for_item(config=item.config, ihook=item.ihook, item=item): + yield + + +@pytest.hookimpl(hookwrapper=True) +def pytest_collection(session): + config = session.config + with catch_warnings_for_item(config=config, ihook=config.hook, item=None): yield diff --git a/testing/test_warnings.py b/testing/test_warnings.py index efb974905f7..3bd7bb52ec1 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -321,3 +321,28 @@ def pytest_warning_captured(self, warning_message, when, item): (RuntimeWarning, "runtest", "test_func"), ] assert collected == expected + + +@pytest.mark.filterwarnings("always") +def test_collection_warnings(testdir): + """ + """ + testdir.makepyfile( + """ + import warnings + + warnings.warn(UserWarning("collection warning")) + + def test_foo(): + pass + """ + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines( + [ + "*== %s ==*" % WARNINGS_SUMMARY_HEADER, + "*collection_warnings.py:3: UserWarning: collection warning", + ' warnings.warn(UserWarning("collection warning"))', + "* 1 passed, 1 warnings*", + ] + ) From 0100f61b62411621f8c5f886221bcbbe6f094a16 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 29 Aug 2018 17:53:51 -0300 Subject: [PATCH 23/62] Start the laywork to capture standard warnings --- src/_pytest/deprecated.py | 5 +---- src/_pytest/nodes.py | 21 +++++++++++++++++---- src/_pytest/python.py | 20 +++++++++++++------- src/_pytest/terminal.py | 7 +++---- src/_pytest/warning_types.py | 10 ++++++++++ testing/test_mark.py | 2 +- 6 files changed, 45 insertions(+), 20 deletions(-) create mode 100644 src/_pytest/warning_types.py diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 20f1cc25b45..237991c5646 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -7,10 +7,7 @@ """ from __future__ import absolute_import, division, print_function - -class RemovedInPytest4Warning(DeprecationWarning): - """warning class for features removed in pytest 4.0""" - +from _pytest.warning_types import RemovedInPytest4Warning MAIN_STR_ARGS = "passing a string to pytest.main() is deprecated, " "pass a list of arguments instead." diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index c8b0f64b752..098136df039 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -1,5 +1,6 @@ from __future__ import absolute_import, division, print_function import os +import warnings import six import py @@ -7,6 +8,7 @@ import _pytest import _pytest._code +from _pytest.compat import getfslineno from _pytest.mark.structures import NodeKeywords, MarkInfo @@ -145,6 +147,14 @@ def warn(self, code, message): ) ) + def std_warn(self, message, category=None): + from _pytest.warning_types import PytestWarning + + if category is None: + assert isinstance(message, PytestWarning) + path, lineno = get_fslocation_from_item(self) + warnings.warn_explicit(message, category, filename=str(path), lineno=lineno) + # methods for ordering nodes @property def nodeid(self): @@ -314,10 +324,13 @@ def get_fslocation_from_item(item): * "fslocation": a pair (path, lineno) * "fspath": just a path """ - fslocation = getattr(item, "location", None) - if fslocation is None: - fslocation = getattr(item, "fspath", None) - return fslocation + result = getattr(item, "location", None) + if result is not None: + return result + obj = getattr(item, "obj", None) + if obj is not None: + return getfslineno(obj) + return getattr(item, "fspath", None), None class Collector(Node): diff --git a/src/_pytest/python.py b/src/_pytest/python.py index a4c3c32528d..9de0dc0ec05 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -44,7 +44,7 @@ get_unpacked_marks, normalize_mark_list, ) - +from _pytest.warning_types import PytestUsageWarning # relative paths that we use to filter traceback entries from appearing to the user; # see filter_traceback @@ -656,17 +656,23 @@ def collect(self): if not safe_getattr(self.obj, "__test__", True): return [] if hasinit(self.obj): - self.warn( - "C1", + # self.warn( + # "C1", + # "cannot collect test class %r because it has a " + # "__init__ constructor" % self.obj.__name__, + # ) + self.std_warn( "cannot collect test class %r because it has a " "__init__ constructor" % self.obj.__name__, + PytestUsageWarning, ) return [] elif hasnew(self.obj): - self.warn( - "C1", - "cannot collect test class %r because it has a " - "__new__ constructor" % self.obj.__name__, + self.std_warn( + PytestUsageWarning( + "cannot collect test class %r because it has a " + "__new__ constructor" % self.obj.__name__ + ) ) return [] return [self._getcustomclass("Instance")(name="()", parent=self)] diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 41b0e755cd0..5140741a372 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -331,12 +331,11 @@ def pytest_logwarning(self, fslocation, message, nodeid): warnings.append(warning) def pytest_warning_captured(self, warning_message, item): - from _pytest.nodes import get_fslocation_from_item + # from _pytest.nodes import get_fslocation_from_item from _pytest.warnings import warning_record_to_str warnings = self.stats.setdefault("warnings", []) - - fslocation = get_fslocation_from_item(item) + fslocation = warning_message.filename, warning_message.lineno message = warning_record_to_str(warning_message) nodeid = item.nodeid if item is not None else "" @@ -713,7 +712,7 @@ def summary_warnings(self): for w in warning_records: lines = w.message.splitlines() indented = "\n".join(" " + x for x in lines) - self._tw.line(indented) + self._tw.line(indented.rstrip()) self._tw.line() self._tw.line("-- Docs: https://docs.pytest.org/en/latest/warnings.html") diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py new file mode 100644 index 00000000000..2b86dd28911 --- /dev/null +++ b/src/_pytest/warning_types.py @@ -0,0 +1,10 @@ +class PytestWarning(UserWarning): + """Base class for all warnings emitted by pytest""" + + +class PytestUsageWarning(PytestWarning): + """Warnings related to pytest usage: either command line or testing code.""" + + +class RemovedInPytest4Warning(PytestWarning): + """warning class for features that will be removed in pytest 4.0""" diff --git a/testing/test_mark.py b/testing/test_mark.py index e47981aca97..ae41fb1b8eb 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -16,7 +16,7 @@ from _pytest.nodes import Node ignore_markinfo = pytest.mark.filterwarnings( - "ignore:MarkInfo objects:_pytest.deprecated.RemovedInPytest4Warning" + "ignore:MarkInfo objects:_pytest.warning_types.RemovedInPytest4Warning" ) From 8e4501ee29771950f9a789fb66800590a7fa13a8 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 30 Aug 2018 21:09:39 -0300 Subject: [PATCH 24/62] Use std_warn for warning about applying marks directly to parameters --- src/_pytest/mark/structures.py | 11 +++++++---- src/_pytest/nodes.py | 2 +- src/_pytest/python.py | 6 +++++- testing/test_capture.py | 7 ++++--- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 9bd89c3c391..8700bd82dfb 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -65,7 +65,7 @@ def param_extract_id(id=None): return cls(values, marks, id_) @classmethod - def extract_from(cls, parameterset, legacy_force_tuple=False): + def extract_from(cls, parameterset, legacy_force_tuple=False, item=None): """ :param parameterset: a legacy style parameterset that may or may not be a tuple, @@ -75,6 +75,7 @@ def extract_from(cls, parameterset, legacy_force_tuple=False): enforce tuple wrapping so single argument tuple values don't get decomposed and break tests + :param item: the item that we will be extracting the parameters from. """ if isinstance(parameterset, cls): @@ -94,19 +95,21 @@ def extract_from(cls, parameterset, legacy_force_tuple=False): argval = (argval,) if newmarks: - warnings.warn(MARK_PARAMETERSET_UNPACKING) + item.std_warn(MARK_PARAMETERSET_UNPACKING) return cls(argval, marks=newmarks, id=None) @classmethod - def _for_parametrize(cls, argnames, argvalues, func, config): + def _for_parametrize(cls, argnames, argvalues, func, config, function_definition): if not isinstance(argnames, (tuple, list)): argnames = [x.strip() for x in argnames.split(",") if x.strip()] force_tuple = len(argnames) == 1 else: force_tuple = False parameters = [ - ParameterSet.extract_from(x, legacy_force_tuple=force_tuple) + ParameterSet.extract_from( + x, legacy_force_tuple=force_tuple, item=function_definition + ) for x in argvalues ] del argvalues diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 098136df039..e0291a0886d 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -326,7 +326,7 @@ def get_fslocation_from_item(item): """ result = getattr(item, "location", None) if result is not None: - return result + return result[:2] obj = getattr(item, "obj", None) if obj is not None: return getfslineno(obj) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 9de0dc0ec05..f20bd582f02 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -967,7 +967,11 @@ def parametrize(self, argnames, argvalues, indirect=False, ids=None, scope=None) from _pytest.mark import ParameterSet argnames, parameters = ParameterSet._for_parametrize( - argnames, argvalues, self.function, self.config + argnames, + argvalues, + self.function, + self.config, + function_definition=self.definition, ) del argvalues diff --git a/testing/test_capture.py b/testing/test_capture.py index 75d82ecde56..3dc422efe61 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -18,7 +18,9 @@ from _pytest.main import EXIT_NOTESTSCOLLECTED -needsosdup = pytest.mark.xfail("not hasattr(os, 'dup')") +needsosdup = pytest.mark.skipif( + not hasattr(os, "dup"), reason="test needs os.dup, not available on this platform" +) def tobytes(obj): @@ -61,9 +63,8 @@ def test_getmethod_default_no_fd(self, monkeypatch): pytest_addoption(parser) assert parser._groups[0].options[0].default == "sys" - @needsosdup @pytest.mark.parametrize( - "method", ["no", "sys", pytest.mark.skipif('not hasattr(os, "dup")', "fd")] + "method", ["no", "sys", pytest.param("fd", marks=needsosdup)] ) def test_capturing_basic_api(self, method): capouter = StdCaptureFD() From 0c8dbdcd92d0f1d4355d1929d0497bb22d598e6e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 1 Sep 2018 17:10:26 -0300 Subject: [PATCH 25/62] Fix existing tests now that we are using standard warnings --- src/_pytest/config/__init__.py | 9 +++++- src/_pytest/deprecated.py | 2 +- src/_pytest/mark/structures.py | 2 +- src/_pytest/python.py | 38 +++++++++++++------------ src/_pytest/warning_types.py | 2 +- src/_pytest/warnings.py | 9 +++++- testing/deprecated_test.py | 40 ++++++++++++++++++++------ testing/python/metafunc.py | 44 +++-------------------------- testing/python/test_deprecations.py | 22 --------------- testing/test_terminal.py | 9 +++--- 10 files changed, 80 insertions(+), 97 deletions(-) delete mode 100644 testing/python/test_deprecations.py diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 2c4361407e1..be412afd338 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -417,7 +417,14 @@ def _importconftest(self, conftestpath): PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST ) - warnings.warn(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST) + from _pytest.warning_types import RemovedInPytest4Warning + + warnings.warn_explicit( + PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST, + RemovedInPytest4Warning, + filename=str(conftestpath), + lineno=0, + ) except Exception: raise ConftestImportFailure(conftestpath, sys.exc_info()) diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 237991c5646..a77ebf6c80f 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -63,7 +63,7 @@ "Please use Metafunc.parametrize instead." ) -PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST = RemovedInPytest4Warning( +PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST = ( "Defining pytest_plugins in a non-top-level conftest is deprecated, " "because it affects the entire directory tree in a non-explicit way.\n" "Please move it to the top level conftest file instead." diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 8700bd82dfb..0e0ba96e5a9 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -94,7 +94,7 @@ def extract_from(cls, parameterset, legacy_force_tuple=False, item=None): if legacy_force_tuple: argval = (argval,) - if newmarks: + if newmarks and item is not None: item.std_warn(MARK_PARAMETERSET_UNPACKING) return cls(argval, marks=newmarks, id=None) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index f20bd582f02..4a7faff0762 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -44,7 +44,7 @@ get_unpacked_marks, normalize_mark_list, ) -from _pytest.warning_types import PytestUsageWarning +from _pytest.warning_types import PytestUsageWarning, RemovedInPytest4Warning # relative paths that we use to filter traceback entries from appearing to the user; # see filter_traceback @@ -982,7 +982,7 @@ def parametrize(self, argnames, argvalues, indirect=False, ids=None, scope=None) arg_values_types = self._resolve_arg_value_types(argnames, indirect) - ids = self._resolve_arg_ids(argnames, ids, parameters) + ids = self._resolve_arg_ids(argnames, ids, parameters, item=self.definition) scopenum = scope2index(scope, descr="call to {}".format(self.parametrize)) @@ -1005,13 +1005,14 @@ def parametrize(self, argnames, argvalues, indirect=False, ids=None, scope=None) newcalls.append(newcallspec) self._calls = newcalls - def _resolve_arg_ids(self, argnames, ids, parameters): + def _resolve_arg_ids(self, argnames, ids, parameters, item): """Resolves the actual ids for the given argnames, based on the ``ids`` parameter given to ``parametrize``. :param List[str] argnames: list of argument names passed to ``parametrize()``. :param ids: the ids parameter of the parametrized call (see docs). :param List[ParameterSet] parameters: the list of parameter values, same size as ``argnames``. + :param Item item: the item that generated this parametrized call. :rtype: List[str] :return: the list of ids for each argname given """ @@ -1032,7 +1033,7 @@ def _resolve_arg_ids(self, argnames, ids, parameters): raise ValueError( msg % (saferepr(id_value), type(id_value).__name__) ) - ids = idmaker(argnames, parameters, idfn, ids, self.config) + ids = idmaker(argnames, parameters, idfn, ids, self.config, item=item) return ids def _resolve_arg_value_types(self, argnames, indirect): @@ -1158,21 +1159,22 @@ def _find_parametrized_scope(argnames, arg2fixturedefs, indirect): return "function" -def _idval(val, argname, idx, idfn, config=None): +def _idval(val, argname, idx, idfn, config=None, item=None): if idfn: s = None try: s = idfn(val) - except Exception: + except Exception as e: # See issue https://github.com/pytest-dev/pytest/issues/2169 - import warnings - - msg = ( - "Raised while trying to determine id of parameter %s at position %d." - % (argname, idx) - ) - msg += "\nUpdate your code as this will raise an error in pytest-4.0." - warnings.warn(msg, DeprecationWarning) + if item is not None: + # should really be None only when unit-testing this function! + msg = ( + "While trying to determine id of parameter {} at position " + "{} the following exception was raised:\n".format(argname, idx) + ) + msg += " {}: {}\n".format(type(e).__name__, e) + msg += "This warning will be an error error in pytest-4.0." + item.std_warn(msg, RemovedInPytest4Warning) if s: return ascii_escaped(s) @@ -1196,12 +1198,12 @@ def _idval(val, argname, idx, idfn, config=None): return str(argname) + str(idx) -def _idvalset(idx, parameterset, argnames, idfn, ids, config=None): +def _idvalset(idx, parameterset, argnames, idfn, ids, config=None, item=None): if parameterset.id is not None: return parameterset.id if ids is None or (idx >= len(ids) or ids[idx] is None): this_id = [ - _idval(val, argname, idx, idfn, config) + _idval(val, argname, idx, idfn, config, item) for val, argname in zip(parameterset.values, argnames) ] return "-".join(this_id) @@ -1209,9 +1211,9 @@ def _idvalset(idx, parameterset, argnames, idfn, ids, config=None): return ascii_escaped(ids[idx]) -def idmaker(argnames, parametersets, idfn=None, ids=None, config=None): +def idmaker(argnames, parametersets, idfn=None, ids=None, config=None, item=None): ids = [ - _idvalset(valindex, parameterset, argnames, idfn, ids, config) + _idvalset(valindex, parameterset, argnames, idfn, ids, config, item) for valindex, parameterset in enumerate(parametersets) ] if len(set(ids)) != len(ids): diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index 2b86dd28911..be06f39c93f 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -6,5 +6,5 @@ class PytestUsageWarning(PytestWarning): """Warnings related to pytest usage: either command line or testing code.""" -class RemovedInPytest4Warning(PytestWarning): +class RemovedInPytest4Warning(PytestWarning, DeprecationWarning): """warning class for features that will be removed in pytest 4.0""" diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 6562d11a3fd..d043e64f7b3 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -91,7 +91,7 @@ def catch_warnings_for_item(config, ihook, item): def warning_record_to_str(warning_message): """Convert a warnings.WarningMessage to a string, taking in account a lot of unicode shenaningans in Python 2. - When Python 2 support is tropped this function can be greatly simplified. + When Python 2 support is dropped this function can be greatly simplified. """ warn_msg = warning_message.message unicode_warning = False @@ -131,3 +131,10 @@ def pytest_collection(session): config = session.config with catch_warnings_for_item(config=config, ihook=config.hook, item=None): yield + + +@pytest.hookimpl(hookwrapper=True) +def pytest_terminal_summary(terminalreporter): + config = terminalreporter.config + with catch_warnings_for_item(config=config, ihook=config.hook, item=None): + yield diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 966de66b205..15e66d71835 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -1,5 +1,6 @@ from __future__ import absolute_import, division, print_function +import os import pytest @@ -197,8 +198,11 @@ def test_func(): ) res = testdir.runpytest_subprocess() assert res.ret == 0 - res.stderr.fnmatch_lines( - "*" + str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0] + msg = PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST.splitlines()[0] + res.stdout.fnmatch_lines( + "*subdirectory{sep}conftest.py:0: RemovedInPytest4Warning: {msg}*".format( + sep=os.sep, msg=msg + ) ) @@ -227,8 +231,11 @@ def test_func(): res = testdir.runpytest_subprocess() assert res.ret == 0 - res.stderr.fnmatch_lines( - "*" + str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0] + msg = PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST.splitlines()[0] + res.stdout.fnmatch_lines( + "*subdirectory{sep}conftest.py:0: RemovedInPytest4Warning: {msg}*".format( + sep=os.sep, msg=msg + ) ) @@ -261,10 +268,8 @@ def test_func(): ) res = testdir.runpytest_subprocess() assert res.ret == 0 - assert ( - str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0] - not in res.stderr.str() - ) + msg = PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST.splitlines()[0] + assert msg not in res.stdout.str() def test_call_fixture_function_deprecated(): @@ -276,3 +281,22 @@ def fix(): with pytest.deprecated_call(): assert fix() == 1 + + +def test_pycollector_makeitem_is_deprecated(): + from _pytest.python import PyCollector + + class PyCollectorMock(PyCollector): + """evil hack""" + + def __init__(self): + self.called = False + + def _makeitem(self, *k): + """hack to disable the actual behaviour""" + self.called = True + + collector = PyCollectorMock() + with pytest.deprecated_call(): + collector.makeitem("foo", "bar") + assert collector.called diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index f5d839f086e..608cd52d450 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -383,44 +383,7 @@ def ids(val): ) assert result == ["a-a0", "a-a1", "a-a2"] - @pytest.mark.issue351 - def test_idmaker_idfn_exception(self): - from _pytest.python import idmaker - from _pytest.recwarn import WarningsRecorder - - class BadIdsException(Exception): - pass - - def ids(val): - raise BadIdsException("ids raised") - - rec = WarningsRecorder() - with rec: - idmaker( - ("a", "b"), - [ - pytest.param(10.0, IndexError()), - pytest.param(20, KeyError()), - pytest.param("three", [1, 2, 3]), - ], - idfn=ids, - ) - - assert [str(i.message) for i in rec.list] == [ - "Raised while trying to determine id of parameter a at position 0." - "\nUpdate your code as this will raise an error in pytest-4.0.", - "Raised while trying to determine id of parameter b at position 0." - "\nUpdate your code as this will raise an error in pytest-4.0.", - "Raised while trying to determine id of parameter a at position 1." - "\nUpdate your code as this will raise an error in pytest-4.0.", - "Raised while trying to determine id of parameter b at position 1." - "\nUpdate your code as this will raise an error in pytest-4.0.", - "Raised while trying to determine id of parameter a at position 2." - "\nUpdate your code as this will raise an error in pytest-4.0.", - "Raised while trying to determine id of parameter b at position 2." - "\nUpdate your code as this will raise an error in pytest-4.0.", - ] - + @pytest.mark.filterwarnings("default") def test_parametrize_ids_exception(self, testdir): """ :param testdir: the instance of Testdir class, a temporary @@ -438,13 +401,14 @@ def test_foo(arg): pass """ ) - with pytest.warns(DeprecationWarning): - result = testdir.runpytest("--collect-only") + result = testdir.runpytest("--collect-only") result.stdout.fnmatch_lines( [ "", " ", " ", + "*test_parametrize_ids_exception.py:5: *parameter arg at position 0*", + "*test_parametrize_ids_exception.py:5: *parameter arg at position 1*", ] ) diff --git a/testing/python/test_deprecations.py b/testing/python/test_deprecations.py deleted file mode 100644 index b0c11f0b03b..00000000000 --- a/testing/python/test_deprecations.py +++ /dev/null @@ -1,22 +0,0 @@ -import pytest - -from _pytest.python import PyCollector - - -class PyCollectorMock(PyCollector): - """evil hack""" - - def __init__(self): - self.called = False - - def _makeitem(self, *k): - """hack to disable the actual behaviour""" - self.called = True - - -def test_pycollector_makeitem_is_deprecated(): - - collector = PyCollectorMock() - with pytest.deprecated_call(): - collector.makeitem("foo", "bar") - assert collector.called diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 02e2824d97a..cca704c4cd5 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -1047,20 +1047,21 @@ def pytest_terminal_summary(terminalreporter, exitstatus): ) +@pytest.mark.filterwarnings("default") def test_terminal_summary_warnings_are_displayed(testdir): """Test that warnings emitted during pytest_terminal_summary are displayed. (#1305). """ testdir.makeconftest( """ + import warnings def pytest_terminal_summary(terminalreporter): - config = terminalreporter.config - config.warn('C1', 'internal warning') + warnings.warn(UserWarning('internal warning')) """ ) - result = testdir.runpytest("-rw") + result = testdir.runpytest() result.stdout.fnmatch_lines( - ["", "*internal warning", "*== 1 warnings in *"] + ["*conftest.py:3:*internal warning", "*== 1 warnings in *"] ) assert "None" not in result.stdout.str() From 78ac7d99f5d8fab0353078a9eccd334780a23e8d Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 1 Sep 2018 21:58:48 -0300 Subject: [PATCH 26/62] Deprecate Config.warn and Node.warn, replaced by standard warnings --- src/_pytest/assertion/rewrite.py | 21 ++++++++++++------ src/_pytest/cacheprovider.py | 10 ++++++--- src/_pytest/config/__init__.py | 26 +++++++++++++++++++--- src/_pytest/config/findpaths.py | 27 +++++++++++++++-------- src/_pytest/fixtures.py | 8 +++++-- src/_pytest/junitxml.py | 7 ++++-- src/_pytest/nodes.py | 34 +++++++++++++++++++++++++---- src/_pytest/python.py | 20 +++++++++++------ src/_pytest/resultlog.py | 4 +++- testing/acceptance_test.py | 2 +- testing/deprecated_test.py | 28 ++++++++++++++++++------ testing/python/collect.py | 14 +++++++++--- testing/python/metafunc.py | 4 ++-- testing/test_assertion.py | 3 ++- testing/test_assertrewrite.py | 12 ++++------- testing/test_cacheprovider.py | 1 + testing/test_config.py | 37 +++++++++++++++++++------------- testing/test_junitxml.py | 6 +++++- testing/test_resultlog.py | 3 +++ testing/test_warnings.py | 2 +- tox.ini | 3 +++ 21 files changed, 197 insertions(+), 75 deletions(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index a48a931ac1f..9c622213c67 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -209,8 +209,11 @@ def mark_rewrite(self, *names): self._must_rewrite.update(names) def _warn_already_imported(self, name): - self.config.warn( - "P1", "Module already imported so cannot be rewritten: %s" % name + import warnings + from _pytest.warning_types import PytestWarning + + warnings.warn( + "Module already imported so cannot be rewritten: %s" % name, PytestWarning ) def load_module(self, name): @@ -746,13 +749,17 @@ def visit_Assert(self, assert_): the expression is false. """ - if isinstance(assert_.test, ast.Tuple) and self.config is not None: - fslocation = (self.module_path, assert_.lineno) - self.config.warn( - "R1", + if isinstance(assert_.test, ast.Tuple): + from _pytest.warning_types import PytestWarning + import warnings + + warnings.warn_explicit( "assertion is always true, perhaps " "remove parentheses?", - fslocation=fslocation, + PytestWarning, + filename=str(self.module_path), + lineno=assert_.lineno, ) + self.statements = [] self.variables = [] self.variable_counter = itertools.count() diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 7cad246c86c..dbe95340672 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -33,7 +33,6 @@ @attr.s class Cache(object): _cachedir = attr.ib(repr=False) - _warn = attr.ib(repr=False) @classmethod def for_config(cls, config): @@ -41,14 +40,19 @@ def for_config(cls, config): if config.getoption("cacheclear") and cachedir.exists(): shutil.rmtree(str(cachedir)) cachedir.mkdir() - return cls(cachedir, config.warn) + return cls(cachedir) @staticmethod def cache_dir_from_config(config): return paths.resolve_from_str(config.getini("cache_dir"), config.rootdir) def warn(self, fmt, **args): - self._warn(code="I9", message=fmt.format(**args) if args else fmt) + import warnings + from _pytest.warning_types import PytestWarning + + warnings.warn( + message=fmt.format(**args) if args else fmt, category=PytestWarning + ) def makedir(self, name): """ return a directory path object with the given name. If the diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index be412afd338..f0aecbe556d 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -176,7 +176,9 @@ def _prepareconfig(args=None, plugins=None): else: pluginmanager.register(plugin) if warning: - config.warn("C1", warning) + from _pytest.warning_types import PytestUsageWarning + + warnings.warn(warning, PytestUsageWarning) return pluginmanager.hook.pytest_cmdline_parse( pluginmanager=pluginmanager, args=args ) @@ -609,7 +611,26 @@ def _ensure_unconfigure(self): fin() def warn(self, code, message, fslocation=None, nodeid=None): - """ generate a warning for this test session. """ + """ + .. deprecated:: 3.8 + + Use :py:func:`warnings.warn` or :py:func:`warnings.warn_explicit` directly instead. + + Generate a warning for this test session. + """ + from _pytest.warning_types import RemovedInPytest4Warning + + if isinstance(fslocation, (tuple, list)) and len(fslocation) > 2: + filename, lineno = fslocation[:2] + else: + filename = "unknown file" + lineno = 0 + msg = "config.warn has been deprecated, use warnings.warn instead" + if nodeid: + msg = "{}: {}".format(nodeid, msg) + warnings.warn_explicit( + msg, RemovedInPytest4Warning, filename=filename, lineno=lineno + ) self.hook.pytest_logwarning.call_historic( kwargs=dict( code=code, message=message, fslocation=fslocation, nodeid=nodeid @@ -674,7 +695,6 @@ def _initini(self, args): r = determine_setup( ns.inifilename, ns.file_or_dir + unknown_args, - warnfunc=self.warn, rootdir_cmd_arg=ns.rootdir or None, ) self.rootdir, self.inifile, self.inicfg = r diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index 234aa69c734..e10c455b1a6 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -10,7 +10,7 @@ def exists(path, ignore=EnvironmentError): return False -def getcfg(args, warnfunc=None): +def getcfg(args): """ Search the list of arguments for a valid ini-file for pytest, and return a tuple of (rootdir, inifile, cfg-dict). @@ -34,9 +34,13 @@ def getcfg(args, warnfunc=None): if exists(p): iniconfig = py.iniconfig.IniConfig(p) if "pytest" in iniconfig.sections: - if inibasename == "setup.cfg" and warnfunc: - warnfunc( - "C1", CFG_PYTEST_SECTION.format(filename=inibasename) + if inibasename == "setup.cfg": + import warnings + from _pytest.warning_types import RemovedInPytest4Warning + + warnings.warn( + CFG_PYTEST_SECTION.format(filename=inibasename), + RemovedInPytest4Warning, ) return base, p, iniconfig["pytest"] if ( @@ -95,7 +99,7 @@ def get_dir_from_path(path): return [get_dir_from_path(path) for path in possible_paths if path.exists()] -def determine_setup(inifile, args, warnfunc=None, rootdir_cmd_arg=None): +def determine_setup(inifile, args, rootdir_cmd_arg=None): dirs = get_dirs_from_args(args) if inifile: iniconfig = py.iniconfig.IniConfig(inifile) @@ -105,23 +109,28 @@ def determine_setup(inifile, args, warnfunc=None, rootdir_cmd_arg=None): for section in sections: try: inicfg = iniconfig[section] - if is_cfg_file and section == "pytest" and warnfunc: + if is_cfg_file and section == "pytest": + from _pytest.warning_types import RemovedInPytest4Warning from _pytest.deprecated import CFG_PYTEST_SECTION + import warnings - warnfunc("C1", CFG_PYTEST_SECTION.format(filename=str(inifile))) + warnings.warn( + CFG_PYTEST_SECTION.format(filename=str(inifile)), + RemovedInPytest4Warning, + ) break except KeyError: inicfg = None rootdir = get_common_ancestor(dirs) else: ancestor = get_common_ancestor(dirs) - rootdir, inifile, inicfg = getcfg([ancestor], warnfunc=warnfunc) + rootdir, inifile, inicfg = getcfg([ancestor]) if rootdir is None: for rootdir in ancestor.parts(reverse=True): if rootdir.join("setup.py").exists(): break else: - rootdir, inifile, inicfg = getcfg(dirs, warnfunc=warnfunc) + rootdir, inifile, inicfg = getcfg(dirs) if rootdir is None: rootdir = get_common_ancestor([py.path.local(), ancestor]) is_fs_root = os.path.splitdrive(str(rootdir))[1] == "/" diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index bfbf7bb546f..476acab0272 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1281,8 +1281,12 @@ def parsefactories(self, node_or_obj, nodeid=NOTSET, unittest=False): marker = defaultfuncargprefixmarker from _pytest import deprecated - self.config.warn( - "C1", deprecated.FUNCARG_PREFIX.format(name=name), nodeid=nodeid + filename, lineno = getfslineno(obj) + warnings.warn_explicit( + deprecated.FUNCARG_PREFIX.format(name=name), + RemovedInPytest4Warning, + filename=str(filename), + lineno=lineno + 1, ) name = name[len(self._argprefix) :] elif not isinstance(marker, FixtureFunctionMarker): diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 86aad69bb6c..2f34970a10b 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -274,8 +274,11 @@ def record_xml_attribute(request): The fixture is callable with ``(name, value)``, with value being automatically xml-encoded """ - request.node.warn( - code="C3", message="record_xml_attribute is an experimental feature" + from _pytest.warning_types import PytestWarning + + request.node.std_warn( + message="record_xml_attribute is an experimental feature", + category=PytestWarning, ) xml = getattr(request.config, "_xml", None) if xml is not None: diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index e0291a0886d..3bb10ee8997 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -137,8 +137,20 @@ def __repr__(self): return "<%s %r>" % (self.__class__.__name__, getattr(self, "name", None)) def warn(self, code, message): - """ generate a warning with the given code and message for this - item. """ + """ + .. deprecated:: 3.8 + + Use :meth:`Node.std_warn <_pytest.nodes.Node.std_warn>` instead. + + Generate a warning with the given code and message for this item. + """ + from _pytest.warning_types import RemovedInPytest4Warning + + self.std_warn( + "Node.warn has been deprecated, use Node.std_warn instead", + RemovedInPytest4Warning, + ) + assert isinstance(code, str) fslocation = get_fslocation_from_item(self) self.ihook.pytest_logwarning.call_historic( @@ -148,12 +160,24 @@ def warn(self, code, message): ) def std_warn(self, message, category=None): + """Issue a warning for this item. + + Warnings will be displayed after the test session, unless explicitly suppressed + + :param Union[str,Warning] message: text message of the warning or ``Warning`` instance. + :param Type[Warning] category: warning category. + """ from _pytest.warning_types import PytestWarning if category is None: assert isinstance(message, PytestWarning) path, lineno = get_fslocation_from_item(self) - warnings.warn_explicit(message, category, filename=str(path), lineno=lineno) + warnings.warn_explicit( + message, + category, + filename=str(path), + lineno=lineno + 1 if lineno is not None else None, + ) # methods for ordering nodes @property @@ -323,6 +347,8 @@ def get_fslocation_from_item(item): * "fslocation": a pair (path, lineno) * "fspath": just a path + + :rtype: a tuple of (str|LocalPath, int) with filename and line number. """ result = getattr(item, "location", None) if result is not None: @@ -330,7 +356,7 @@ def get_fslocation_from_item(item): obj = getattr(item, "obj", None) if obj is not None: return getfslineno(obj) - return getattr(item, "fspath", None), None + return getattr(item, "fspath", "unknown location"), -1 class Collector(Node): diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 4a7faff0762..5a531766b2d 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -44,7 +44,11 @@ get_unpacked_marks, normalize_mark_list, ) -from _pytest.warning_types import PytestUsageWarning, RemovedInPytest4Warning +from _pytest.warning_types import ( + PytestUsageWarning, + RemovedInPytest4Warning, + PytestWarning, +) # relative paths that we use to filter traceback entries from appearing to the user; # see filter_traceback @@ -239,9 +243,12 @@ def pytest_pycollect_makeitem(collector, name, obj): # or a funtools.wrapped. # We musn't if it's been wrapped with mock.patch (python 2 only) if not (isfunction(obj) or isfunction(get_real_func(obj))): - collector.warn( - code="C2", + filename, lineno = getfslineno(obj) + warnings.warn_explicit( message="cannot collect %r because it is not a function." % name, + category=PytestWarning, + filename=str(filename), + lineno=lineno + 1, ) elif getattr(obj, "__test__", True): if is_generator(obj): @@ -800,7 +807,7 @@ def collect(self): ) seen[name] = True values.append(self.Function(name, self, args=args, callobj=call)) - self.warn("C1", deprecated.YIELD_TESTS) + self.std_warn(deprecated.YIELD_TESTS, RemovedInPytest4Warning) return values def getcallargs(self, obj): @@ -1107,9 +1114,10 @@ def addcall(self, funcargs=None, id=NOTSET, param=NOTSET): invocation through the ``request.param`` attribute. """ if self.config: - self.config.warn( - "C1", message=deprecated.METAFUNC_ADD_CALL, fslocation=None + self.definition.std_warn( + deprecated.METAFUNC_ADD_CALL, RemovedInPytest4Warning ) + assert funcargs is None or isinstance(funcargs, dict) if funcargs is not None: for name in funcargs: diff --git a/src/_pytest/resultlog.py b/src/_pytest/resultlog.py index 0ad31b8bc80..308abd251af 100644 --- a/src/_pytest/resultlog.py +++ b/src/_pytest/resultlog.py @@ -31,8 +31,10 @@ def pytest_configure(config): config.pluginmanager.register(config._resultlog) from _pytest.deprecated import RESULT_LOG + import warnings + from _pytest.warning_types import RemovedInPytest4Warning - config.warn("C1", RESULT_LOG) + warnings.warn(RESULT_LOG, RemovedInPytest4Warning) def pytest_unconfigure(config): diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 428ac464c0b..6b374083f14 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -526,7 +526,7 @@ def test_equivalence_pytest_pytest(self): assert pytest.main == py.test.cmdline.main def test_invoke_with_string(self, capsys): - retcode = pytest.main("-h") + retcode = pytest.main(["-h"]) assert not retcode out, err = capsys.readouterr() assert "--help" in out diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 15e66d71835..70c6df63f39 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -5,6 +5,7 @@ import pytest +@pytest.mark.filterwarnings("default") def test_yield_tests_deprecation(testdir): testdir.makepyfile( """ @@ -18,16 +19,18 @@ def test_gen2(): yield func1, 1, 1 """ ) - result = testdir.runpytest("-ra") + result = testdir.runpytest() result.stdout.fnmatch_lines( [ - "*yield tests are deprecated, and scheduled to be removed in pytest 4.0*", + "*test_yield_tests_deprecation.py:3:*yield tests are deprecated*", + "*test_yield_tests_deprecation.py:6:*yield tests are deprecated*", "*2 passed*", ] ) assert result.stdout.str().count("yield tests are deprecated") == 2 +@pytest.mark.filterwarnings("default") def test_funcarg_prefix_deprecation(testdir): testdir.makepyfile( """ @@ -42,16 +45,18 @@ def test_funcarg_prefix(value): result.stdout.fnmatch_lines( [ ( - "*pytest_funcarg__value: " - 'declaring fixtures using "pytest_funcarg__" prefix is deprecated ' - "and scheduled to be removed in pytest 4.0. " - "Please remove the prefix and use the @pytest.fixture decorator instead." + "*test_funcarg_prefix_deprecation.py:1: *pytest_funcarg__value: " + 'declaring fixtures using "pytest_funcarg__" prefix is deprecated*' ), "*1 passed*", ] ) +@pytest.mark.filterwarnings("default") +@pytest.mark.xfail( + reason="#2891 need to handle warnings during pre-config", strict=True +) def test_pytest_setup_cfg_deprecated(testdir): testdir.makefile( ".cfg", @@ -66,6 +71,10 @@ def test_pytest_setup_cfg_deprecated(testdir): ) +@pytest.mark.filterwarnings("default") +@pytest.mark.xfail( + reason="#2891 need to handle warnings during pre-config", strict=True +) def test_pytest_custom_cfg_deprecated(testdir): testdir.makefile( ".cfg", @@ -80,6 +89,9 @@ def test_pytest_custom_cfg_deprecated(testdir): ) +@pytest.mark.xfail( + reason="#2891 need to handle warnings during pre-config", strict=True +) def test_str_args_deprecated(tmpdir, testdir): """Deprecate passing strings to pytest.main(). Scheduled for removal in pytest-4.0.""" from _pytest.main import EXIT_NOTESTSCOLLECTED @@ -103,6 +115,10 @@ def test_getfuncargvalue_is_deprecated(request): pytest.deprecated_call(request.getfuncargvalue, "tmpdir") +@pytest.mark.filterwarnings("default") +@pytest.mark.xfail( + reason="#2891 need to handle warnings during pre-config", strict=True +) def test_resultlog_is_deprecated(testdir): result = testdir.runpytest("--help") result.stdout.fnmatch_lines(["*DEPRECATED path for machine-readable result log*"]) diff --git a/testing/python/collect.py b/testing/python/collect.py index 8f4283e4072..b5475f03f55 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -462,6 +462,7 @@ def test_getmodulecollector(self, testdir): assert isinstance(modcol, pytest.Module) assert hasattr(modcol.obj, "test_func") + @pytest.mark.filterwarnings("default") def test_function_as_object_instance_ignored(self, testdir): testdir.makepyfile( """ @@ -472,8 +473,14 @@ def __call__(self, tmpdir): test_a = A() """ ) - reprec = testdir.inline_run() - reprec.assertoutcome() + result = testdir.runpytest() + result.stdout.fnmatch_lines( + [ + "collected 0 items", + "*test_function_as_object_instance_ignored.py:2: " + "*cannot collect 'test_a' because it is not a function.", + ] + ) def test_function_equality(self, testdir, tmpdir): from _pytest.fixtures import FixtureManager @@ -1468,6 +1475,7 @@ def check3(fix1, i, fix2): result.assertoutcome(passed=6, failed=2) +@pytest.mark.filterwarnings("default") def test_dont_collect_non_function_callable(testdir): """Test for issue https://github.com/pytest-dev/pytest/issues/331 @@ -1490,7 +1498,7 @@ def test_real(): result.stdout.fnmatch_lines( [ "*collected 1 item*", - "*cannot collect 'test_a' because it is not a function*", + "*test_dont_collect_non_function_callable.py:2: *cannot collect 'test_a' because it is not a function*", "*1 passed, 1 warnings in *", ] ) diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 608cd52d450..5d928243541 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -407,8 +407,8 @@ def test_foo(arg): "", " ", " ", - "*test_parametrize_ids_exception.py:5: *parameter arg at position 0*", - "*test_parametrize_ids_exception.py:5: *parameter arg at position 1*", + "*test_parametrize_ids_exception.py:6: *parameter arg at position 0*", + "*test_parametrize_ids_exception.py:6: *parameter arg at position 1*", ] ) diff --git a/testing/test_assertion.py b/testing/test_assertion.py index a9e6247139d..2c7f4b33d9c 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -1075,6 +1075,7 @@ def test_diff(): ) +@pytest.mark.filterwarnings("default") def test_assert_tuple_warning(testdir): testdir.makepyfile( """ @@ -1084,7 +1085,7 @@ def test_tuple(): ) result = testdir.runpytest("-rw") result.stdout.fnmatch_lines( - ["*test_assert_tuple_warning.py:2", "*assertion is always true*"] + ["*test_assert_tuple_warning.py:2:*assertion is always true*"] ) diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index c436ab0decd..fdbaa9e90a3 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -759,16 +759,12 @@ def test_package_without__init__py(self, testdir): testdir.makepyfile("import a_package_without_init_py.module") assert testdir.runpytest().ret == EXIT_NOTESTSCOLLECTED - def test_rewrite_warning(self, pytestconfig, monkeypatch): + def test_rewrite_warning(self, pytestconfig): hook = AssertionRewritingHook(pytestconfig) - warnings = [] - - def mywarn(code, msg): - warnings.append((code, msg)) + from _pytest.warning_types import PytestWarning - monkeypatch.setattr(hook.config, "warn", mywarn) - hook.mark_rewrite("_pytest") - assert "_pytest" in warnings[0][1] + with pytest.warns(PytestWarning): + hook.mark_rewrite("_pytest") def test_rewrite_module_imported_from_conftest(self, testdir): testdir.makeconftest( diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index 23ec7359906..c9d17422971 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -31,6 +31,7 @@ def test_config_cache_dataerror(self, testdir): val = config.cache.get("key/name", -2) assert val == -2 + @pytest.mark.filterwarnings("default") def test_cache_writefail_cachfile_silent(self, testdir): testdir.makeini("[pytest]") testdir.tmpdir.join(".pytest_cache").write("gone wrong") diff --git a/testing/test_config.py b/testing/test_config.py index ad7f35b574d..c0630d68830 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -135,13 +135,13 @@ def pytest_addoption(parser): """ ) testdir.makefile( - ".cfg", + ".ini", custom=""" [pytest] custom = 1 """, ) - config = testdir.parseconfig("-c", "custom.cfg") + config = testdir.parseconfig("-c", "custom.ini") assert config.getini("custom") == "1" testdir.makefile( @@ -155,8 +155,8 @@ def pytest_addoption(parser): assert config.getini("custom") == "1" def test_absolute_win32_path(self, testdir): - temp_cfg_file = testdir.makefile( - ".cfg", + temp_ini_file = testdir.makefile( + ".ini", custom=""" [pytest] addopts = --version @@ -164,8 +164,8 @@ def test_absolute_win32_path(self, testdir): ) from os.path import normpath - temp_cfg_file = normpath(str(temp_cfg_file)) - ret = pytest.main("-c " + temp_cfg_file) + temp_ini_file = normpath(str(temp_ini_file)) + ret = pytest.main(["-c", temp_ini_file]) assert ret == _pytest.main.EXIT_OK @@ -783,13 +783,14 @@ class pytest_something(object): assert pm.parse_hookimpl_opts(Dummy(), "pytest_something") is None -class TestWarning(object): +class TestLegacyWarning(object): + @pytest.mark.filterwarnings("default") def test_warn_config(self, testdir): testdir.makeconftest( """ values = [] - def pytest_configure(config): - config.warn("C1", "hello") + def pytest_runtest_setup(item): + item.config.warn("C1", "hello") def pytest_logwarning(code, message): if message == "hello" and code == "C1": values.append(1) @@ -802,9 +803,12 @@ def test_proper(pytestconfig): assert conftest.values == [1] """ ) - reprec = testdir.inline_run() - reprec.assertoutcome(passed=1) + result = testdir.runpytest() + result.stdout.fnmatch_lines( + ["*hello", "*config.warn has been deprecated*", "*1 passed*"] + ) + @pytest.mark.filterwarnings("default") def test_warn_on_test_item_from_request(self, testdir, request): testdir.makepyfile( """ @@ -819,7 +823,6 @@ def test_hello(fix): """ ) result = testdir.runpytest("--disable-pytest-warnings") - assert result.parseoutcomes()["warnings"] > 0 assert "hello" not in result.stdout.str() result = testdir.runpytest() @@ -828,6 +831,7 @@ def test_hello(fix): ===*warnings summary*=== *test_warn_on_test_item_from_request.py::test_hello* *hello* + *test_warn_on_test_item_from_request.py:7:*Node.warn has been deprecated, use Node.std_warn instead* """ ) @@ -847,7 +851,7 @@ def test_simple_noini(self, tmpdir): @pytest.mark.parametrize("name", "setup.cfg tox.ini pytest.ini".split()) def test_with_ini(self, tmpdir, name): inifile = tmpdir.join(name) - inifile.write("[pytest]\n") + inifile.write("[pytest]\n" if name != "setup.cfg" else "[tool:pytest]\n") a = tmpdir.mkdir("a") b = a.mkdir("b") @@ -893,11 +897,14 @@ def test_with_specific_inifile(self, tmpdir): class TestOverrideIniArgs(object): @pytest.mark.parametrize("name", "setup.cfg tox.ini pytest.ini".split()) def test_override_ini_names(self, testdir, name): + section = "[pytest]" if name != "setup.cfg" else "[tool:pytest]" testdir.tmpdir.join(name).write( textwrap.dedent( """ - [pytest] - custom = 1.0""" + {section} + custom = 1.0""".format( + section=section + ) ) ) testdir.makeconftest( diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 0678d59e80d..04b4ee2d71f 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -1005,6 +1005,7 @@ def test_record_with_same_name(record_property): pnodes[1].assert_attr(name="foo", value="baz") +@pytest.mark.filterwarnings("default") def test_record_attribute(testdir): testdir.makepyfile( """ @@ -1023,7 +1024,10 @@ def test_record(record_xml_attribute, other): tnode.assert_attr(bar="1") tnode.assert_attr(foo="<1") result.stdout.fnmatch_lines( - ["test_record_attribute.py::test_record", "*record_xml_attribute*experimental*"] + [ + "test_record_attribute.py::test_record", + "*test_record_attribute.py:6:*record_xml_attribute is an experimental feature", + ] ) diff --git a/testing/test_resultlog.py b/testing/test_resultlog.py index 173384ffb30..1bb0cca48cd 100644 --- a/testing/test_resultlog.py +++ b/testing/test_resultlog.py @@ -13,6 +13,9 @@ ) +pytestmark = pytest.mark.filterwarnings("ignore:--result-log is deprecated") + + def test_generic_path(testdir): from _pytest.main import Session diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 3bd7bb52ec1..eb7033f1d80 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -37,7 +37,7 @@ def test_func(): ) -@pytest.mark.filterwarnings("always") +@pytest.mark.filterwarnings("default") def test_normal_flow(testdir, pyfile_with_warnings): """ Check that the warnings section is displayed, containing test node ids followed by diff --git a/tox.ini b/tox.ini index fbc5d4779d6..d1e251e9943 100644 --- a/tox.ini +++ b/tox.ini @@ -218,6 +218,9 @@ norecursedirs = .tox ja .hg cx_freeze_source testing/example_scripts xfail_strict=true filterwarnings = error + ignore:yield tests are deprecated, and scheduled to be removed in pytest 4.0: + ignore:Metafunc.addcall is deprecated and scheduled to be removed in pytest 4.0: + ignore:Module already imported so cannot be rewritten: # produced by path.local ignore:bad escape.*:DeprecationWarning:re # produced by path.readlines From 19a01c9849978517b6213dd3d679fb23951c6cc8 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 3 Sep 2018 13:14:57 -0300 Subject: [PATCH 27/62] Make PytestWarning and RemovedInPytest4Warning part of the public API --- src/_pytest/config/__init__.py | 4 +-- src/_pytest/python.py | 10 ++---- src/_pytest/warning_types.py | 4 --- src/pytest.py | 56 ++++++++++++++++++---------------- testing/test_assertrewrite.py | 3 +- testing/test_mark.py | 2 +- tox.ini | 6 ++-- 7 files changed, 39 insertions(+), 46 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index f0aecbe556d..cec56e80085 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -176,9 +176,9 @@ def _prepareconfig(args=None, plugins=None): else: pluginmanager.register(plugin) if warning: - from _pytest.warning_types import PytestUsageWarning + from _pytest.warning_types import PytestWarning - warnings.warn(warning, PytestUsageWarning) + warnings.warn(warning, PytestWarning) return pluginmanager.hook.pytest_cmdline_parse( pluginmanager=pluginmanager, args=args ) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 5a531766b2d..9ac21633251 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -44,11 +44,7 @@ get_unpacked_marks, normalize_mark_list, ) -from _pytest.warning_types import ( - PytestUsageWarning, - RemovedInPytest4Warning, - PytestWarning, -) +from _pytest.warning_types import RemovedInPytest4Warning, PytestWarning # relative paths that we use to filter traceback entries from appearing to the user; # see filter_traceback @@ -671,12 +667,12 @@ def collect(self): self.std_warn( "cannot collect test class %r because it has a " "__init__ constructor" % self.obj.__name__, - PytestUsageWarning, + PytestWarning, ) return [] elif hasnew(self.obj): self.std_warn( - PytestUsageWarning( + PytestWarning( "cannot collect test class %r because it has a " "__new__ constructor" % self.obj.__name__ ) diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index be06f39c93f..a98732ee354 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -2,9 +2,5 @@ class PytestWarning(UserWarning): """Base class for all warnings emitted by pytest""" -class PytestUsageWarning(PytestWarning): - """Warnings related to pytest usage: either command line or testing code.""" - - class RemovedInPytest4Warning(PytestWarning, DeprecationWarning): """warning class for features that will be removed in pytest 4.0""" diff --git a/src/pytest.py b/src/pytest.py index ae542b76d94..bf6a9416fca 100644 --- a/src/pytest.py +++ b/src/pytest.py @@ -19,45 +19,47 @@ from _pytest.nodes import Item, Collector, File from _pytest.fixtures import fillfixtures as _fillfuncargs from _pytest.python import Package, Module, Class, Instance, Function, Generator - from _pytest.python_api import approx, raises +from _pytest.warning_types import PytestWarning, RemovedInPytest4Warning set_trace = __pytestPDB.set_trace __all__ = [ - "main", - "UsageError", - "cmdline", - "hookspec", - "hookimpl", "__version__", - "register_assert_rewrite", - "freeze_includes", - "set_trace", - "warns", + "_fillfuncargs", + "approx", + "Class", + "cmdline", + "Collector", "deprecated_call", - "fixture", - "yield_fixture", - "fail", - "skip", - "xfail", - "importorskip", "exit", - "mark", - "param", - "approx", - "_fillfuncargs", - "Item", + "fail", "File", - "Collector", - "Package", - "Session", - "Module", - "Class", - "Instance", + "fixture", + "freeze_includes", "Function", "Generator", + "hookimpl", + "hookspec", + "importorskip", + "Instance", + "Item", + "main", + "mark", + "Module", + "Package", + "param", + "PytestWarning", "raises", + "register_assert_rewrite", + "RemovedInPytest4Warning", + "Session", + "set_trace", + "skip", + "UsageError", + "warns", + "xfail", + "yield_fixture", ] if __name__ == "__main__": diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index fdbaa9e90a3..c82e1dccfd7 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -761,9 +761,8 @@ def test_package_without__init__py(self, testdir): def test_rewrite_warning(self, pytestconfig): hook = AssertionRewritingHook(pytestconfig) - from _pytest.warning_types import PytestWarning - with pytest.warns(PytestWarning): + with pytest.warns(pytest.PytestWarning): hook.mark_rewrite("_pytest") def test_rewrite_module_imported_from_conftest(self, testdir): diff --git a/testing/test_mark.py b/testing/test_mark.py index ae41fb1b8eb..12aded41683 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -16,7 +16,7 @@ from _pytest.nodes import Node ignore_markinfo = pytest.mark.filterwarnings( - "ignore:MarkInfo objects:_pytest.warning_types.RemovedInPytest4Warning" + "ignore:MarkInfo objects:pytest.RemovedInPytest4Warning" ) diff --git a/tox.ini b/tox.ini index d1e251e9943..4b5eae06640 100644 --- a/tox.ini +++ b/tox.ini @@ -218,9 +218,9 @@ norecursedirs = .tox ja .hg cx_freeze_source testing/example_scripts xfail_strict=true filterwarnings = error - ignore:yield tests are deprecated, and scheduled to be removed in pytest 4.0: - ignore:Metafunc.addcall is deprecated and scheduled to be removed in pytest 4.0: - ignore:Module already imported so cannot be rewritten: + ignore:yield tests are deprecated, and scheduled to be removed in pytest 4.0:pytest.RemovedInPytest4Warning + ignore:Metafunc.addcall is deprecated and scheduled to be removed in pytest 4.0:pytest.RemovedInPytest4Warning + ignore:Module already imported so cannot be rewritten:pytest.PytestWarning # produced by path.local ignore:bad escape.*:DeprecationWarning:re # produced by path.readlines From 208dd3aad1a094b8066d7ba374700035afde27ce Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 3 Sep 2018 13:36:12 -0300 Subject: [PATCH 28/62] Add docs for internal warnings and introduce PytestDeprecationWarning Fix #2477 --- doc/en/warnings.rst | 49 ++++++++++++++++++++++++++++++++++++ src/_pytest/warning_types.py | 22 +++++++++++++--- src/pytest.py | 7 +++++- 3 files changed, 74 insertions(+), 4 deletions(-) diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index d1c927dd067..a435324b62a 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -296,3 +296,52 @@ You can also use it as a contextmanager:: def test_global(): with pytest.deprecated_call(): myobject.deprecated_method() + + +Internal pytest warnings +------------------------ + +.. versionadded:: 3.8 + +pytest may generate its own warnings in some situations, such as improper usage or deprecated features. + +For example, pytest will emit a warning if it encounters a class that matches :confval:`python_classes` but also +defines an ``__init__`` constructor, as this prevents the class from being instantiated: + +.. code-block:: python + + # content of test_pytest_warnings.py + class Test: + def __init__(self): + pass + + def test_foo(self): + assert 1 == 1 + +:: + + $ pytest test_pytest_warnings.py -q + ======================================== warnings summary ========================================= + test_pytest_warnings.py:1 + $REGENDOC_TMPDIR/test_pytest_warnings.py:1: PytestWarning: cannot collect test class 'Test' because it has a __init__ constructor + class Test: + + -- Docs: http://doc.pytest.org/en/latest/warnings.html + 1 warnings in 0.01 seconds + + + +These warnings might be filtered using the same builtin mechanisms used to filter other types of warnings. + +Following our :ref:`backwards-compatibility`, deprecated features will be kept *at least* two minor releases. After that, +they will changed so they by default raise errors instead of just warnings, so users can adapt to it on their own time +if not having done so until now. In a later release the deprecated feature will be removed completely. + +The following warning types ares used by pytest and are part of the public API: + +.. autoclass:: pytest.PytestWarning + +.. autoclass:: pytest.PytestDeprecationWarning + +.. autoclass:: pytest.RemovedInPytest4Warning + diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index a98732ee354..092a5a430d6 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -1,6 +1,22 @@ class PytestWarning(UserWarning): - """Base class for all warnings emitted by pytest""" + """ + Bases: :class:`UserWarning`. + Base class for all warnings emitted by pytest. + """ -class RemovedInPytest4Warning(PytestWarning, DeprecationWarning): - """warning class for features that will be removed in pytest 4.0""" + +class PytestDeprecationWarning(PytestWarning, DeprecationWarning): + """ + Bases: :class:`pytest.PytestWarning`, :class:`DeprecationWarning`. + + Warning class for features that will be removed in a future version. + """ + + +class RemovedInPytest4Warning(PytestDeprecationWarning): + """ + Bases: :class:`pytest.PytestDeprecationWarning`. + + Warning class for features scheduled to be removed in pytest 4.0. + """ diff --git a/src/pytest.py b/src/pytest.py index bf6a9416fca..c5a0666627b 100644 --- a/src/pytest.py +++ b/src/pytest.py @@ -20,7 +20,11 @@ from _pytest.fixtures import fillfixtures as _fillfuncargs from _pytest.python import Package, Module, Class, Instance, Function, Generator from _pytest.python_api import approx, raises -from _pytest.warning_types import PytestWarning, RemovedInPytest4Warning +from _pytest.warning_types import ( + PytestWarning, + PytestDeprecationWarning, + RemovedInPytest4Warning, +) set_trace = __pytestPDB.set_trace @@ -50,6 +54,7 @@ "Package", "param", "PytestWarning", + "PytestDeprecationWarning", "raises", "register_assert_rewrite", "RemovedInPytest4Warning", From 7e135934528732c6628c4ba83fa12ed00b951889 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 3 Sep 2018 14:15:23 -0300 Subject: [PATCH 29/62] Add CHANGELOG entries for #2452 Fix #2452 Fix #2684 --- changelog/2452.feature.rst | 5 +++++ changelog/2452.removal.rst | 2 ++ 2 files changed, 7 insertions(+) create mode 100644 changelog/2452.feature.rst create mode 100644 changelog/2452.removal.rst diff --git a/changelog/2452.feature.rst b/changelog/2452.feature.rst new file mode 100644 index 00000000000..847e9540f72 --- /dev/null +++ b/changelog/2452.feature.rst @@ -0,0 +1,5 @@ +Internal pytest warnings are now issued using the standard ``warnings`` module, making it possible to use +the standard warnings filters to manage those warnings. This introduces ``PytestWarning``, +``PytestDeprecationWarning`` and ``RemovedInPytest4Warning`` warning types as part of the public API. + +Consult `the documentation `_ for more info. diff --git a/changelog/2452.removal.rst b/changelog/2452.removal.rst new file mode 100644 index 00000000000..3c60f8803c6 --- /dev/null +++ b/changelog/2452.removal.rst @@ -0,0 +1,2 @@ +The functions ``Node.warn`` and ``Config.warn`` have been deprecated. Instead of ``Node.warn`` users should now use +``Node.std_warn``, while ``Config.warn`` should be replaced by the standard ``warnings.warn``. From 9965ed84da81130681ad8d56085c25110e5dda78 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 3 Sep 2018 17:52:44 -0300 Subject: [PATCH 30/62] Show deprecation warnings by default if no other filters are configured Fix #2908 --- changelog/2908.feature.rst | 3 ++ doc/en/warnings.rst | 28 +++++++++++++-- src/_pytest/warnings.py | 8 +++++ testing/python/collect.py | 43 +++++++++++----------- testing/test_warnings.py | 73 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 130 insertions(+), 25 deletions(-) create mode 100644 changelog/2908.feature.rst diff --git a/changelog/2908.feature.rst b/changelog/2908.feature.rst new file mode 100644 index 00000000000..957fc30f05a --- /dev/null +++ b/changelog/2908.feature.rst @@ -0,0 +1,3 @@ +``DeprecationWarning`` and ``PendingDeprecationWarning`` are now shown by default if no other warning filter is +configured. This makes pytest compliant with +`PEP-0506 `_. diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index a435324b62a..ed73a69e89e 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -36,8 +36,6 @@ Running pytest now produces this output:: -- Docs: https://docs.pytest.org/en/latest/warnings.html =================== 1 passed, 1 warnings in 0.12 seconds =================== -Pytest by default catches all warnings except for ``DeprecationWarning`` and ``PendingDeprecationWarning``. - The ``-W`` flag can be passed to control which warnings will be displayed or even turn them into errors:: @@ -78,6 +76,32 @@ Both ``-W`` command-line option and ``filterwarnings`` ini option are based on P `-W option`_ and `warnings.simplefilter`_, so please refer to those sections in the Python documentation for other examples and advanced usage. +Disabling warning summary +------------------------- + +Although not recommended, you can use the ``--disable-warnings`` command-line option to suppress the +warning summary entirely from the test run output. + + +DeprecationWarning and PendingDeprecationWarning +------------------------------------------------ + +.. versionadded:: 3.8 + +By default pytest will display ``DeprecationWarning`` and ``PendingDeprecationWarning`` if no other warning filters +are configured. This complies with `PEP-0506 `_ which suggests that those warnings should +be shown by default by test runners. + +To disable this behavior, you might define any warnings filter either in the command-line or in the ini file, but +if you don't have any other warnings to filter you can use: + +.. code-block:: ini + + [pytest] + filterwarnings = + ignore::DeprecationWarning + ignore::PendingDeprecationWarning + .. _`filterwarnings`: diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index d043e64f7b3..952c4a0be15 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -1,5 +1,6 @@ from __future__ import absolute_import, division, print_function +import sys import warnings from contextlib import contextmanager @@ -69,6 +70,8 @@ def catch_warnings_for_item(config, ihook, item): args = config.getoption("pythonwarnings") or [] inifilters = config.getini("filterwarnings") with warnings.catch_warnings(record=True) as log: + filters_configured = args or inifilters or sys.warnoptions + for arg in args: warnings._setoption(arg) @@ -79,6 +82,11 @@ def catch_warnings_for_item(config, ihook, item): for mark in item.iter_markers(name="filterwarnings"): for arg in mark.args: warnings._setoption(arg) + filters_configured = True + + if not filters_configured: + warnings.filterwarnings("always", category=DeprecationWarning) + warnings.filterwarnings("always", category=PendingDeprecationWarning) yield diff --git a/testing/python/collect.py b/testing/python/collect.py index b5475f03f55..c92de12a09c 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -8,10 +8,6 @@ from _pytest.main import EXIT_NOTESTSCOLLECTED from _pytest.nodes import Collector -ignore_parametrized_marks = pytest.mark.filterwarnings( - "ignore:Applying marks directly to parameters" -) - class TestModule(object): def test_failing_import(self, testdir): @@ -456,6 +452,13 @@ def test_setuplist(): class TestFunction(object): + @pytest.fixture + def ignore_parametrized_marks_args(self): + """Provides arguments to pytester.runpytest() to ignore the warning about marks being applied directly + to parameters. + """ + return ("-W", "ignore:Applying marks directly to parameters") + def test_getmodulecollector(self, testdir): item = testdir.getitem("def test_func(): pass") modcol = item.getparent(pytest.Module) @@ -669,7 +672,7 @@ def test_it(fix1): rec = testdir.inline_run() rec.assertoutcome(passed=1) - @ignore_parametrized_marks + @pytest.mark.filterwarnings("ignore:Applying marks directly to parameters") def test_parametrize_with_mark(self, testdir): items = testdir.getitems( """ @@ -755,8 +758,7 @@ def test2(self, x, y): assert colitems[2].name == "test2[a-c]" assert colitems[3].name == "test2[b-c]" - @ignore_parametrized_marks - def test_parametrize_skipif(self, testdir): + def test_parametrize_skipif(self, testdir, ignore_parametrized_marks_args): testdir.makepyfile( """ import pytest @@ -768,11 +770,10 @@ def test_skip_if(x): assert x < 2 """ ) - result = testdir.runpytest() + result = testdir.runpytest(*ignore_parametrized_marks_args) result.stdout.fnmatch_lines("* 2 passed, 1 skipped in *") - @ignore_parametrized_marks - def test_parametrize_skip(self, testdir): + def test_parametrize_skip(self, testdir, ignore_parametrized_marks_args): testdir.makepyfile( """ import pytest @@ -784,11 +785,10 @@ def test_skip(x): assert x < 2 """ ) - result = testdir.runpytest() + result = testdir.runpytest(*ignore_parametrized_marks_args) result.stdout.fnmatch_lines("* 2 passed, 1 skipped in *") - @ignore_parametrized_marks - def test_parametrize_skipif_no_skip(self, testdir): + def test_parametrize_skipif_no_skip(self, testdir, ignore_parametrized_marks_args): testdir.makepyfile( """ import pytest @@ -800,11 +800,10 @@ def test_skipif_no_skip(x): assert x < 2 """ ) - result = testdir.runpytest() + result = testdir.runpytest(*ignore_parametrized_marks_args) result.stdout.fnmatch_lines("* 1 failed, 2 passed in *") - @ignore_parametrized_marks - def test_parametrize_xfail(self, testdir): + def test_parametrize_xfail(self, testdir, ignore_parametrized_marks_args): testdir.makepyfile( """ import pytest @@ -816,11 +815,10 @@ def test_xfail(x): assert x < 2 """ ) - result = testdir.runpytest() + result = testdir.runpytest(*ignore_parametrized_marks_args) result.stdout.fnmatch_lines("* 2 passed, 1 xfailed in *") - @ignore_parametrized_marks - def test_parametrize_passed(self, testdir): + def test_parametrize_passed(self, testdir, ignore_parametrized_marks_args): testdir.makepyfile( """ import pytest @@ -832,11 +830,10 @@ def test_xfail(x): pass """ ) - result = testdir.runpytest() + result = testdir.runpytest(*ignore_parametrized_marks_args) result.stdout.fnmatch_lines("* 2 passed, 1 xpassed in *") - @ignore_parametrized_marks - def test_parametrize_xfail_passed(self, testdir): + def test_parametrize_xfail_passed(self, testdir, ignore_parametrized_marks_args): testdir.makepyfile( """ import pytest @@ -848,7 +845,7 @@ def test_passed(x): pass """ ) - result = testdir.runpytest() + result = testdir.runpytest(*ignore_parametrized_marks_args) result.stdout.fnmatch_lines("* 3 passed in *") def test_function_original_name(self, testdir): diff --git a/testing/test_warnings.py b/testing/test_warnings.py index eb7033f1d80..73813fa0bbb 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -326,6 +326,7 @@ def pytest_warning_captured(self, warning_message, when, item): @pytest.mark.filterwarnings("always") def test_collection_warnings(testdir): """ + Check that we also capture warnings issued during test collection (#3251). """ testdir.makepyfile( """ @@ -346,3 +347,75 @@ def test_foo(): "* 1 passed, 1 warnings*", ] ) + + +class TestDeprecationWarningsByDefault: + """ + Note: all pytest runs are executed in a subprocess so we don't inherit warning filters + from pytest's own test suite + """ + + def create_file(self, testdir, mark=""): + testdir.makepyfile( + """ + import pytest, warnings + + warnings.warn(DeprecationWarning("collection")) + + {mark} + def test_foo(): + warnings.warn(PendingDeprecationWarning("test run")) + """.format( + mark=mark + ) + ) + + def test_shown_by_default(self, testdir): + self.create_file(testdir) + result = testdir.runpytest_subprocess() + result.stdout.fnmatch_lines( + [ + "*== %s ==*" % WARNINGS_SUMMARY_HEADER, + "*test_shown_by_default.py:3: DeprecationWarning: collection", + "*test_shown_by_default.py:7: PendingDeprecationWarning: test run", + "* 1 passed, 2 warnings*", + ] + ) + + def test_hidden_by_ini(self, testdir): + self.create_file(testdir) + testdir.makeini( + """ + [pytest] + filterwarnings = once::UserWarning + """ + ) + result = testdir.runpytest_subprocess() + assert WARNINGS_SUMMARY_HEADER not in result.stdout.str() + + def test_hidden_by_mark(self, testdir): + """Should hide the deprecation warning from the function, but the warning during collection should + be displayed normally. + """ + self.create_file( + testdir, mark='@pytest.mark.filterwarnings("once::UserWarning")' + ) + result = testdir.runpytest_subprocess() + result.stdout.fnmatch_lines( + [ + "*== %s ==*" % WARNINGS_SUMMARY_HEADER, + "*test_hidden_by_mark.py:3: DeprecationWarning: collection", + "* 1 passed, 1 warnings*", + ] + ) + + def test_hidden_by_cmdline(self, testdir): + self.create_file(testdir) + result = testdir.runpytest_subprocess("-W", "once::UserWarning") + assert WARNINGS_SUMMARY_HEADER not in result.stdout.str() + + def test_hidden_by_system(self, testdir, monkeypatch): + self.create_file(testdir) + monkeypatch.setenv(str("PYTHONWARNINGS"), str("once::UserWarning")) + result = testdir.runpytest_subprocess() + assert WARNINGS_SUMMARY_HEADER not in result.stdout.str() From 60499d221e0b051bb392a4b43e32311df0143184 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 3 Sep 2018 17:56:18 -0300 Subject: [PATCH 31/62] Add test to ensure that users can suppress internal warnings --- testing/test_warnings.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 73813fa0bbb..f0a17219671 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -349,6 +349,46 @@ def test_foo(): ) +@pytest.mark.filterwarnings("default") +@pytest.mark.parametrize("ignore_pytest_warnings", ["no", "ini", "cmdline"]) +def test_hide_pytest_internal_warnings(testdir, ignore_pytest_warnings): + """Make sure we can ignore internal pytest warnings using a warnings filter.""" + testdir.makepyfile( + """ + import pytest + import warnings + + warnings.warn(pytest.PytestWarning("some internal warning")) + + def test_bar(): + pass + """ + ) + if ignore_pytest_warnings == "ini": + testdir.makeini( + """ + [pytest] + filterwarnings = ignore::pytest.PytestWarning + """ + ) + args = ( + ["-W", "ignore::pytest.PytestWarning"] + if ignore_pytest_warnings == "cmdline" + else [] + ) + result = testdir.runpytest(*args) + if ignore_pytest_warnings != "no": + assert WARNINGS_SUMMARY_HEADER not in result.stdout.str() + else: + result.stdout.fnmatch_lines( + [ + "*== %s ==*" % WARNINGS_SUMMARY_HEADER, + "*test_hide_pytest_internal_warnings.py:4: PytestWarning: some internal warning", + "* 1 passed, 1 warnings *", + ] + ) + + class TestDeprecationWarningsByDefault: """ Note: all pytest runs are executed in a subprocess so we don't inherit warning filters From 0fffa6ba2f5458a22778551db7bf64b1fbd4f5b3 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 3 Sep 2018 19:27:46 -0300 Subject: [PATCH 32/62] Implement hack to issue warnings during config Once we can capture warnings during the config stage, we can then get rid of this function Related to #2891 --- doc/en/reference.rst | 2 ++ src/_pytest/config/__init__.py | 10 +++++---- src/_pytest/config/findpaths.py | 39 +++++++++++++++++---------------- src/_pytest/hookspec.py | 16 ++++++++++++-- src/_pytest/resultlog.py | 4 ++-- src/_pytest/warnings.py | 17 ++++++++++++++ testing/deprecated_test.py | 18 +++------------ 7 files changed, 64 insertions(+), 42 deletions(-) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index e19b5ae8765..52d83cf6ee3 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -611,6 +611,8 @@ Session related reporting hooks: .. autofunction:: pytest_terminal_summary .. autofunction:: pytest_fixture_setup .. autofunction:: pytest_fixture_post_finalizer +.. autofunction:: pytest_logwarning +.. autofunction:: pytest_warning_captured And here is the central hook for reporting about test execution: diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index cec56e80085..e1f126af005 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -154,7 +154,7 @@ def get_plugin_manager(): def _prepareconfig(args=None, plugins=None): - warning = None + warning_msg = None if args is None: args = sys.argv[1:] elif isinstance(args, py.path.local): @@ -165,7 +165,7 @@ def _prepareconfig(args=None, plugins=None): args = shlex.split(args, posix=sys.platform != "win32") from _pytest import deprecated - warning = deprecated.MAIN_STR_ARGS + warning_msg = deprecated.MAIN_STR_ARGS config = get_config() pluginmanager = config.pluginmanager try: @@ -175,10 +175,11 @@ def _prepareconfig(args=None, plugins=None): pluginmanager.consider_pluginarg(plugin) else: pluginmanager.register(plugin) - if warning: + if warning_msg: from _pytest.warning_types import PytestWarning + from _pytest.warnings import _issue_config_warning - warnings.warn(warning, PytestWarning) + _issue_config_warning(PytestWarning(warning_msg), config=config) return pluginmanager.hook.pytest_cmdline_parse( pluginmanager=pluginmanager, args=args ) @@ -696,6 +697,7 @@ def _initini(self, args): ns.inifilename, ns.file_or_dir + unknown_args, rootdir_cmd_arg=ns.rootdir or None, + config=self, ) self.rootdir, self.inifile, self.inicfg = r self._parser.extra_info["rootdir"] = self.rootdir diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index e10c455b1a6..7480603bece 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -10,15 +10,12 @@ def exists(path, ignore=EnvironmentError): return False -def getcfg(args): +def getcfg(args, config=None): """ Search the list of arguments for a valid ini-file for pytest, and return a tuple of (rootdir, inifile, cfg-dict). - note: warnfunc is an optional function used to warn - about ini-files that use deprecated features. - This parameter should be removed when pytest - adopts standard deprecation warnings (#1804). + note: config is optional and used only to issue warnings explicitly (#2891). """ from _pytest.deprecated import CFG_PYTEST_SECTION @@ -34,13 +31,15 @@ def getcfg(args): if exists(p): iniconfig = py.iniconfig.IniConfig(p) if "pytest" in iniconfig.sections: - if inibasename == "setup.cfg": - import warnings + if inibasename == "setup.cfg" and config is not None: + from _pytest.warnings import _issue_config_warning from _pytest.warning_types import RemovedInPytest4Warning - warnings.warn( - CFG_PYTEST_SECTION.format(filename=inibasename), - RemovedInPytest4Warning, + _issue_config_warning( + RemovedInPytest4Warning( + CFG_PYTEST_SECTION.format(filename=inibasename) + ), + config=config, ) return base, p, iniconfig["pytest"] if ( @@ -99,7 +98,7 @@ def get_dir_from_path(path): return [get_dir_from_path(path) for path in possible_paths if path.exists()] -def determine_setup(inifile, args, rootdir_cmd_arg=None): +def determine_setup(inifile, args, rootdir_cmd_arg=None, config=None): dirs = get_dirs_from_args(args) if inifile: iniconfig = py.iniconfig.IniConfig(inifile) @@ -109,14 +108,16 @@ def determine_setup(inifile, args, rootdir_cmd_arg=None): for section in sections: try: inicfg = iniconfig[section] - if is_cfg_file and section == "pytest": - from _pytest.warning_types import RemovedInPytest4Warning + if is_cfg_file and section == "pytest" and config is not None: from _pytest.deprecated import CFG_PYTEST_SECTION - import warnings + from _pytest.warning_types import RemovedInPytest4Warning + from _pytest.warnings import _issue_config_warning - warnings.warn( - CFG_PYTEST_SECTION.format(filename=str(inifile)), - RemovedInPytest4Warning, + _issue_config_warning( + RemovedInPytest4Warning( + CFG_PYTEST_SECTION.format(filename=str(inifile)) + ), + config, ) break except KeyError: @@ -124,13 +125,13 @@ def determine_setup(inifile, args, rootdir_cmd_arg=None): rootdir = get_common_ancestor(dirs) else: ancestor = get_common_ancestor(dirs) - rootdir, inifile, inicfg = getcfg([ancestor]) + rootdir, inifile, inicfg = getcfg([ancestor], config=config) if rootdir is None: for rootdir in ancestor.parts(reverse=True): if rootdir.join("setup.py").exists(): break else: - rootdir, inifile, inicfg = getcfg(dirs) + rootdir, inifile, inicfg = getcfg(dirs, config=config) if rootdir is None: rootdir = get_common_ancestor([py.path.local(), ancestor]) is_fs_root = os.path.splitdrive(str(rootdir))[1] == "/" diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 001f59b86bd..dac36b30677 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -526,7 +526,17 @@ def pytest_terminal_summary(terminalreporter, exitstatus): @hookspec(historic=True) def pytest_logwarning(message, code, nodeid, fslocation): - """ process a warning specified by a message, a code string, + """ + .. deprecated:: 3.8 + + This hook is will stop working in a future release. + + pytest no longer triggers this hook, but the + terminal writer still implements it to display warnings issued by + :meth:`_pytest.config.Config.warn` and :meth:`_pytest.nodes.Node.warn`. Calling those functions will be + an error in future releases. + + process a warning specified by a message, a code string, a nodeid and fslocation (both of which may be None if the warning is not tied to a particular node/location). @@ -538,7 +548,7 @@ def pytest_logwarning(message, code, nodeid, fslocation): @hookspec(historic=True) def pytest_warning_captured(warning_message, when, item): """ - Process a warning captured by the internal pytest plugin. + Process a warning captured by the internal pytest warnings plugin. :param warnings.WarningMessage warning_message: The captured warning. This is the same object produced by :py:func:`warnings.catch_warnings`, and contains @@ -546,6 +556,8 @@ def pytest_warning_captured(warning_message, when, item): :param str when: Indicates when the warning was captured. Possible values: + + * ``"config"``: during pytest configuration/initialization stage. * ``"collect"``: during test collection. * ``"runtest"``: during test execution. diff --git a/src/_pytest/resultlog.py b/src/_pytest/resultlog.py index 308abd251af..8a972eed736 100644 --- a/src/_pytest/resultlog.py +++ b/src/_pytest/resultlog.py @@ -31,10 +31,10 @@ def pytest_configure(config): config.pluginmanager.register(config._resultlog) from _pytest.deprecated import RESULT_LOG - import warnings from _pytest.warning_types import RemovedInPytest4Warning + from _pytest.warnings import _issue_config_warning - warnings.warn(RESULT_LOG, RemovedInPytest4Warning) + _issue_config_warning(RemovedInPytest4Warning(RESULT_LOG), config) def pytest_unconfigure(config): diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 952c4a0be15..986343fd31f 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -146,3 +146,20 @@ def pytest_terminal_summary(terminalreporter): config = terminalreporter.config with catch_warnings_for_item(config=config, ihook=config.hook, item=None): yield + + +def _issue_config_warning(warning, config): + """ + This function should be used instead of calling ``warnings.warn`` directly when we are in the "configure" stage: + at this point the actual options might not have been set, so we manually trigger the pytest_warning_captured + hook so we can display this warnings in the terminal. This is a hack until we can sort out #2891. + + :param warning: the warning instance. + :param config: + """ + with warnings.catch_warnings(record=True) as records: + warnings.simplefilter("always", type(warning)) + warnings.warn(warning, stacklevel=2) + config.hook.pytest_warning_captured.call_historic( + kwargs=dict(warning_message=records[0], when="config", item=None) + ) diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 70c6df63f39..ec53bf7eb81 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -54,9 +54,6 @@ def test_funcarg_prefix(value): @pytest.mark.filterwarnings("default") -@pytest.mark.xfail( - reason="#2891 need to handle warnings during pre-config", strict=True -) def test_pytest_setup_cfg_deprecated(testdir): testdir.makefile( ".cfg", @@ -72,9 +69,6 @@ def test_pytest_setup_cfg_deprecated(testdir): @pytest.mark.filterwarnings("default") -@pytest.mark.xfail( - reason="#2891 need to handle warnings during pre-config", strict=True -) def test_pytest_custom_cfg_deprecated(testdir): testdir.makefile( ".cfg", @@ -89,18 +83,15 @@ def test_pytest_custom_cfg_deprecated(testdir): ) -@pytest.mark.xfail( - reason="#2891 need to handle warnings during pre-config", strict=True -) -def test_str_args_deprecated(tmpdir, testdir): +def test_str_args_deprecated(tmpdir): """Deprecate passing strings to pytest.main(). Scheduled for removal in pytest-4.0.""" from _pytest.main import EXIT_NOTESTSCOLLECTED warnings = [] class Collect(object): - def pytest_logwarning(self, message): - warnings.append(message) + def pytest_warning_captured(self, warning_message): + warnings.append(str(warning_message.message)) ret = pytest.main("%s -x" % tmpdir, plugins=[Collect()]) msg = ( @@ -116,9 +107,6 @@ def test_getfuncargvalue_is_deprecated(request): @pytest.mark.filterwarnings("default") -@pytest.mark.xfail( - reason="#2891 need to handle warnings during pre-config", strict=True -) def test_resultlog_is_deprecated(testdir): result = testdir.runpytest("--help") result.stdout.fnmatch_lines(["*DEPRECATED path for machine-readable result log*"]) From 56d414177adb0194c52d5e994eef7fe264c5e82a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 3 Sep 2018 20:13:41 -0300 Subject: [PATCH 33/62] Remove nodeid from messages for warnings generated by standard warnings Standard warnings already contain the proper location, so we don't need to also print the node id --- src/_pytest/assertion/rewrite.py | 7 ++++--- src/_pytest/deprecated.py | 4 ++-- src/_pytest/junitxml.py | 5 ++--- src/_pytest/pytester.py | 2 +- src/_pytest/terminal.py | 27 +++++++++++++++++++++------ testing/test_assertrewrite.py | 15 ++++++++++----- testing/test_junitxml.py | 5 +---- testing/test_warnings.py | 7 ++----- 8 files changed, 43 insertions(+), 29 deletions(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 9c622213c67..4f16750b230 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -209,11 +209,12 @@ def mark_rewrite(self, *names): self._must_rewrite.update(names) def _warn_already_imported(self, name): - import warnings from _pytest.warning_types import PytestWarning + from _pytest.warnings import _issue_config_warning - warnings.warn( - "Module already imported so cannot be rewritten: %s" % name, PytestWarning + _issue_config_warning( + PytestWarning("Module already imported so cannot be rewritten: %s" % name), + self.config, ) def load_module(self, name): diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index a77ebf6c80f..82bf1d98d84 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -20,7 +20,7 @@ ) FIXTURE_FUNCTION_CALL = ( - "Fixture {name} called directly. Fixtures are not meant to be called directly, " + 'Fixture "{name}" called directly. Fixtures are not meant to be called directly, ' "are created automatically when test functions request them as parameters. " "See https://docs.pytest.org/en/latest/fixture.html for more information." ) @@ -29,7 +29,7 @@ "[pytest] section in {filename} files is deprecated, use [tool:pytest] instead." ) -GETFUNCARGVALUE = "use of getfuncargvalue is deprecated, use getfixturevalue" +GETFUNCARGVALUE = "getfuncargvalue is deprecated, use getfixturevalue" RESULT_LOG = ( "--result-log is deprecated and scheduled for removal in pytest 4.0.\n" diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 2f34970a10b..776cd935c9d 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -258,12 +258,11 @@ def append_property(name, value): @pytest.fixture -def record_xml_property(record_property): +def record_xml_property(record_property, request): """(Deprecated) use record_property.""" - import warnings from _pytest import deprecated - warnings.warn(deprecated.RECORD_XML_PROPERTY, DeprecationWarning, stacklevel=2) + request.node.std_warn(deprecated.RECORD_XML_PROPERTY, DeprecationWarning) return record_property diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index f8824446849..002eb62a583 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -126,7 +126,7 @@ def pytest_runtest_protocol(self, item): error.append(error[0]) error.append("*** function %s:%s: %s " % item.location) error.append("See issue #2366") - item.warn("", "\n".join(error)) + item.std_warn("", "\n".join(error), pytest.PytestWarning) # XXX copied from execnet's conftest.py - needs to be merged diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 5140741a372..4f6f0953710 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -188,17 +188,20 @@ def pytest_report_teststatus(report): @attr.s class WarningReport(object): """ - Simple structure to hold warnings information captured by ``pytest_logwarning``. + Simple structure to hold warnings information captured by ``pytest_logwarning`` and ``pytest_warning_captured``. :ivar str message: user friendly message about the warning :ivar str|None nodeid: node id that generated the warning (see ``get_location``). :ivar tuple|py.path.local fslocation: file system location of the source of the warning (see ``get_location``). + + :ivar bool legacy: if this warning report was generated from the deprecated ``pytest_logwarning`` hook. """ message = attr.ib() nodeid = attr.ib(default=None) fslocation = attr.ib(default=None) + legacy = attr.ib(default=False) def get_location(self, config): """ @@ -211,6 +214,8 @@ def get_location(self, config): if isinstance(self.fslocation, tuple) and len(self.fslocation) >= 2: filename, linenum = self.fslocation[:2] relpath = py.path.local(filename).relto(config.invocation_dir) + if not relpath: + relpath = str(filename) return "%s:%s" % (relpath, linenum) else: return str(self.fslocation) @@ -327,7 +332,9 @@ def pytest_internalerror(self, excrepr): def pytest_logwarning(self, fslocation, message, nodeid): warnings = self.stats.setdefault("warnings", []) - warning = WarningReport(fslocation=fslocation, message=message, nodeid=nodeid) + warning = WarningReport( + fslocation=fslocation, message=message, nodeid=nodeid, legacy=True + ) warnings.append(warning) def pytest_warning_captured(self, warning_message, item): @@ -707,12 +714,20 @@ def summary_warnings(self): self.write_sep("=", "warnings summary", yellow=True, bold=False) for location, warning_records in grouped: - if location: + # legacy warnings show their location explicitly, while standard warnings look better without + # it because the location is already formatted into the message + warning_records = list(warning_records) + is_legacy = warning_records[0].legacy + if location and is_legacy: self._tw.line(str(location)) for w in warning_records: - lines = w.message.splitlines() - indented = "\n".join(" " + x for x in lines) - self._tw.line(indented.rstrip()) + if is_legacy: + lines = w.message.splitlines() + indented = "\n".join(" " + x for x in lines) + message = indented.rstrip() + else: + message = w.message.rstrip() + self._tw.line(message) self._tw.line() self._tw.line("-- Docs: https://docs.pytest.org/en/latest/warnings.html") diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index c82e1dccfd7..c4cafcaab8b 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -759,11 +759,16 @@ def test_package_without__init__py(self, testdir): testdir.makepyfile("import a_package_without_init_py.module") assert testdir.runpytest().ret == EXIT_NOTESTSCOLLECTED - def test_rewrite_warning(self, pytestconfig): - hook = AssertionRewritingHook(pytestconfig) - - with pytest.warns(pytest.PytestWarning): - hook.mark_rewrite("_pytest") + def test_rewrite_warning(self, testdir): + testdir.makeconftest( + """ + import pytest + pytest.register_assert_rewrite("_pytest") + """ + ) + # needs to be a subprocess because pytester explicitly disables this warning + result = testdir.runpytest_subprocess() + result.stdout.fnmatch_lines("*Module already imported*: _pytest") def test_rewrite_module_imported_from_conftest(self, testdir): testdir.makeconftest( diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 04b4ee2d71f..3928548a8e4 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -1024,10 +1024,7 @@ def test_record(record_xml_attribute, other): tnode.assert_attr(bar="1") tnode.assert_attr(foo="<1") result.stdout.fnmatch_lines( - [ - "test_record_attribute.py::test_record", - "*test_record_attribute.py:6:*record_xml_attribute is an experimental feature", - ] + ["*test_record_attribute.py:6:*record_xml_attribute is an experimental feature"] ) diff --git a/testing/test_warnings.py b/testing/test_warnings.py index f0a17219671..eb4928a462e 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -40,14 +40,12 @@ def test_func(): @pytest.mark.filterwarnings("default") def test_normal_flow(testdir, pyfile_with_warnings): """ - Check that the warnings section is displayed, containing test node ids followed by - all warnings generated by that test node. + Check that the warnings section is displayed. """ result = testdir.runpytest() result.stdout.fnmatch_lines( [ "*== %s ==*" % WARNINGS_SUMMARY_HEADER, - "*test_normal_flow.py::test_func", "*normal_flow_module.py:3: UserWarning: user warning", '* warnings.warn(UserWarning("user warning"))', "*normal_flow_module.py:4: RuntimeWarning: runtime warning", @@ -55,7 +53,6 @@ def test_normal_flow(testdir, pyfile_with_warnings): "* 1 passed, 2 warnings*", ] ) - assert result.stdout.str().count("test_normal_flow.py::test_func") == 1 @pytest.mark.filterwarnings("always") @@ -343,7 +340,7 @@ def test_foo(): [ "*== %s ==*" % WARNINGS_SUMMARY_HEADER, "*collection_warnings.py:3: UserWarning: collection warning", - ' warnings.warn(UserWarning("collection warning"))', + ' warnings.warn(UserWarning("collection warning"))', "* 1 passed, 1 warnings*", ] ) From b81831404524b78003d7b884a6fb9ed478d21d8a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 4 Sep 2018 09:30:13 -0300 Subject: [PATCH 34/62] Improve docs for warnings capture and PEP-0506 remarks --- changelog/2908.feature.rst | 6 ++++-- doc/en/warnings.rst | 41 +++++++++++++++++++++++--------------- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/changelog/2908.feature.rst b/changelog/2908.feature.rst index 957fc30f05a..e904a98de82 100644 --- a/changelog/2908.feature.rst +++ b/changelog/2908.feature.rst @@ -1,3 +1,5 @@ ``DeprecationWarning`` and ``PendingDeprecationWarning`` are now shown by default if no other warning filter is -configured. This makes pytest compliant with -`PEP-0506 `_. +configured. This makes pytest more compliant with +`PEP-0506 `_. See +`the docs `_ for +more info. diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index ed73a69e89e..eac99730863 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -82,6 +82,21 @@ Disabling warning summary Although not recommended, you can use the ``--disable-warnings`` command-line option to suppress the warning summary entirely from the test run output. +Disabling warning capture entirely +---------------------------------- + +This plugin is enabled by default but can be disabled entirely in your ``pytest.ini`` file with: + + .. code-block:: ini + + [pytest] + addopts = -p no:warnings + +Or passing ``-p no:warnings`` in the command-line. This might be useful if your test suites handles warnings +using an external system. + + +.. _`deprecation-warnings`: DeprecationWarning and PendingDeprecationWarning ------------------------------------------------ @@ -89,11 +104,10 @@ DeprecationWarning and PendingDeprecationWarning .. versionadded:: 3.8 By default pytest will display ``DeprecationWarning`` and ``PendingDeprecationWarning`` if no other warning filters -are configured. This complies with `PEP-0506 `_ which suggests that those warnings should -be shown by default by test runners. +are configured. -To disable this behavior, you might define any warnings filter either in the command-line or in the ini file, but -if you don't have any other warnings to filter you can use: +To disable showing ``DeprecationWarning`` and ``PendingDeprecationWarning`` warnings, you might define any warnings +filter either in the command-line or in the ini file, or you can use: .. code-block:: ini @@ -102,6 +116,13 @@ if you don't have any other warnings to filter you can use: ignore::DeprecationWarning ignore::PendingDeprecationWarning +.. note:: + This makes pytest more compliant with `PEP-0506 `_ which suggests that those warnings should + be shown by default by test runners, but pytest doesn't follow ``PEP-0506`` completely because resetting all + warning filters like suggested in the PEP will break existing test suites that configure warning filters themselves + by calling ``warnings.simplefilter`` (see issue `#2430 `_ + for an example of that). + .. _`filterwarnings`: @@ -168,18 +189,6 @@ decorator or to all tests in a module by setting the ``pytestmark`` variable: .. _`pytest-warnings`: https://github.com/fschulze/pytest-warnings -Disabling warning capture -------------------------- - -This feature is enabled by default but can be disabled entirely in your ``pytest.ini`` file with: - - .. code-block:: ini - - [pytest] - addopts = -p no:warnings - -Or passing ``-p no:warnings`` in the command-line. - .. _`asserting warnings`: .. _assertwarnings: From 8ce3aeadbfc4c88c785ff2d86c644a1e5ea4d1b1 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 4 Sep 2018 10:48:11 -0300 Subject: [PATCH 35/62] Move PytestExerimentalApiWarning to warning_types --- doc/en/warnings.rst | 2 ++ src/_pytest/experiments.py | 13 ------------- src/_pytest/pytester.py | 6 ++++-- src/_pytest/warning_types.py | 17 +++++++++++++++++ src/pytest.py | 4 +++- tox.ini | 4 ++-- 6 files changed, 28 insertions(+), 18 deletions(-) delete mode 100644 src/_pytest/experiments.py diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index eac99730863..6db100fd7cf 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -378,3 +378,5 @@ The following warning types ares used by pytest and are part of the public API: .. autoclass:: pytest.RemovedInPytest4Warning +.. autoclass:: pytest.PytestExerimentalApiWarning + diff --git a/src/_pytest/experiments.py b/src/_pytest/experiments.py deleted file mode 100644 index aa6b664462e..00000000000 --- a/src/_pytest/experiments.py +++ /dev/null @@ -1,13 +0,0 @@ -class PytestExerimentalApiWarning(FutureWarning): - "warning category used to denote experiments in pytest" - - @classmethod - def simple(cls, apiname): - return cls( - "{apiname} is an experimental api that may change over time".format( - apiname=apiname - ) - ) - - -PYTESTER_COPY_EXAMPLE = PytestExerimentalApiWarning.simple("testdir.copy_example") diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 002eb62a583..ea0ccf13640 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -642,10 +642,12 @@ def mkpydir(self, name): return p def copy_example(self, name=None): - from . import experiments import warnings - warnings.warn(experiments.PYTESTER_COPY_EXAMPLE, stacklevel=2) + warnings.warn( + pytest.PytestExerimentalApiWarning.simple("testdir.copy_example"), + stacklevel=2, + ) example_dir = self.request.config.getini("pytester_example_dir") if example_dir is None: raise ValueError("pytester_example_dir is unset, can't copy examples") diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index 092a5a430d6..407cfb10083 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -20,3 +20,20 @@ class RemovedInPytest4Warning(PytestDeprecationWarning): Warning class for features scheduled to be removed in pytest 4.0. """ + + +class PytestExerimentalApiWarning(PytestWarning, FutureWarning): + """ + Bases: :class:`pytest.PytestWarning`, :class:`FutureWarning`. + + Warning category used to denote experiments in pytest. Use sparingly as the API might change or even be + removed completely in future version + """ + + @classmethod + def simple(cls, apiname): + return cls( + "{apiname} is an experimental api that may change over time".format( + apiname=apiname + ) + ) diff --git a/src/pytest.py b/src/pytest.py index c5a0666627b..e29e6116ea5 100644 --- a/src/pytest.py +++ b/src/pytest.py @@ -24,6 +24,7 @@ PytestWarning, PytestDeprecationWarning, RemovedInPytest4Warning, + PytestExerimentalApiWarning, ) set_trace = __pytestPDB.set_trace @@ -53,8 +54,9 @@ "Module", "Package", "param", - "PytestWarning", "PytestDeprecationWarning", + "PytestExerimentalApiWarning", + "PytestWarning", "raises", "register_assert_rewrite", "RemovedInPytest4Warning", diff --git a/tox.ini b/tox.ini index 4b5eae06640..a071b5bf4ee 100644 --- a/tox.ini +++ b/tox.ini @@ -229,8 +229,8 @@ filterwarnings = ignore:.*type argument to addoption.*:DeprecationWarning # produced by python >=3.5 on execnet (pytest-xdist) ignore:.*inspect.getargspec.*deprecated, use inspect.signature.*:DeprecationWarning - #pytests own futurewarnings - ignore::_pytest.experiments.PytestExerimentalApiWarning + # pytest's own futurewarnings + ignore::pytest.PytestExerimentalApiWarning pytester_example_dir = testing/example_scripts [flake8] max-line-length = 120 From 415a62e373d96be3aa28dd2dc2e2831940fd428c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 4 Sep 2018 11:15:39 -0300 Subject: [PATCH 36/62] Fix typo in PytestExperimentalApiWarning --- changelog/2452.removal.rst | 3 +++ doc/en/warnings.rst | 2 +- doc/en/writing_plugins.rst | 2 +- src/_pytest/pytester.py | 2 +- src/_pytest/warning_types.py | 2 +- src/pytest.py | 4 ++-- tox.ini | 2 +- 7 files changed, 10 insertions(+), 7 deletions(-) diff --git a/changelog/2452.removal.rst b/changelog/2452.removal.rst index 3c60f8803c6..2a2e3f81038 100644 --- a/changelog/2452.removal.rst +++ b/changelog/2452.removal.rst @@ -1,2 +1,5 @@ The functions ``Node.warn`` and ``Config.warn`` have been deprecated. Instead of ``Node.warn`` users should now use ``Node.std_warn``, while ``Config.warn`` should be replaced by the standard ``warnings.warn``. + +``RemovedInPytest4Warning`` and ``PytestExperimentalApiWarning`` are now part of the public API and should be accessed +using ``pytest.RemovedInPytest4Warning`` and ``pytest.PytestExperimentalApiWarning``. diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index 6db100fd7cf..89d064f1681 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -378,5 +378,5 @@ The following warning types ares used by pytest and are part of the public API: .. autoclass:: pytest.RemovedInPytest4Warning -.. autoclass:: pytest.PytestExerimentalApiWarning +.. autoclass:: pytest.PytestExperimentalApiWarning diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index 27e13d93208..03bad31be40 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -419,7 +419,7 @@ additionally it is possible to copy examples for a example folder before running ============================= warnings summary ============================= test_example.py::test_plugin - $REGENDOC_TMPDIR/test_example.py:4: PytestExerimentalApiWarning: testdir.copy_example is an experimental api that may change over time + $REGENDOC_TMPDIR/test_example.py:4: PytestExperimentalApiWarning: testdir.copy_example is an experimental api that may change over time testdir.copy_example("test_example.py") -- Docs: https://docs.pytest.org/en/latest/warnings.html diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index ea0ccf13640..4140dfc5020 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -645,7 +645,7 @@ def copy_example(self, name=None): import warnings warnings.warn( - pytest.PytestExerimentalApiWarning.simple("testdir.copy_example"), + pytest.PytestExperimentalApiWarning.simple("testdir.copy_example"), stacklevel=2, ) example_dir = self.request.config.getini("pytester_example_dir") diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index 407cfb10083..4422363b11a 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -22,7 +22,7 @@ class RemovedInPytest4Warning(PytestDeprecationWarning): """ -class PytestExerimentalApiWarning(PytestWarning, FutureWarning): +class PytestExperimentalApiWarning(PytestWarning, FutureWarning): """ Bases: :class:`pytest.PytestWarning`, :class:`FutureWarning`. diff --git a/src/pytest.py b/src/pytest.py index e29e6116ea5..e173fd3d4bb 100644 --- a/src/pytest.py +++ b/src/pytest.py @@ -24,7 +24,7 @@ PytestWarning, PytestDeprecationWarning, RemovedInPytest4Warning, - PytestExerimentalApiWarning, + PytestExperimentalApiWarning, ) set_trace = __pytestPDB.set_trace @@ -55,7 +55,7 @@ "Package", "param", "PytestDeprecationWarning", - "PytestExerimentalApiWarning", + "PytestExperimentalApiWarning", "PytestWarning", "raises", "register_assert_rewrite", diff --git a/tox.ini b/tox.ini index a071b5bf4ee..0b6e89aa6eb 100644 --- a/tox.ini +++ b/tox.ini @@ -230,7 +230,7 @@ filterwarnings = # produced by python >=3.5 on execnet (pytest-xdist) ignore:.*inspect.getargspec.*deprecated, use inspect.signature.*:DeprecationWarning # pytest's own futurewarnings - ignore::pytest.PytestExerimentalApiWarning + ignore::pytest.PytestExperimentalApiWarning pytester_example_dir = testing/example_scripts [flake8] max-line-length = 120 From c304998ed785debbaccb21aa21e8c6bd2148fc0e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 4 Sep 2018 11:28:23 -0300 Subject: [PATCH 37/62] Remove commented out code --- src/_pytest/python.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 9ac21633251..0fb7fb732f9 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -659,11 +659,6 @@ def collect(self): if not safe_getattr(self.obj, "__test__", True): return [] if hasinit(self.obj): - # self.warn( - # "C1", - # "cannot collect test class %r because it has a " - # "__init__ constructor" % self.obj.__name__, - # ) self.std_warn( "cannot collect test class %r because it has a " "__init__ constructor" % self.obj.__name__, From e9417be9dfa17873ce3d16ae09187d5bfffda168 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 4 Sep 2018 11:31:03 -0300 Subject: [PATCH 38/62] Add comment about deprecation warnings being shown by default --- src/_pytest/warnings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 986343fd31f..0b67bd8f137 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -85,6 +85,7 @@ def catch_warnings_for_item(config, ihook, item): filters_configured = True if not filters_configured: + # if user is not explicitly configuring warning filters, show deprecation warnings by default (#2908) warnings.filterwarnings("always", category=DeprecationWarning) warnings.filterwarnings("always", category=PendingDeprecationWarning) From 016f8f153632f9338a5ff6f290a2239ed0c72f01 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 4 Sep 2018 11:48:11 -0300 Subject: [PATCH 39/62] Improve get_fslocation_from_item's docstring --- src/_pytest/nodes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 3bb10ee8997..e9ee74b1ac2 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -346,6 +346,7 @@ def get_fslocation_from_item(item): """Tries to extract the actual location from an item, depending on available attributes: * "fslocation": a pair (path, lineno) + * "obj": a Python object that the item wraps. * "fspath": just a path :rtype: a tuple of (str|LocalPath, int) with filename and line number. From 615c6714341516df43134f997de3006a677359ae Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 4 Sep 2018 13:34:05 -0300 Subject: [PATCH 40/62] Connect string literals --- src/_pytest/assertion/rewrite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 4f16750b230..5859dd50950 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -755,7 +755,7 @@ def visit_Assert(self, assert_): import warnings warnings.warn_explicit( - "assertion is always true, perhaps " "remove parentheses?", + "assertion is always true, perhaps remove parentheses?", PytestWarning, filename=str(self.module_path), lineno=assert_.lineno, From 9ae0a3cd85bebc74c4b2f179b1b035358f9540bf Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 4 Sep 2018 13:41:11 -0300 Subject: [PATCH 41/62] Do not trigger warning about tuples being always True if the tuple has size != 2 --- src/_pytest/assertion/rewrite.py | 2 +- testing/test_assertion.py | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 5859dd50950..1868f0f7a38 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -750,7 +750,7 @@ def visit_Assert(self, assert_): the expression is false. """ - if isinstance(assert_.test, ast.Tuple): + if isinstance(assert_.test, ast.Tuple) and len(assert_.test.elts) == 2: from _pytest.warning_types import PytestWarning import warnings diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 2c7f4b33d9c..6a2a1ed3856 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -1077,16 +1077,25 @@ def test_diff(): @pytest.mark.filterwarnings("default") def test_assert_tuple_warning(testdir): + msg = "assertion is always true" testdir.makepyfile( """ def test_tuple(): assert(False, 'you shall not pass') """ ) - result = testdir.runpytest("-rw") - result.stdout.fnmatch_lines( - ["*test_assert_tuple_warning.py:2:*assertion is always true*"] + result = testdir.runpytest() + result.stdout.fnmatch_lines(["*test_assert_tuple_warning.py:2:*{}*".format(msg)]) + + # tuples with size != 2 should not trigger the warning + testdir.makepyfile( + """ + def test_tuple(): + assert () + """ ) + result = testdir.runpytest() + assert msg not in result.stdout.str() def test_assert_indirect_tuple_no_warning(testdir): From 284a2d110fa840610cfa6e05ec69e37ce31587cb Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 4 Sep 2018 13:46:33 -0300 Subject: [PATCH 42/62] Move warnings import to top level --- src/_pytest/cacheprovider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index dbe95340672..f27a045493b 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -6,6 +6,7 @@ """ from __future__ import absolute_import, division, print_function from collections import OrderedDict +import warnings import py import six @@ -47,7 +48,6 @@ def cache_dir_from_config(config): return paths.resolve_from_str(config.getini("cache_dir"), config.rootdir) def warn(self, fmt, **args): - import warnings from _pytest.warning_types import PytestWarning warnings.warn( From b42518acd5c8f7d8d034bd47addd06c36b062b48 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 4 Sep 2018 14:20:42 -0300 Subject: [PATCH 43/62] Change std_warn to receive a single warning instance, addressed review suggestions --- src/_pytest/config/__init__.py | 9 ++++----- src/_pytest/deprecated.py | 17 +++++++++++++---- src/_pytest/fixtures.py | 3 ++- src/_pytest/junitxml.py | 5 ++--- src/_pytest/mark/structures.py | 12 +++++++----- src/_pytest/nodes.py | 26 ++++++++++++++------------ src/_pytest/pytester.py | 8 +++----- src/_pytest/python.py | 33 +++++++++++++++------------------ src/_pytest/warning_types.py | 3 +++ src/_pytest/warnings.py | 6 +++--- testing/acceptance_test.py | 2 +- testing/deprecated_test.py | 3 ++- testing/python/metafunc.py | 8 ++++---- testing/test_mark.py | 6 +++++- testing/test_nodes.py | 11 +++++++++++ 15 files changed, 89 insertions(+), 63 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index e1f126af005..dfee41bdf10 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -154,7 +154,7 @@ def get_plugin_manager(): def _prepareconfig(args=None, plugins=None): - warning_msg = None + warning = None if args is None: args = sys.argv[1:] elif isinstance(args, py.path.local): @@ -165,7 +165,7 @@ def _prepareconfig(args=None, plugins=None): args = shlex.split(args, posix=sys.platform != "win32") from _pytest import deprecated - warning_msg = deprecated.MAIN_STR_ARGS + warning = deprecated.MAIN_STR_ARGS config = get_config() pluginmanager = config.pluginmanager try: @@ -175,11 +175,10 @@ def _prepareconfig(args=None, plugins=None): pluginmanager.consider_pluginarg(plugin) else: pluginmanager.register(plugin) - if warning_msg: - from _pytest.warning_types import PytestWarning + if warning: from _pytest.warnings import _issue_config_warning - _issue_config_warning(PytestWarning(warning_msg), config=config) + _issue_config_warning(warning, config=config) return pluginmanager.hook.pytest_cmdline_parse( pluginmanager=pluginmanager, args=args ) diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 82bf1d98d84..d7a07503d9a 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -9,9 +9,14 @@ from _pytest.warning_types import RemovedInPytest4Warning -MAIN_STR_ARGS = "passing a string to pytest.main() is deprecated, " "pass a list of arguments instead." +MAIN_STR_ARGS = RemovedInPytest4Warning( + "passing a string to pytest.main() is deprecated, " + "pass a list of arguments instead." +) -YIELD_TESTS = "yield tests are deprecated, and scheduled to be removed in pytest 4.0" +YIELD_TESTS = RemovedInPytest4Warning( + "yield tests are deprecated, and scheduled to be removed in pytest 4.0" +) FUNCARG_PREFIX = ( '{name}: declaring fixtures using "pytest_funcarg__" prefix is deprecated ' @@ -48,7 +53,11 @@ "For more details, see: https://docs.pytest.org/en/latest/parametrize.html" ) -RECORD_XML_PROPERTY = ( +NODE_WARN = RemovedInPytest4Warning( + "Node.warn has been deprecated, use Node.std_warn instead" +) + +RECORD_XML_PROPERTY = RemovedInPytest4Warning( 'Fixture renamed from "record_xml_property" to "record_property" as user ' "properties are now available to all reporters.\n" '"record_xml_property" is now deprecated.' @@ -58,7 +67,7 @@ "pycollector makeitem was removed " "as it is an accidentially leaked internal api" ) -METAFUNC_ADD_CALL = ( +METAFUNC_ADD_CALL = RemovedInPytest4Warning( "Metafunc.addcall is deprecated and scheduled to be removed in pytest 4.0.\n" "Please use Metafunc.parametrize instead." ) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 476acab0272..cbfda9a82c6 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1257,6 +1257,8 @@ def pytest_collection_modifyitems(self, items): items[:] = reorder_items(items) def parsefactories(self, node_or_obj, nodeid=NOTSET, unittest=False): + from _pytest import deprecated + if nodeid is not NOTSET: holderobj = node_or_obj else: @@ -1279,7 +1281,6 @@ def parsefactories(self, node_or_obj, nodeid=NOTSET, unittest=False): if not callable(obj): continue marker = defaultfuncargprefixmarker - from _pytest import deprecated filename, lineno = getfslineno(obj) warnings.warn_explicit( diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 776cd935c9d..e2579860b6c 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -262,7 +262,7 @@ def record_xml_property(record_property, request): """(Deprecated) use record_property.""" from _pytest import deprecated - request.node.std_warn(deprecated.RECORD_XML_PROPERTY, DeprecationWarning) + request.node.std_warn(deprecated.RECORD_XML_PROPERTY) return record_property @@ -276,8 +276,7 @@ def record_xml_attribute(request): from _pytest.warning_types import PytestWarning request.node.std_warn( - message="record_xml_attribute is an experimental feature", - category=PytestWarning, + PytestWarning("record_xml_attribute is an experimental feature") ) xml = getattr(request.config, "_xml", None) if xml is not None: diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 0e0ba96e5a9..52a780ead26 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -65,7 +65,7 @@ def param_extract_id(id=None): return cls(values, marks, id_) @classmethod - def extract_from(cls, parameterset, legacy_force_tuple=False, item=None): + def extract_from(cls, parameterset, belonging_definition, legacy_force_tuple=False): """ :param parameterset: a legacy style parameterset that may or may not be a tuple, @@ -75,7 +75,7 @@ def extract_from(cls, parameterset, legacy_force_tuple=False, item=None): enforce tuple wrapping so single argument tuple values don't get decomposed and break tests - :param item: the item that we will be extracting the parameters from. + :param belonging_definition: the item that we will be extracting the parameters from. """ if isinstance(parameterset, cls): @@ -94,8 +94,8 @@ def extract_from(cls, parameterset, legacy_force_tuple=False, item=None): if legacy_force_tuple: argval = (argval,) - if newmarks and item is not None: - item.std_warn(MARK_PARAMETERSET_UNPACKING) + if newmarks and belonging_definition is not None: + belonging_definition.std_warn(MARK_PARAMETERSET_UNPACKING) return cls(argval, marks=newmarks, id=None) @@ -108,7 +108,9 @@ def _for_parametrize(cls, argnames, argvalues, func, config, function_definition force_tuple = False parameters = [ ParameterSet.extract_from( - x, legacy_force_tuple=force_tuple, item=function_definition + x, + legacy_force_tuple=force_tuple, + belonging_definition=function_definition, ) for x in argvalues ] diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index e9ee74b1ac2..7a2b48ce2d1 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -144,12 +144,9 @@ def warn(self, code, message): Generate a warning with the given code and message for this item. """ - from _pytest.warning_types import RemovedInPytest4Warning + from _pytest.deprecated import NODE_WARN - self.std_warn( - "Node.warn has been deprecated, use Node.std_warn instead", - RemovedInPytest4Warning, - ) + self.std_warn(NODE_WARN) assert isinstance(code, str) fslocation = get_fslocation_from_item(self) @@ -159,22 +156,27 @@ def warn(self, code, message): ) ) - def std_warn(self, message, category=None): + def std_warn(self, warning): """Issue a warning for this item. Warnings will be displayed after the test session, unless explicitly suppressed - :param Union[str,Warning] message: text message of the warning or ``Warning`` instance. - :param Type[Warning] category: warning category. + :param Warning warning: the warning instance to issue. Must be a subclass of PytestWarning. + + :raise ValueError: if ``warning`` instance is not a subclass of PytestWarning. """ from _pytest.warning_types import PytestWarning - if category is None: - assert isinstance(message, PytestWarning) + if not isinstance(warning, PytestWarning): + raise ValueError( + "warning must be an instance of PytestWarning or subclass, got {!r}".format( + warning + ) + ) path, lineno = get_fslocation_from_item(self) warnings.warn_explicit( - message, - category, + six.text_type(warning), + type(warning), filename=str(path), lineno=lineno + 1 if lineno is not None else None, ) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 4140dfc5020..62127651abb 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -126,7 +126,7 @@ def pytest_runtest_protocol(self, item): error.append(error[0]) error.append("*** function %s:%s: %s " % item.location) error.append("See issue #2366") - item.std_warn("", "\n".join(error), pytest.PytestWarning) + item.std_warn(pytest.PytestWarning("\n".join(error))) # XXX copied from execnet's conftest.py - needs to be merged @@ -643,11 +643,9 @@ def mkpydir(self, name): def copy_example(self, name=None): import warnings + from _pytest.warning_types import PYTESTER_COPY_EXAMPLE - warnings.warn( - pytest.PytestExperimentalApiWarning.simple("testdir.copy_example"), - stacklevel=2, - ) + warnings.warn(PYTESTER_COPY_EXAMPLE, stacklevel=2) example_dir = self.request.config.getini("pytester_example_dir") if example_dir is None: raise ValueError("pytester_example_dir is unset, can't copy examples") diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 0fb7fb732f9..fa14b7a33f2 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -660,9 +660,10 @@ def collect(self): return [] if hasinit(self.obj): self.std_warn( - "cannot collect test class %r because it has a " - "__init__ constructor" % self.obj.__name__, - PytestWarning, + PytestWarning( + "cannot collect test class %r because it has a " + "__init__ constructor" % self.obj.__name__ + ) ) return [] elif hasnew(self.obj): @@ -798,7 +799,7 @@ def collect(self): ) seen[name] = True values.append(self.Function(name, self, args=args, callobj=call)) - self.std_warn(deprecated.YIELD_TESTS, RemovedInPytest4Warning) + self.std_warn(deprecated.YIELD_TESTS) return values def getcallargs(self, obj): @@ -1105,9 +1106,7 @@ def addcall(self, funcargs=None, id=NOTSET, param=NOTSET): invocation through the ``request.param`` attribute. """ if self.config: - self.definition.std_warn( - deprecated.METAFUNC_ADD_CALL, RemovedInPytest4Warning - ) + self.definition.std_warn(deprecated.METAFUNC_ADD_CALL) assert funcargs is None or isinstance(funcargs, dict) if funcargs is not None: @@ -1158,22 +1157,20 @@ def _find_parametrized_scope(argnames, arg2fixturedefs, indirect): return "function" -def _idval(val, argname, idx, idfn, config=None, item=None): +def _idval(val, argname, idx, idfn, item, config=None): if idfn: s = None try: s = idfn(val) except Exception as e: # See issue https://github.com/pytest-dev/pytest/issues/2169 - if item is not None: - # should really be None only when unit-testing this function! - msg = ( - "While trying to determine id of parameter {} at position " - "{} the following exception was raised:\n".format(argname, idx) - ) - msg += " {}: {}\n".format(type(e).__name__, e) - msg += "This warning will be an error error in pytest-4.0." - item.std_warn(msg, RemovedInPytest4Warning) + msg = ( + "While trying to determine id of parameter {} at position " + "{} the following exception was raised:\n".format(argname, idx) + ) + msg += " {}: {}\n".format(type(e).__name__, e) + msg += "This warning will be an error error in pytest-4.0." + item.std_warn(RemovedInPytest4Warning(msg)) if s: return ascii_escaped(s) @@ -1202,7 +1199,7 @@ def _idvalset(idx, parameterset, argnames, idfn, ids, config=None, item=None): return parameterset.id if ids is None or (idx >= len(ids) or ids[idx] is None): this_id = [ - _idval(val, argname, idx, idfn, config, item) + _idval(val, argname, idx, idfn, item=item, config=config) for val, argname in zip(parameterset.values, argnames) ] return "-".join(this_id) diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index 4422363b11a..8861f6f2b06 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -37,3 +37,6 @@ def simple(cls, apiname): apiname=apiname ) ) + + +PYTESTER_COPY_EXAMPLE = PytestExperimentalApiWarning.simple("testdir.copy_example") diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 0b67bd8f137..2d73def0ff3 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -129,20 +129,20 @@ def warning_record_to_str(warning_message): return msg -@pytest.hookimpl(hookwrapper=True) +@pytest.hookimpl(hookwrapper=True, tryfirst=True) def pytest_runtest_protocol(item): with catch_warnings_for_item(config=item.config, ihook=item.ihook, item=item): yield -@pytest.hookimpl(hookwrapper=True) +@pytest.hookimpl(hookwrapper=True, tryfirst=True) def pytest_collection(session): config = session.config with catch_warnings_for_item(config=config, ihook=config.hook, item=None): yield -@pytest.hookimpl(hookwrapper=True) +@pytest.hookimpl(hookwrapper=True, tryfirst=True) def pytest_terminal_summary(terminalreporter): config = terminalreporter.config with catch_warnings_for_item(config=config, ihook=config.hook, item=None): diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 6b374083f14..428ac464c0b 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -526,7 +526,7 @@ def test_equivalence_pytest_pytest(self): assert pytest.main == py.test.cmdline.main def test_invoke_with_string(self, capsys): - retcode = pytest.main(["-h"]) + retcode = pytest.main("-h") assert not retcode out, err = capsys.readouterr() assert "--help" in out diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index ec53bf7eb81..a74ce662d10 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -289,6 +289,7 @@ def fix(): def test_pycollector_makeitem_is_deprecated(): from _pytest.python import PyCollector + from _pytest.warning_types import RemovedInPytest4Warning class PyCollectorMock(PyCollector): """evil hack""" @@ -301,6 +302,6 @@ def _makeitem(self, *k): self.called = True collector = PyCollectorMock() - with pytest.deprecated_call(): + with pytest.warns(RemovedInPytest4Warning): collector.makeitem("foo", "bar") assert collector.called diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 5d928243541..e1dfb6d8b33 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -217,7 +217,7 @@ class A(object): def test_idval_hypothesis(self, value): from _pytest.python import _idval - escaped = _idval(value, "a", 6, None) + escaped = _idval(value, "a", 6, None, item=None) assert isinstance(escaped, str) if PY3: escaped.encode("ascii") @@ -244,7 +244,7 @@ def test_unicode_idval(self): ), ] for val, expected in values: - assert _idval(val, "a", 6, None) == expected + assert _idval(val, "a", 6, None, item=None) == expected def test_bytes_idval(self): """unittest for the expected behavior to obtain ids for parametrized @@ -262,7 +262,7 @@ def test_bytes_idval(self): (u"αρά".encode("utf-8"), "\\xce\\xb1\\xcf\\x81\\xce\\xac"), ] for val, expected in values: - assert _idval(val, "a", 6, None) == expected + assert _idval(val, "a", 6, idfn=None, item=None, config=None) == expected def test_class_or_function_idval(self): """unittest for the expected behavior to obtain ids for parametrized @@ -278,7 +278,7 @@ def test_function(): values = [(TestClass, "TestClass"), (test_function, "test_function")] for val, expected in values: - assert _idval(val, "a", 6, None) == expected + assert _idval(val, "a", 6, None, item=None) == expected @pytest.mark.issue250 def test_idmaker_autoname(self): diff --git a/testing/test_mark.py b/testing/test_mark.py index 12aded41683..0a6567521d3 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -1041,7 +1041,11 @@ def assert_test_is_not_selected(keyword): ) @pytest.mark.filterwarnings("ignore") def test_parameterset_extractfrom(argval, expected): - extracted = ParameterSet.extract_from(argval) + class DummyItem: + def std_warn(self, warning): + pass + + extracted = ParameterSet.extract_from(argval, belonging_definition=DummyItem()) assert extracted == expected diff --git a/testing/test_nodes.py b/testing/test_nodes.py index eee3ac8e9e4..d62d7d78a94 100644 --- a/testing/test_nodes.py +++ b/testing/test_nodes.py @@ -19,3 +19,14 @@ def test_ischildnode(baseid, nodeid, expected): result = nodes.ischildnode(baseid, nodeid) assert result is expected + + +def test_std_warn_not_pytestwarning(testdir): + items = testdir.getitems( + """ + def test(): + pass + """ + ) + with pytest.raises(ValueError, match=".*instance of PytestWarning.*"): + items[0].std_warn(UserWarning("some warning")) From 022c58bf640a014a1c78b872840d9fa5fbeba084 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 4 Sep 2018 14:26:34 -0300 Subject: [PATCH 44/62] Revert pytest_terminal_summary(tryfirst) in warnings module as this breaks tests --- src/_pytest/warnings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 2d73def0ff3..4ef4e7f0e00 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -142,7 +142,7 @@ def pytest_collection(session): yield -@pytest.hookimpl(hookwrapper=True, tryfirst=True) +@pytest.hookimpl(hookwrapper=True) def pytest_terminal_summary(terminalreporter): config = terminalreporter.config with catch_warnings_for_item(config=config, ihook=config.hook, item=None): From d3f72ca20204251b261638297052c356dfab1f47 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 4 Sep 2018 14:33:41 -0300 Subject: [PATCH 45/62] Fix linting for warnings.rst --- doc/en/warnings.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index 89d064f1681..a460ac121a4 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -379,4 +379,3 @@ The following warning types ares used by pytest and are part of the public API: .. autoclass:: pytest.RemovedInPytest4Warning .. autoclass:: pytest.PytestExperimentalApiWarning - From f1cfd10c94f0d62f868ed00a7406c9fe91426173 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 4 Sep 2018 14:44:02 -0300 Subject: [PATCH 46/62] Handle cache warnings in tests --- testing/test_cacheprovider.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index c9d17422971..1048994d3e7 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -40,6 +40,7 @@ def test_cache_writefail_cachfile_silent(self, testdir): cache.set("test/broken", []) @pytest.mark.skipif(sys.platform.startswith("win"), reason="no chmod on windows") + @pytest.mark.filterwarnings("ignore:could not create cache path:PytestWarning") def test_cache_writefail_permissions(self, testdir): testdir.makeini("[pytest]") testdir.tmpdir.ensure_dir(".pytest_cache").chmod(0) @@ -48,6 +49,7 @@ def test_cache_writefail_permissions(self, testdir): cache.set("test/broken", []) @pytest.mark.skipif(sys.platform.startswith("win"), reason="no chmod on windows") + @pytest.mark.filterwarnings("default") def test_cache_failure_warns(self, testdir): testdir.tmpdir.ensure_dir(".pytest_cache").chmod(0) testdir.makepyfile( From a054aa47978770aa9c12e1b244615816c3bbe052 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 4 Sep 2018 14:45:48 -0300 Subject: [PATCH 47/62] Issue assert rewrite warning if tuple >=1 as suggested in review --- src/_pytest/assertion/rewrite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 1868f0f7a38..8c7944593ae 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -750,7 +750,7 @@ def visit_Assert(self, assert_): the expression is false. """ - if isinstance(assert_.test, ast.Tuple) and len(assert_.test.elts) == 2: + if isinstance(assert_.test, ast.Tuple) and len(assert_.test.elts) >= 1: from _pytest.warning_types import PytestWarning import warnings From 5ef51262f7f8db58efd10800e8335a71d3b1cb4a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 4 Sep 2018 15:06:14 -0300 Subject: [PATCH 48/62] Fix reference to PytestWarning in warningsfilter mark --- testing/test_cacheprovider.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index 1048994d3e7..6d425f95b03 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -40,7 +40,9 @@ def test_cache_writefail_cachfile_silent(self, testdir): cache.set("test/broken", []) @pytest.mark.skipif(sys.platform.startswith("win"), reason="no chmod on windows") - @pytest.mark.filterwarnings("ignore:could not create cache path:PytestWarning") + @pytest.mark.filterwarnings( + "ignore:could not create cache path:pytest.PytestWarning" + ) def test_cache_writefail_permissions(self, testdir): testdir.makeini("[pytest]") testdir.tmpdir.ensure_dir(".pytest_cache").chmod(0) From 47bf58d69e3995f0eb04ecb268a9c19e52c52ed5 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 4 Sep 2018 15:07:52 -0300 Subject: [PATCH 49/62] Make Node.warn support two forms, new and deprecated As suggested during review, it now accepts two forms: Node.warn(warning_instance) (recommended) Node.warn(code, message) (deprecated) --- changelog/2452.removal.rst | 11 +++++++-- src/_pytest/deprecated.py | 2 +- src/_pytest/junitxml.py | 6 ++--- src/_pytest/mark/structures.py | 2 +- src/_pytest/nodes.py | 45 ++++++++++++++++++++++++++++++---- src/_pytest/pytester.py | 2 +- src/_pytest/python.py | 10 ++++---- testing/test_config.py | 2 +- testing/test_mark.py | 2 +- testing/test_nodes.py | 2 +- 10 files changed, 62 insertions(+), 22 deletions(-) diff --git a/changelog/2452.removal.rst b/changelog/2452.removal.rst index 2a2e3f81038..c2a02830389 100644 --- a/changelog/2452.removal.rst +++ b/changelog/2452.removal.rst @@ -1,5 +1,12 @@ -The functions ``Node.warn`` and ``Config.warn`` have been deprecated. Instead of ``Node.warn`` users should now use -``Node.std_warn``, while ``Config.warn`` should be replaced by the standard ``warnings.warn``. +``Config.warn`` has been deprecated, it should be replaced by calls to the standard ``warnings.warn``. + +``Node.warn`` now supports two signatures: + +* ``node.warn(PytestWarning("some message"))``: is now the recommended way to call this function. The warning + instance must be a ``PytestWarning`` or subclass instance. + +* ``node.warn("CI", "some message")``: this code/message form is now deprecated and should be converted to + the warning instance form above. ``RemovedInPytest4Warning`` and ``PytestExperimentalApiWarning`` are now part of the public API and should be accessed using ``pytest.RemovedInPytest4Warning`` and ``pytest.PytestExperimentalApiWarning``. diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index d7a07503d9a..5e2c589620d 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -54,7 +54,7 @@ ) NODE_WARN = RemovedInPytest4Warning( - "Node.warn has been deprecated, use Node.std_warn instead" + "Node.warn(code, message) form has been deprecated, use Node.warn(warning_instance) instead." ) RECORD_XML_PROPERTY = RemovedInPytest4Warning( diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index e2579860b6c..7fa49bc2876 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -262,7 +262,7 @@ def record_xml_property(record_property, request): """(Deprecated) use record_property.""" from _pytest import deprecated - request.node.std_warn(deprecated.RECORD_XML_PROPERTY) + request.node.warn(deprecated.RECORD_XML_PROPERTY) return record_property @@ -275,9 +275,7 @@ def record_xml_attribute(request): """ from _pytest.warning_types import PytestWarning - request.node.std_warn( - PytestWarning("record_xml_attribute is an experimental feature") - ) + request.node.warn(PytestWarning("record_xml_attribute is an experimental feature")) xml = getattr(request.config, "_xml", None) if xml is not None: node_reporter = xml.node_reporter(request.node.nodeid) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 52a780ead26..8e8937d592d 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -95,7 +95,7 @@ def extract_from(cls, parameterset, belonging_definition, legacy_force_tuple=Fal argval = (argval,) if newmarks and belonging_definition is not None: - belonging_definition.std_warn(MARK_PARAMETERSET_UNPACKING) + belonging_definition.warn(MARK_PARAMETERSET_UNPACKING) return cls(argval, marks=newmarks, id=None) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 7a2b48ce2d1..53d22dc2277 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -136,7 +136,42 @@ def _getcustomclass(self, name): def __repr__(self): return "<%s %r>" % (self.__class__.__name__, getattr(self, "name", None)) - def warn(self, code, message): + def warn(self, code_or_warning, message=None): + """Issue a warning for this item. + + Warnings will be displayed after the test session, unless explicitly suppressed. + + This can be called in two forms: + + **Warning instance** + + This was introduced in pytest 3.8 and uses the standard warning mechanism to issue warnings. + + .. code-block:: python + + node.warn(PytestWarning("some message")) + + The warning instance must be a subclass of :class:`pytest.PytestWarning`. + + **code/message (deprecated)** + + This form was used in pytest prior to 3.8 and is considered deprecated. Using this form will emit another + warning about the deprecation: + + .. code-block:: python + + node.warn("CI", "some message") + + :param Union[Warning,str] code_or_warning: warning instance or warning code (legacy). + :param Union[str,None] message: message to display when called in the legacy form. + :return: + """ + if message is None: + self._std_warn(code_or_warning) + else: + self._legacy_warn(code_or_warning, message) + + def _legacy_warn(self, code, message): """ .. deprecated:: 3.8 @@ -146,7 +181,7 @@ def warn(self, code, message): """ from _pytest.deprecated import NODE_WARN - self.std_warn(NODE_WARN) + self._std_warn(NODE_WARN) assert isinstance(code, str) fslocation = get_fslocation_from_item(self) @@ -156,7 +191,7 @@ def warn(self, code, message): ) ) - def std_warn(self, warning): + def _std_warn(self, warning): """Issue a warning for this item. Warnings will be displayed after the test session, unless explicitly suppressed @@ -175,8 +210,8 @@ def std_warn(self, warning): ) path, lineno = get_fslocation_from_item(self) warnings.warn_explicit( - six.text_type(warning), - type(warning), + warning, + category=None, filename=str(path), lineno=lineno + 1 if lineno is not None else None, ) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 62127651abb..a5099917240 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -126,7 +126,7 @@ def pytest_runtest_protocol(self, item): error.append(error[0]) error.append("*** function %s:%s: %s " % item.location) error.append("See issue #2366") - item.std_warn(pytest.PytestWarning("\n".join(error))) + item.warn(pytest.PytestWarning("\n".join(error))) # XXX copied from execnet's conftest.py - needs to be merged diff --git a/src/_pytest/python.py b/src/_pytest/python.py index fa14b7a33f2..10a3b1ec321 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -659,7 +659,7 @@ def collect(self): if not safe_getattr(self.obj, "__test__", True): return [] if hasinit(self.obj): - self.std_warn( + self.warn( PytestWarning( "cannot collect test class %r because it has a " "__init__ constructor" % self.obj.__name__ @@ -667,7 +667,7 @@ def collect(self): ) return [] elif hasnew(self.obj): - self.std_warn( + self.warn( PytestWarning( "cannot collect test class %r because it has a " "__new__ constructor" % self.obj.__name__ @@ -799,7 +799,7 @@ def collect(self): ) seen[name] = True values.append(self.Function(name, self, args=args, callobj=call)) - self.std_warn(deprecated.YIELD_TESTS) + self.warn(deprecated.YIELD_TESTS) return values def getcallargs(self, obj): @@ -1106,7 +1106,7 @@ def addcall(self, funcargs=None, id=NOTSET, param=NOTSET): invocation through the ``request.param`` attribute. """ if self.config: - self.definition.std_warn(deprecated.METAFUNC_ADD_CALL) + self.definition.warn(deprecated.METAFUNC_ADD_CALL) assert funcargs is None or isinstance(funcargs, dict) if funcargs is not None: @@ -1170,7 +1170,7 @@ def _idval(val, argname, idx, idfn, item, config=None): ) msg += " {}: {}\n".format(type(e).__name__, e) msg += "This warning will be an error error in pytest-4.0." - item.std_warn(RemovedInPytest4Warning(msg)) + item.warn(RemovedInPytest4Warning(msg)) if s: return ascii_escaped(s) diff --git a/testing/test_config.py b/testing/test_config.py index c0630d68830..fac780a0542 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -831,7 +831,7 @@ def test_hello(fix): ===*warnings summary*=== *test_warn_on_test_item_from_request.py::test_hello* *hello* - *test_warn_on_test_item_from_request.py:7:*Node.warn has been deprecated, use Node.std_warn instead* + *test_warn_on_test_item_from_request.py:7:*Node.warn(code, message) form has been deprecated* """ ) diff --git a/testing/test_mark.py b/testing/test_mark.py index 0a6567521d3..f50902eb18e 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -1042,7 +1042,7 @@ def assert_test_is_not_selected(keyword): @pytest.mark.filterwarnings("ignore") def test_parameterset_extractfrom(argval, expected): class DummyItem: - def std_warn(self, warning): + def warn(self, warning): pass extracted = ParameterSet.extract_from(argval, belonging_definition=DummyItem()) diff --git a/testing/test_nodes.py b/testing/test_nodes.py index d62d7d78a94..9219f45e517 100644 --- a/testing/test_nodes.py +++ b/testing/test_nodes.py @@ -29,4 +29,4 @@ def test(): """ ) with pytest.raises(ValueError, match=".*instance of PytestWarning.*"): - items[0].std_warn(UserWarning("some warning")) + items[0].warn(UserWarning("some warning")) From 438f7a12545915cd5a5d638a0daa64fcfc295492 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 4 Sep 2018 15:22:07 -0300 Subject: [PATCH 50/62] Add "setup", "call" and "teardown" values to "when" parameter of pytest_warning_captured hook --- src/_pytest/hookspec.py | 7 +++++-- src/_pytest/warnings.py | 34 ++++++++++++++++++++++++++++------ testing/test_warnings.py | 36 ++++++++++++++++++++++++++++++++---- 3 files changed, 65 insertions(+), 12 deletions(-) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index dac36b30677..3e647d1a153 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -559,10 +559,13 @@ def pytest_warning_captured(warning_message, when, item): * ``"config"``: during pytest configuration/initialization stage. * ``"collect"``: during test collection. - * ``"runtest"``: during test execution. + * ``"setup"``: during test setup. + * ``"call"``: during test call. + * ``"teardown"``: during test teardown. :param pytest.Item|None item: - The item being executed if ``when == "runtest"``, else ``None``. + The item being executed if ``when`` is ``"setup"``, ``"call"`` or ``"teardown"``, otherwise + ``None``. """ diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 4ef4e7f0e00..e0206883a8f 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -59,7 +59,7 @@ def pytest_configure(config): @contextmanager -def catch_warnings_for_item(config, ihook, item): +def catch_warnings_for_item(config, ihook, when, item): """ Context manager that catches warnings generated in the contained execution block. @@ -93,7 +93,7 @@ def catch_warnings_for_item(config, ihook, item): for warning_message in log: ihook.pytest_warning_captured.call_historic( - kwargs=dict(warning_message=warning_message, when="runtest", item=item) + kwargs=dict(warning_message=warning_message, when=when, item=item) ) @@ -130,22 +130,44 @@ def warning_record_to_str(warning_message): @pytest.hookimpl(hookwrapper=True, tryfirst=True) -def pytest_runtest_protocol(item): - with catch_warnings_for_item(config=item.config, ihook=item.ihook, item=item): +def pytest_runtest_setup(item): + with catch_warnings_for_item( + config=item.config, ihook=item.ihook, when="setup", item=item + ): + yield + + +@pytest.hookimpl(hookwrapper=True, tryfirst=True) +def pytest_runtest_call(item): + with catch_warnings_for_item( + config=item.config, ihook=item.ihook, when="call", item=item + ): + yield + + +@pytest.hookimpl(hookwrapper=True, tryfirst=True) +def pytest_runtest_teardown(item): + with catch_warnings_for_item( + config=item.config, ihook=item.ihook, when="teardown", item=item + ): yield @pytest.hookimpl(hookwrapper=True, tryfirst=True) def pytest_collection(session): config = session.config - with catch_warnings_for_item(config=config, ihook=config.hook, item=None): + with catch_warnings_for_item( + config=config, ihook=config.hook, when="collect", item=None + ): yield @pytest.hookimpl(hookwrapper=True) def pytest_terminal_summary(terminalreporter): config = terminalreporter.config - with catch_warnings_for_item(config=config, ihook=config.hook, item=None): + with catch_warnings_for_item( + config=config, ihook=config.hook, when="config", item=None + ): yield diff --git a/testing/test_warnings.py b/testing/test_warnings.py index eb4928a462e..119b67ceed1 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -302,20 +302,48 @@ def test_func(): @pytest.mark.filterwarnings("always") -def test_warning_captured_hook(testdir, pyfile_with_warnings): +def test_warning_captured_hook(testdir): + testdir.makeconftest( + """ + from _pytest.warnings import _issue_config_warning + def pytest_configure(config): + _issue_config_warning(UserWarning("config warning"), config) + """ + ) + testdir.makepyfile( + """ + import pytest, warnings + + warnings.warn(UserWarning("collect warning")) + + @pytest.fixture + def fix(): + warnings.warn(UserWarning("setup warning")) + yield 1 + warnings.warn(UserWarning("teardown warning")) + + def test_func(fix): + warnings.warn(UserWarning("call warning")) + assert fix == 1 + """ + ) collected = [] class WarningCollector: def pytest_warning_captured(self, warning_message, when, item): - collected.append((warning_message.category, when, item.name)) + imge_name = item.name if item is not None else "" + collected.append((str(warning_message.message), when, imge_name)) result = testdir.runpytest(plugins=[WarningCollector()]) result.stdout.fnmatch_lines(["*1 passed*"]) expected = [ - (UserWarning, "runtest", "test_func"), - (RuntimeWarning, "runtest", "test_func"), + ("config warning", "config", ""), + ("collect warning", "collect", ""), + ("setup warning", "setup", "test_func"), + ("call warning", "call", "test_func"), + ("teardown warning", "teardown", "test_func"), ] assert collected == expected From 3db76ccf3d83e9ea2f4aa4640623f580a87f3ac5 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 4 Sep 2018 15:52:44 -0300 Subject: [PATCH 51/62] Fix Cache.warn function to issue a "config" warning --- src/_pytest/cacheprovider.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index f27a045493b..791cf3a33aa 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -6,7 +6,6 @@ """ from __future__ import absolute_import, division, print_function from collections import OrderedDict -import warnings import py import six @@ -34,6 +33,7 @@ @attr.s class Cache(object): _cachedir = attr.ib(repr=False) + _config = attr.ib(repr=False) @classmethod def for_config(cls, config): @@ -41,17 +41,18 @@ def for_config(cls, config): if config.getoption("cacheclear") and cachedir.exists(): shutil.rmtree(str(cachedir)) cachedir.mkdir() - return cls(cachedir) + return cls(cachedir, config) @staticmethod def cache_dir_from_config(config): return paths.resolve_from_str(config.getini("cache_dir"), config.rootdir) def warn(self, fmt, **args): + from _pytest.warnings import _issue_config_warning from _pytest.warning_types import PytestWarning - warnings.warn( - message=fmt.format(**args) if args else fmt, category=PytestWarning + _issue_config_warning( + PytestWarning(fmt.format(**args) if args else fmt), self._config ) def makedir(self, name): From d3ca739c00d6560ccab84a3f94814094ba87b55a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 4 Sep 2018 16:29:48 -0300 Subject: [PATCH 52/62] Use explicit instances when calling warnings.warn_explicit --- src/_pytest/assertion/rewrite.py | 4 ++-- src/_pytest/config/__init__.py | 9 +++++---- src/_pytest/deprecated.py | 2 +- src/_pytest/fixtures.py | 6 ++++-- src/_pytest/python.py | 6 ++++-- testing/deprecated_test.py | 6 +++--- 6 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 8c7944593ae..1f485eb8940 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -755,8 +755,8 @@ def visit_Assert(self, assert_): import warnings warnings.warn_explicit( - "assertion is always true, perhaps remove parentheses?", - PytestWarning, + PytestWarning("assertion is always true, perhaps remove parentheses?"), + category=None, filename=str(self.module_path), lineno=assert_.lineno, ) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index dfee41bdf10..993e091f265 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -419,11 +419,9 @@ def _importconftest(self, conftestpath): PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST ) - from _pytest.warning_types import RemovedInPytest4Warning - warnings.warn_explicit( PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST, - RemovedInPytest4Warning, + category=None, filename=str(conftestpath), lineno=0, ) @@ -629,7 +627,10 @@ def warn(self, code, message, fslocation=None, nodeid=None): if nodeid: msg = "{}: {}".format(nodeid, msg) warnings.warn_explicit( - msg, RemovedInPytest4Warning, filename=filename, lineno=lineno + RemovedInPytest4Warning(msg), + category=None, + filename=filename, + lineno=lineno, ) self.hook.pytest_logwarning.call_historic( kwargs=dict( diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 5e2c589620d..dea8bbde867 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -72,7 +72,7 @@ "Please use Metafunc.parametrize instead." ) -PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST = ( +PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST = RemovedInPytest4Warning( "Defining pytest_plugins in a non-top-level conftest is deprecated, " "because it affects the entire directory tree in a non-explicit way.\n" "Please move it to the top level conftest file instead." diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index cbfda9a82c6..068e6814c1d 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1284,8 +1284,10 @@ def parsefactories(self, node_or_obj, nodeid=NOTSET, unittest=False): filename, lineno = getfslineno(obj) warnings.warn_explicit( - deprecated.FUNCARG_PREFIX.format(name=name), - RemovedInPytest4Warning, + RemovedInPytest4Warning( + deprecated.FUNCARG_PREFIX.format(name=name) + ), + category=None, filename=str(filename), lineno=lineno + 1, ) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 10a3b1ec321..3ce70064e83 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -241,8 +241,10 @@ def pytest_pycollect_makeitem(collector, name, obj): if not (isfunction(obj) or isfunction(get_real_func(obj))): filename, lineno = getfslineno(obj) warnings.warn_explicit( - message="cannot collect %r because it is not a function." % name, - category=PytestWarning, + message=PytestWarning( + "cannot collect %r because it is not a function." % name + ), + category=None, filename=str(filename), lineno=lineno + 1, ) diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index a74ce662d10..fbaca4e30fd 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -202,7 +202,7 @@ def test_func(): ) res = testdir.runpytest_subprocess() assert res.ret == 0 - msg = PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST.splitlines()[0] + msg = str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0] res.stdout.fnmatch_lines( "*subdirectory{sep}conftest.py:0: RemovedInPytest4Warning: {msg}*".format( sep=os.sep, msg=msg @@ -235,7 +235,7 @@ def test_func(): res = testdir.runpytest_subprocess() assert res.ret == 0 - msg = PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST.splitlines()[0] + msg = str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0] res.stdout.fnmatch_lines( "*subdirectory{sep}conftest.py:0: RemovedInPytest4Warning: {msg}*".format( sep=os.sep, msg=msg @@ -272,7 +272,7 @@ def test_func(): ) res = testdir.runpytest_subprocess() assert res.ret == 0 - msg = PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST.splitlines()[0] + msg = str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0] assert msg not in res.stdout.str() From b7560a88084cb7812059a616c5ec757b46bb45b0 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 4 Sep 2018 16:48:21 -0300 Subject: [PATCH 53/62] Keep backward compatibility for code as kw in Node.warn --- src/_pytest/nodes.py | 12 ++++++++++-- testing/test_config.py | 11 ++++++++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 53d22dc2277..f70ad6a54b7 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -136,7 +136,7 @@ def _getcustomclass(self, name): def __repr__(self): return "<%s %r>" % (self.__class__.__name__, getattr(self, "name", None)) - def warn(self, code_or_warning, message=None): + def warn(self, code_or_warning=None, message=None, code=None): """Issue a warning for this item. Warnings will be displayed after the test session, unless explicitly suppressed. @@ -164,12 +164,20 @@ def warn(self, code_or_warning, message=None): :param Union[Warning,str] code_or_warning: warning instance or warning code (legacy). :param Union[str,None] message: message to display when called in the legacy form. + :param str code: code for the warning, in legacy form when using keyword arguments. :return: """ if message is None: + if code_or_warning is None: + raise ValueError("code_or_warning must be given") self._std_warn(code_or_warning) else: - self._legacy_warn(code_or_warning, message) + if code_or_warning and code: + raise ValueError( + "code_or_warning and code cannot both be passed to this function" + ) + code = code_or_warning or code + self._legacy_warn(code, message) def _legacy_warn(self, code, message): """ diff --git a/testing/test_config.py b/testing/test_config.py index fac780a0542..8d67d7e9d19 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -809,18 +809,23 @@ def test_proper(pytestconfig): ) @pytest.mark.filterwarnings("default") - def test_warn_on_test_item_from_request(self, testdir, request): + @pytest.mark.parametrize("use_kw", [True, False]) + def test_warn_on_test_item_from_request(self, testdir, use_kw): + code_kw = "code=" if use_kw else "" + message_kw = "message=" if use_kw else "" testdir.makepyfile( """ import pytest @pytest.fixture def fix(request): - request.node.warn("T1", "hello") + request.node.warn({code_kw}"T1", {message_kw}"hello") def test_hello(fix): pass - """ + """.format( + code_kw=code_kw, message_kw=message_kw + ) ) result = testdir.runpytest("--disable-pytest-warnings") assert "hello" not in result.stdout.str() From 6d497f2c77a6aac70611bcdf0c968e23d166935e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 4 Sep 2018 16:50:24 -0300 Subject: [PATCH 54/62] Fix stacklevel for warning about Metafunc.addcall --- src/_pytest/python.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 3ce70064e83..9d6e2384025 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1107,8 +1107,7 @@ def addcall(self, funcargs=None, id=NOTSET, param=NOTSET): :arg param: a parameter which will be exposed to a later fixture function invocation through the ``request.param`` attribute. """ - if self.config: - self.definition.warn(deprecated.METAFUNC_ADD_CALL) + warnings.warn(deprecated.METAFUNC_ADD_CALL, stacklevel=2) assert funcargs is None or isinstance(funcargs, dict) if funcargs is not None: From 5a52acaa92b1c3d8dd04d1df7d9ebdcc9d9d397f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 4 Sep 2018 16:55:52 -0300 Subject: [PATCH 55/62] Make config no longer optional in parametrize id functions --- src/_pytest/python.py | 6 +++--- testing/python/metafunc.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 9d6e2384025..05165027231 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1158,7 +1158,7 @@ def _find_parametrized_scope(argnames, arg2fixturedefs, indirect): return "function" -def _idval(val, argname, idx, idfn, item, config=None): +def _idval(val, argname, idx, idfn, item, config): if idfn: s = None try: @@ -1195,7 +1195,7 @@ def _idval(val, argname, idx, idfn, item, config=None): return str(argname) + str(idx) -def _idvalset(idx, parameterset, argnames, idfn, ids, config=None, item=None): +def _idvalset(idx, parameterset, argnames, idfn, ids, item, config): if parameterset.id is not None: return parameterset.id if ids is None or (idx >= len(ids) or ids[idx] is None): @@ -1210,7 +1210,7 @@ def _idvalset(idx, parameterset, argnames, idfn, ids, config=None, item=None): def idmaker(argnames, parametersets, idfn=None, ids=None, config=None, item=None): ids = [ - _idvalset(valindex, parameterset, argnames, idfn, ids, config, item) + _idvalset(valindex, parameterset, argnames, idfn, ids, config=config, item=item) for valindex, parameterset in enumerate(parametersets) ] if len(set(ids)) != len(ids): diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index e1dfb6d8b33..b8cd68b1143 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -244,7 +244,7 @@ def test_unicode_idval(self): ), ] for val, expected in values: - assert _idval(val, "a", 6, None, item=None) == expected + assert _idval(val, "a", 6, None, item=None, config=None) == expected def test_bytes_idval(self): """unittest for the expected behavior to obtain ids for parametrized @@ -278,7 +278,7 @@ def test_function(): values = [(TestClass, "TestClass"), (test_function, "test_function")] for val, expected in values: - assert _idval(val, "a", 6, None, item=None) == expected + assert _idval(val, "a", 6, None, item=None, config=None) == expected @pytest.mark.issue250 def test_idmaker_autoname(self): From 2e0a7cf78dff53c534bf4aad2841ba8731051773 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 4 Sep 2018 17:01:23 -0300 Subject: [PATCH 56/62] Revert to having just "runtest" as "when" parameter of the pytest_warning_captured hook --- src/_pytest/hookspec.py | 7 ++----- src/_pytest/warnings.py | 20 ++------------------ testing/test_warnings.py | 6 +++--- 3 files changed, 7 insertions(+), 26 deletions(-) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 3e647d1a153..1a932614938 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -559,13 +559,10 @@ def pytest_warning_captured(warning_message, when, item): * ``"config"``: during pytest configuration/initialization stage. * ``"collect"``: during test collection. - * ``"setup"``: during test setup. - * ``"call"``: during test call. - * ``"teardown"``: during test teardown. + * ``"runtest"``: during test execution. :param pytest.Item|None item: - The item being executed if ``when`` is ``"setup"``, ``"call"`` or ``"teardown"``, otherwise - ``None``. + The item being executed if ``when`` is ``"runtest"``, otherwise ``None``. """ diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index e0206883a8f..770d6b2a675 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -130,25 +130,9 @@ def warning_record_to_str(warning_message): @pytest.hookimpl(hookwrapper=True, tryfirst=True) -def pytest_runtest_setup(item): +def pytest_runtest_protocol(item): with catch_warnings_for_item( - config=item.config, ihook=item.ihook, when="setup", item=item - ): - yield - - -@pytest.hookimpl(hookwrapper=True, tryfirst=True) -def pytest_runtest_call(item): - with catch_warnings_for_item( - config=item.config, ihook=item.ihook, when="call", item=item - ): - yield - - -@pytest.hookimpl(hookwrapper=True, tryfirst=True) -def pytest_runtest_teardown(item): - with catch_warnings_for_item( - config=item.config, ihook=item.ihook, when="teardown", item=item + config=item.config, ihook=item.ihook, when="runtest", item=item ): yield diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 119b67ceed1..11ca1e8f4bc 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -341,9 +341,9 @@ def pytest_warning_captured(self, warning_message, when, item): expected = [ ("config warning", "config", ""), ("collect warning", "collect", ""), - ("setup warning", "setup", "test_func"), - ("call warning", "call", "test_func"), - ("teardown warning", "teardown", "test_func"), + ("setup warning", "runtest", "test_func"), + ("call warning", "runtest", "test_func"), + ("teardown warning", "runtest", "test_func"), ] assert collected == expected From 4592def14d63aa32215bea548c53fc208a88fd10 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 4 Sep 2018 17:02:56 -0300 Subject: [PATCH 57/62] Improve test_rewarn_functional --- testing/test_recwarn.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index f81d278895e..82bd66c555c 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -7,7 +7,7 @@ def test_recwarn_functional(testdir): - reprec = testdir.inline_runsource( + testdir.makepyfile( """ import warnings def test_method(recwarn): @@ -16,8 +16,8 @@ def test_method(recwarn): assert isinstance(warn.message, UserWarning) """ ) - res = reprec.countoutcomes() - assert tuple(res) == (1, 0, 0), res + reprec = testdir.inline_run() + reprec.assertoutcome(passed=1) class TestWarningsRecorderChecker(object): From adc9ed85bcbfe3c3c499a7a2cf874583508213c1 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 4 Sep 2018 18:49:20 -0300 Subject: [PATCH 58/62] Fix test_idval_hypothesis --- testing/python/metafunc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index b8cd68b1143..36ef5041df0 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -217,7 +217,7 @@ class A(object): def test_idval_hypothesis(self, value): from _pytest.python import _idval - escaped = _idval(value, "a", 6, None, item=None) + escaped = _idval(value, "a", 6, None, item=None, config=None) assert isinstance(escaped, str) if PY3: escaped.encode("ascii") From f42b5019ec3a3c2c12bd5321641950118e812dd9 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 4 Sep 2018 18:53:58 -0300 Subject: [PATCH 59/62] Make code_or_warning parameter private for backward-compatibility --- src/_pytest/nodes.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index f70ad6a54b7..29d1f0a871b 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -136,7 +136,7 @@ def _getcustomclass(self, name): def __repr__(self): return "<%s %r>" % (self.__class__.__name__, getattr(self, "name", None)) - def warn(self, code_or_warning=None, message=None, code=None): + def warn(self, _code_or_warning=None, message=None, code=None): """Issue a warning for this item. Warnings will be displayed after the test session, unless explicitly suppressed. @@ -162,21 +162,25 @@ def warn(self, code_or_warning=None, message=None, code=None): node.warn("CI", "some message") - :param Union[Warning,str] code_or_warning: warning instance or warning code (legacy). + :param Union[Warning,str] _code_or_warning: + warning instance or warning code (legacy). This parameter receives an underscore for backward + compatibility with the legacy code/message form, and will be replaced for something + more usual when the legacy form is removed. + :param Union[str,None] message: message to display when called in the legacy form. :param str code: code for the warning, in legacy form when using keyword arguments. :return: """ if message is None: - if code_or_warning is None: + if _code_or_warning is None: raise ValueError("code_or_warning must be given") - self._std_warn(code_or_warning) + self._std_warn(_code_or_warning) else: - if code_or_warning and code: + if _code_or_warning and code: raise ValueError( "code_or_warning and code cannot both be passed to this function" ) - code = code_or_warning or code + code = _code_or_warning or code self._legacy_warn(code, message) def _legacy_warn(self, code, message): From ddb308455ae615d23bf3a494f84a5059c9ceb979 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 5 Sep 2018 09:01:29 -0300 Subject: [PATCH 60/62] Make sure warn is called in test_parameterset_extractfrom --- testing/test_mark.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/testing/test_mark.py b/testing/test_mark.py index f50902eb18e..9dad7a16509 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -1039,14 +1039,19 @@ def assert_test_is_not_selected(keyword): ), ], ) -@pytest.mark.filterwarnings("ignore") +@pytest.mark.filterwarnings("default") def test_parameterset_extractfrom(argval, expected): + from _pytest.deprecated import MARK_PARAMETERSET_UNPACKING + + warn_called = [] + class DummyItem: def warn(self, warning): - pass + warn_called.append(warning) extracted = ParameterSet.extract_from(argval, belonging_definition=DummyItem()) assert extracted == expected + assert warn_called == [MARK_PARAMETERSET_UNPACKING] def test_legacy_transfer(): From f63c683faa85c2a30b0bb2c584484d9b814a2018 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 5 Sep 2018 10:20:25 -0300 Subject: [PATCH 61/62] No longer escape regex in pytest.mark.filterwarnings Fix #3936 --- changelog/3936.removal.rst | 5 +++++ src/_pytest/warnings.py | 2 +- testing/test_warnings.py | 16 ++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 changelog/3936.removal.rst diff --git a/changelog/3936.removal.rst b/changelog/3936.removal.rst new file mode 100644 index 00000000000..bf0ba0897b1 --- /dev/null +++ b/changelog/3936.removal.rst @@ -0,0 +1,5 @@ +``@pytest.mark.filterwarnings`` second parameter is no longer regex-escaped, +making it possible to actually use regular expressions to check the warning message. + +**Note**: regex-escaping the match string was an implementation oversight that might break test suites which depend +on the old behavior. diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 770d6b2a675..6c4b921fa7d 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -81,7 +81,7 @@ def catch_warnings_for_item(config, ihook, when, item): if item is not None: for mark in item.iter_markers(name="filterwarnings"): for arg in mark.args: - warnings._setoption(arg) + _setoption(warnings, arg) filters_configured = True if not filters_configured: diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 11ca1e8f4bc..3f748d666d9 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -374,6 +374,22 @@ def test_foo(): ) +@pytest.mark.filterwarnings("always") +def test_mark_regex_escape(testdir): + """@pytest.mark.filterwarnings should not try to escape regex characters (#3936)""" + testdir.makepyfile( + r""" + import pytest, warnings + + @pytest.mark.filterwarnings(r"ignore:some \(warning\)") + def test_foo(): + warnings.warn(UserWarning("some (warning)")) + """ + ) + result = testdir.runpytest() + assert WARNINGS_SUMMARY_HEADER not in result.stdout.str() + + @pytest.mark.filterwarnings("default") @pytest.mark.parametrize("ignore_pytest_warnings", ["no", "ini", "cmdline"]) def test_hide_pytest_internal_warnings(testdir, ignore_pytest_warnings): From 1f2062661826efb5ac4322c17e24db202bf0a147 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 5 Sep 2018 21:06:32 +0000 Subject: [PATCH 62/62] Preparing release version 3.8.0 --- CHANGELOG.rst | 84 +++++++++++++++++++++++++++++++ changelog/2452.feature.rst | 5 -- changelog/2452.removal.rst | 12 ----- changelog/2908.feature.rst | 5 -- changelog/3566.doc.rst | 1 - changelog/3784.feature.rst | 1 - changelog/3829.feature.rst | 1 - changelog/3837.feature.rst | 1 - changelog/3853.trivial.rst | 1 - changelog/3907.doc.rst | 1 - changelog/3911.bugfix.rst | 1 - changelog/3913.bugfix.rst | 1 - changelog/3918.bugfix.rst | 1 - changelog/3936.removal.rst | 5 -- doc/en/announce/index.rst | 1 + doc/en/announce/release-3.8.0.rst | 38 ++++++++++++++ doc/en/example/reportingdemo.rst | 6 +-- doc/en/usage.rst | 19 +++---- doc/en/warnings.rst | 17 +++---- doc/en/writing_plugins.rst | 5 +- 20 files changed, 145 insertions(+), 61 deletions(-) delete mode 100644 changelog/2452.feature.rst delete mode 100644 changelog/2452.removal.rst delete mode 100644 changelog/2908.feature.rst delete mode 100644 changelog/3566.doc.rst delete mode 100644 changelog/3784.feature.rst delete mode 100644 changelog/3829.feature.rst delete mode 100644 changelog/3837.feature.rst delete mode 100644 changelog/3853.trivial.rst delete mode 100644 changelog/3907.doc.rst delete mode 100644 changelog/3911.bugfix.rst delete mode 100644 changelog/3913.bugfix.rst delete mode 100644 changelog/3918.bugfix.rst delete mode 100644 changelog/3936.removal.rst create mode 100644 doc/en/announce/release-3.8.0.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 78f2156e803..7a0de069c56 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,90 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 3.8.0 (2018-09-05) +========================= + +Deprecations and Removals +------------------------- + +- `#2452 `_: ``Config.warn`` has been deprecated, it should be replaced by calls to the standard ``warnings.warn``. + + ``Node.warn`` now supports two signatures: + + * ``node.warn(PytestWarning("some message"))``: is now the recommended way to call this function. The warning + instance must be a ``PytestWarning`` or subclass instance. + + * ``node.warn("CI", "some message")``: this code/message form is now deprecated and should be converted to + the warning instance form above. + + ``RemovedInPytest4Warning`` and ``PytestExperimentalApiWarning`` are now part of the public API and should be accessed + using ``pytest.RemovedInPytest4Warning`` and ``pytest.PytestExperimentalApiWarning``. + + +- `#3936 `_: ``@pytest.mark.filterwarnings`` second parameter is no longer regex-escaped, + making it possible to actually use regular expressions to check the warning message. + + **Note**: regex-escaping the match string was an implementation oversight that might break test suites which depend + on the old behavior. + + + +Features +-------- + +- `#2452 `_: Internal pytest warnings are now issued using the standard ``warnings`` module, making it possible to use + the standard warnings filters to manage those warnings. This introduces ``PytestWarning``, + ``PytestDeprecationWarning`` and ``RemovedInPytest4Warning`` warning types as part of the public API. + + Consult `the documentation `_ for more info. + + +- `#2908 `_: ``DeprecationWarning`` and ``PendingDeprecationWarning`` are now shown by default if no other warning filter is + configured. This makes pytest more compliant with + `PEP-0506 `_. See + `the docs `_ for + more info. + + +- `#3784 `_: Add option to disable plugin auto-loading. + + +- `#3829 `_: Added the ``count`` option to ``console_output_style`` to enable displaying the progress as a count instead of a percentage. + + +- `#3837 `_: Added support for 'xfailed' and 'xpassed' outcomes to the ``pytester.RunResult.assert_outcomes`` signature. + + + +Bug Fixes +--------- + +- `#3911 `_: Terminal writer now takes into account unicode character width when writing out progress. + + +- `#3913 `_: Pytest now returns with correct exit code (EXIT_USAGEERROR, 4) when called with unknown arguments. + + +- `#3918 `_: Improve performance of assertion rewriting. + + + +Improved Documentation +---------------------- + +- `#3566 `_: Added a blurb in usage.rst for the usage of -r flag which is used to show an extra test summary info. + + +- `#3907 `_: Corrected type of the exceptions collection passed to ``xfail``: ``raises`` argument accepts a ``tuple`` instead of ``list``. + + + +Trivial/Internal Changes +------------------------ + +- `#3853 `_: Removed ``"run all (no recorded failures)"`` message printed with ``--failed-first`` and ``--last-failed`` when there are no failed tests. + + pytest 3.7.4 (2018-08-29) ========================= diff --git a/changelog/2452.feature.rst b/changelog/2452.feature.rst deleted file mode 100644 index 847e9540f72..00000000000 --- a/changelog/2452.feature.rst +++ /dev/null @@ -1,5 +0,0 @@ -Internal pytest warnings are now issued using the standard ``warnings`` module, making it possible to use -the standard warnings filters to manage those warnings. This introduces ``PytestWarning``, -``PytestDeprecationWarning`` and ``RemovedInPytest4Warning`` warning types as part of the public API. - -Consult `the documentation `_ for more info. diff --git a/changelog/2452.removal.rst b/changelog/2452.removal.rst deleted file mode 100644 index c2a02830389..00000000000 --- a/changelog/2452.removal.rst +++ /dev/null @@ -1,12 +0,0 @@ -``Config.warn`` has been deprecated, it should be replaced by calls to the standard ``warnings.warn``. - -``Node.warn`` now supports two signatures: - -* ``node.warn(PytestWarning("some message"))``: is now the recommended way to call this function. The warning - instance must be a ``PytestWarning`` or subclass instance. - -* ``node.warn("CI", "some message")``: this code/message form is now deprecated and should be converted to - the warning instance form above. - -``RemovedInPytest4Warning`` and ``PytestExperimentalApiWarning`` are now part of the public API and should be accessed -using ``pytest.RemovedInPytest4Warning`` and ``pytest.PytestExperimentalApiWarning``. diff --git a/changelog/2908.feature.rst b/changelog/2908.feature.rst deleted file mode 100644 index e904a98de82..00000000000 --- a/changelog/2908.feature.rst +++ /dev/null @@ -1,5 +0,0 @@ -``DeprecationWarning`` and ``PendingDeprecationWarning`` are now shown by default if no other warning filter is -configured. This makes pytest more compliant with -`PEP-0506 `_. See -`the docs `_ for -more info. diff --git a/changelog/3566.doc.rst b/changelog/3566.doc.rst deleted file mode 100644 index d8eda4241c6..00000000000 --- a/changelog/3566.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Added a blurb in usage.rst for the usage of -r flag which is used to show an extra test summary info. diff --git a/changelog/3784.feature.rst b/changelog/3784.feature.rst deleted file mode 100644 index 87b6cafb6a1..00000000000 --- a/changelog/3784.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Add option to disable plugin auto-loading. diff --git a/changelog/3829.feature.rst b/changelog/3829.feature.rst deleted file mode 100644 index d3bfdb8e67e..00000000000 --- a/changelog/3829.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Added the ``count`` option to ``console_output_style`` to enable displaying the progress as a count instead of a percentage. diff --git a/changelog/3837.feature.rst b/changelog/3837.feature.rst deleted file mode 100644 index 707c3b0da30..00000000000 --- a/changelog/3837.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Added support for 'xfailed' and 'xpassed' outcomes to the ``pytester.RunResult.assert_outcomes`` signature. diff --git a/changelog/3853.trivial.rst b/changelog/3853.trivial.rst deleted file mode 100644 index 252d4604349..00000000000 --- a/changelog/3853.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Removed ``"run all (no recorded failures)"`` message printed with ``--failed-first`` and ``--last-failed`` when there are no failed tests. diff --git a/changelog/3907.doc.rst b/changelog/3907.doc.rst deleted file mode 100644 index c556344f4b7..00000000000 --- a/changelog/3907.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Corrected type of the exceptions collection passed to ``xfail``: ``raises`` argument accepts a ``tuple`` instead of ``list``. diff --git a/changelog/3911.bugfix.rst b/changelog/3911.bugfix.rst deleted file mode 100644 index 8839fe7d9d8..00000000000 --- a/changelog/3911.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Terminal writer now takes into account unicode character width when writing out progress. diff --git a/changelog/3913.bugfix.rst b/changelog/3913.bugfix.rst deleted file mode 100644 index 33ed4ce28f0..00000000000 --- a/changelog/3913.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Pytest now returns with correct exit code (EXIT_USAGEERROR, 4) when called with unknown arguments. diff --git a/changelog/3918.bugfix.rst b/changelog/3918.bugfix.rst deleted file mode 100644 index 7ba81191616..00000000000 --- a/changelog/3918.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Improve performance of assertion rewriting. diff --git a/changelog/3936.removal.rst b/changelog/3936.removal.rst deleted file mode 100644 index bf0ba0897b1..00000000000 --- a/changelog/3936.removal.rst +++ /dev/null @@ -1,5 +0,0 @@ -``@pytest.mark.filterwarnings`` second parameter is no longer regex-escaped, -making it possible to actually use regular expressions to check the warning message. - -**Note**: regex-escaping the match string was an implementation oversight that might break test suites which depend -on the old behavior. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index f4814ac7d1a..1eaae502a17 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-3.8.0 release-3.7.4 release-3.7.3 release-3.7.2 diff --git a/doc/en/announce/release-3.8.0.rst b/doc/en/announce/release-3.8.0.rst new file mode 100644 index 00000000000..1fc344ea23e --- /dev/null +++ b/doc/en/announce/release-3.8.0.rst @@ -0,0 +1,38 @@ +pytest-3.8.0 +======================================= + +The pytest team is proud to announce the 3.8.0 release! + +pytest is a mature Python testing tool with more than a 2000 tests +against itself, passing on many different interpreters and platforms. + +This release contains a number of bugs fixes and improvements, so users are encouraged +to take a look at the CHANGELOG: + + https://docs.pytest.org/en/latest/changelog.html + +For complete documentation, please visit: + + https://docs.pytest.org/en/latest/ + +As usual, you can upgrade from pypi via: + + pip install -U pytest + +Thanks to all who contributed to this release, among them: + +* Anthony Sottile +* Bruno Oliveira +* CrazyMerlyn +* Daniel Hahler +* Fabio Zadrozny +* Jeffrey Rackauckas +* Ronny Pfannschmidt +* Virgil Dupras +* dhirensr +* hoefling +* wim glenn + + +Happy testing, +The Pytest Development Team diff --git a/doc/en/example/reportingdemo.rst b/doc/en/example/reportingdemo.rst index 61891eebd8b..a411aa49a97 100644 --- a/doc/en/example/reportingdemo.rst +++ b/doc/en/example/reportingdemo.rst @@ -613,9 +613,9 @@ get on the terminal - we are working on that):: failure_demo.py:261: AssertionError ============================= warnings summary ============================= - - Metafunc.addcall is deprecated and scheduled to be removed in pytest 4.0. - Please use Metafunc.parametrize instead. + $REGENDOC_TMPDIR/assertion/failure_demo.py:24: RemovedInPytest4Warning: Metafunc.addcall is deprecated and scheduled to be removed in pytest 4.0. + Please use Metafunc.parametrize instead. + metafunc.addcall(funcargs=dict(param1=3, param2=6)) -- Docs: https://docs.pytest.org/en/latest/warnings.html ================== 42 failed, 1 warnings in 0.12 seconds =================== diff --git a/doc/en/usage.rst b/doc/en/usage.rst index f1f0c079e7b..4da7861017a 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -153,15 +153,12 @@ making it easy in large test suites to get a clear picture of all failures, skip Example:: $ pytest -ra - ======================== test session starts ======================== - ... - ====================== short test summary info ====================== - FAIL summary\test_foo.py::test_1 - SKIP [1] summary\test_foo.py:12: not supported in this platform - XPASS summary\test_bar.py::test_4 flaky - - ===== 1 failed, 1 passed, 1 skipped, 1 xpassed in 0.08 seconds ====== + =========================== test session starts ============================ + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y + rootdir: $REGENDOC_TMPDIR, inifile: + collected 0 items + ======================= no tests ran in 0.12 seconds ======================= The ``-r`` options accepts a number of characters after it, with ``a`` used above meaning "all except passes". @@ -179,8 +176,12 @@ Here is the full list of available characters that can be used: More than one character can be used, so for example to only see failed and skipped tests, you can execute:: $ pytest -rfs + =========================== test session starts ============================ + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y + rootdir: $REGENDOC_TMPDIR, inifile: + collected 0 items - + ======================= no tests ran in 0.12 seconds ======================= .. _pdb-option: diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index a460ac121a4..1f0c3bf97a6 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -29,9 +29,8 @@ Running pytest now produces this output:: test_show_warnings.py . [100%] ============================= warnings summary ============================= - test_show_warnings.py::test_one - $REGENDOC_TMPDIR/test_show_warnings.py:4: UserWarning: api v1, should use functions from v2 - warnings.warn(UserWarning("api v1, should use functions from v2")) + $REGENDOC_TMPDIR/test_show_warnings.py:4: UserWarning: api v1, should use functions from v2 + warnings.warn(UserWarning("api v1, should use functions from v2")) -- Docs: https://docs.pytest.org/en/latest/warnings.html =================== 1 passed, 1 warnings in 0.12 seconds =================== @@ -354,15 +353,13 @@ defines an ``__init__`` constructor, as this prevents the class from being insta :: $ pytest test_pytest_warnings.py -q - ======================================== warnings summary ========================================= - test_pytest_warnings.py:1 - $REGENDOC_TMPDIR/test_pytest_warnings.py:1: PytestWarning: cannot collect test class 'Test' because it has a __init__ constructor - class Test: - - -- Docs: http://doc.pytest.org/en/latest/warnings.html - 1 warnings in 0.01 seconds + ============================= warnings summary ============================= + $REGENDOC_TMPDIR/test_pytest_warnings.py:1: PytestWarning: cannot collect test class 'Test' because it has a __init__ constructor + class Test: + -- Docs: https://docs.pytest.org/en/latest/warnings.html + 1 warnings in 0.12 seconds These warnings might be filtered using the same builtin mechanisms used to filter other types of warnings. diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index 03bad31be40..70e48f81791 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -418,9 +418,8 @@ additionally it is possible to copy examples for a example folder before running test_example.py .. [100%] ============================= warnings summary ============================= - test_example.py::test_plugin - $REGENDOC_TMPDIR/test_example.py:4: PytestExperimentalApiWarning: testdir.copy_example is an experimental api that may change over time - testdir.copy_example("test_example.py") + $REGENDOC_TMPDIR/test_example.py:4: PytestExperimentalApiWarning: testdir.copy_example is an experimental api that may change over time + testdir.copy_example("test_example.py") -- Docs: https://docs.pytest.org/en/latest/warnings.html =================== 2 passed, 1 warnings in 0.12 seconds ===================