From 10f21b423a5676311974d0870af841ebe344d340 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 25 Aug 2018 20:31:18 -0300 Subject: [PATCH 01/44] 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 5c412047c..f88244468 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -525,7 +525,6 @@ class Testdir(object): 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 02/44] 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 e2969110a..246a59d59 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 3a93f92f3..7600840ea 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 a26fb4597..d0e5bbe4b 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -302,3 +302,22 @@ def test_filterwarnings_mark_registration(testdir): ) 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 03/44] 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 246a59d59..001f59b86 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 49c30e903..c8b0f64b7 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -138,9 +138,7 @@ class Node(object): """ 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 @@ class Node(object): 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 cc83959fd..14549ddf7 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -9,6 +9,7 @@ import platform 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 @@ class TerminalReporter(object): 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 7600840ea..7c772f7c4 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): +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. """ - Emits the deprecated ``pytest_logwarning`` for the given warning and item. - """ - 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 d0e5bbe4b..efb974905 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 04/44] 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 f175394a8..a4c3c3252 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -349,11 +349,6 @@ class PyCollector(PyobjMixin, nodes.Collector): 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 05/44] 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 000000000..3ade3093d --- /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 14549ddf7..41b0e755c 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -339,8 +339,9 @@ class TerminalReporter(object): 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 @@ class TerminalReporter(object): 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 7c772f7c4..6562d11a3 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 efb974905..3bd7bb52e 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -321,3 +321,28 @@ def test_warning_captured_hook(testdir, pyfile_with_warnings): (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 06/44] 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 20f1cc25b..237991c56 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -7,10 +7,7 @@ be removed when the time comes. """ 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 c8b0f64b7..098136df0 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 attr import _pytest import _pytest._code +from _pytest.compat import getfslineno from _pytest.mark.structures import NodeKeywords, MarkInfo @@ -145,6 +147,14 @@ class Node(object): ) ) + 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 a4c3c3252..9de0dc0ec 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -44,7 +44,7 @@ from _pytest.mark.structures import ( 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 @@ class Class(PyCollector): 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 41b0e755c..5140741a3 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -331,12 +331,11 @@ class TerminalReporter(object): 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 @@ class TerminalReporter(object): 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 000000000..2b86dd289 --- /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 e47981aca..ae41fb1b8 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -16,7 +16,7 @@ from _pytest.mark import ( 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 07/44] 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 9bd89c3c3..8700bd82d 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -65,7 +65,7 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")): 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 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")): 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 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")): 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 098136df0..e0291a088 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 9de0dc0ec..f20bd582f 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -967,7 +967,11 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): 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 75d82ecde..3dc422efe 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -18,7 +18,9 @@ from _pytest.capture import CaptureManager 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 @@ class TestCaptureManager(object): 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 08/44] 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 2c4361407..be412afd3 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -417,7 +417,14 @@ class PytestPluginManager(PluginManager): 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 237991c56..a77ebf6c8 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -63,7 +63,7 @@ METAFUNC_ADD_CALL = ( "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 8700bd82d..0e0ba96e5 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -94,7 +94,7 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")): 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 f20bd582f..4a7faff07 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -44,7 +44,7 @@ from _pytest.mark.structures import ( 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 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): 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 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): 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 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): 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 2b86dd289..be06f39c9 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 6562d11a3..d043e64f7 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 966de66b2..15e66d718 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_pytest_plugins_in_non_top_level_conftest_deprecated(testdir): ) 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_pytest_plugins_in_non_top_level_conftest_deprecated_no_top_level_confte 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_pytest_plugins_in_non_top_level_conftest_deprecated_no_false_positives( ) 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 test_call_fixture_function_deprecated(): 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 f5d839f08..608cd52d4 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -383,44 +383,7 @@ class TestMetafunc(object): ) 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 @@ class TestMetafunc(object): 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 b0c11f0b0..000000000 --- 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 02e2824d9..cca704c4c 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -1047,20 +1047,21 @@ def test_terminal_summary(testdir): ) +@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 09/44] 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 a48a931ac..9c622213c 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -209,8 +209,11 @@ class AssertionRewritingHook(object): 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 @@ class AssertionRewriter(ast.NodeVisitor): 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 7cad246c8..dbe953406 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -33,7 +33,6 @@ See [the docs](https://docs.pytest.org/en/latest/cache.html) for more informatio @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 @@ class Cache(object): 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 be412afd3..f0aecbe55 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 @@ class Config(object): 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 @@ class Config(object): 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 234aa69c7..e10c455b1 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_dirs_from_args(args): 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 bfbf7bb54..476acab02 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1281,8 +1281,12 @@ class FixtureManager(object): 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 86aad69bb..2f34970a1 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 e0291a088..3bb10ee89 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -137,8 +137,20 @@ class Node(object): 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 @@ class Node(object): ) 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 4a7faff07..5a531766b 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -44,7 +44,11 @@ from _pytest.mark.structures import ( 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 @@ class Generator(FunctionMixin, PyCollector): ) 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 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): 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 0ad31b8bc..308abd251 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 428ac464c..6b374083f 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -526,7 +526,7 @@ class TestInvocationVariants(object): 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 15e66d718..70c6df63f 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -5,6 +5,7 @@ import os import pytest +@pytest.mark.filterwarnings("default") def test_yield_tests_deprecation(testdir): testdir.makepyfile( """ @@ -18,16 +19,18 @@ def test_yield_tests_deprecation(testdir): 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_deprecation(testdir): 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 8f4283e40..b5475f03f 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -462,6 +462,7 @@ class TestFunction(object): 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 @@ class TestFunction(object): 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 test_collect_functools_partial(testdir): 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_dont_collect_non_function_callable(testdir): 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 608cd52d4..5d9282435 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -407,8 +407,8 @@ class TestMetafunc(object): "", " ", " ", - "*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 a9e624713..2c7f4b33d 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -1075,6 +1075,7 @@ def test_diff_newline_at_end(monkeypatch, testdir): ) +@pytest.mark.filterwarnings("default") def test_assert_tuple_warning(testdir): testdir.makepyfile( """ @@ -1084,7 +1085,7 @@ def test_assert_tuple_warning(testdir): ) 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 c436ab0de..fdbaa9e90 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -759,16 +759,12 @@ def test_rewritten(): 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 = [] + from _pytest.warning_types import PytestWarning - def mywarn(code, msg): - warnings.append((code, msg)) - - 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 23ec73599..c9d174229 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -31,6 +31,7 @@ class TestNewAPI(object): 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 ad7f35b57..c0630d688 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -135,13 +135,13 @@ class TestConfigCmdlineParsing(object): """ ) 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 @@ class TestConfigCmdlineParsing(object): 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 @@ class TestConfigCmdlineParsing(object): ) 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 @@ def test_collect_pytest_prefix_bug(pytestconfig): 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 @@ class TestWarning(object): 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 @@ class TestWarning(object): """ ) 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 @@ class TestWarning(object): ===*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 @@ class TestRootdir(object): @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 @@ class TestRootdir(object): 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 0678d59e8..04b4ee2d7 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -1005,6 +1005,7 @@ def test_record_property_same_name(testdir): 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_attribute(testdir): 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 173384ffb..1bb0cca48 100644 --- a/testing/test_resultlog.py +++ b/testing/test_resultlog.py @@ -13,6 +13,9 @@ from _pytest.resultlog import ( ) +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 3bd7bb52e..eb7033f1d 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -37,7 +37,7 @@ def pyfile_with_warnings(testdir, request): ) -@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 fbc5d4779..d1e251e99 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 10/44] 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 | 58 ++++++++++++++++++---------------- testing/test_assertrewrite.py | 3 +- testing/test_mark.py | 2 +- tox.ini | 6 ++-- 7 files changed, 40 insertions(+), 47 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index f0aecbe55..cec56e800 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 5a531766b..9ac216332 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -44,11 +44,7 @@ from _pytest.mark.structures import ( 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 @@ class Class(PyCollector): 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 be06f39c9..a98732ee3 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 ae542b76d..bf6a9416f 100644 --- a/src/pytest.py +++ b/src/pytest.py @@ -19,45 +19,47 @@ from _pytest.main import Session 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", - "deprecated_call", - "fixture", - "yield_fixture", - "fail", - "skip", - "xfail", - "importorskip", - "exit", - "mark", - "param", - "approx", "_fillfuncargs", - "Item", - "File", - "Collector", - "Package", - "Session", - "Module", + "approx", "Class", - "Instance", + "cmdline", + "Collector", + "deprecated_call", + "exit", + "fail", + "File", + "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 fdbaa9e90..c82e1dccf 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -761,9 +761,8 @@ def test_rewritten(): 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 ae41fb1b8..12aded416 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -16,7 +16,7 @@ from _pytest.mark import ( 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 d1e251e99..4b5eae066 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 11/44] 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 d1c927dd0..a435324b6 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 a98732ee3..092a5a430 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 bf6a9416f..c5a066662 100644 --- a/src/pytest.py +++ b/src/pytest.py @@ -20,7 +20,11 @@ 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 +from _pytest.warning_types import ( + PytestWarning, + PytestDeprecationWarning, + RemovedInPytest4Warning, +) set_trace = __pytestPDB.set_trace @@ -50,6 +54,7 @@ __all__ = [ "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 12/44] 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 000000000..847e9540f --- /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 000000000..3c60f8803 --- /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 13/44] 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 000000000..957fc30f0 --- /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 a435324b6..ed73a69e8 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 d043e64f7..952c4a0be 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 b5475f03f..c92de12a0 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -8,10 +8,6 @@ import pytest 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 @@ class TestGenerator(object): 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 @@ class TestFunction(object): 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 @@ class TestFunction(object): 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 @@ class TestFunction(object): 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 @@ class TestFunction(object): 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 @@ class TestFunction(object): 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 @@ class TestFunction(object): 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 @@ class TestFunction(object): 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 @@ class TestFunction(object): 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 eb7033f1d..73813fa0b 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -326,6 +326,7 @@ def test_warning_captured_hook(testdir, pyfile_with_warnings): @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_collection_warnings(testdir): "* 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 14/44] 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 73813fa0b..f0a172196 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -349,6 +349,46 @@ def test_collection_warnings(testdir): ) +@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 15/44] 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 e19b5ae87..52d83cf6e 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 cec56e800..e1f126af0 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 @@ class Config(object): 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 e10c455b1..7480603be 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_dirs_from_args(args): 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 001f59b86..dac36b306 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 308abd251..8a972eed7 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 952c4a0be..986343fd3 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 70c6df63f..ec53bf7eb 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -54,9 +54,6 @@ def test_funcarg_prefix_deprecation(testdir): @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 16/44] 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 9c622213c..4f16750b2 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -209,11 +209,12 @@ class AssertionRewritingHook(object): 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 a77ebf6c8..82bf1d98d 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -20,7 +20,7 @@ FUNCARG_PREFIX = ( ) 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 @@ CFG_PYTEST_SECTION = ( "[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 2f34970a1..776cd935c 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -258,12 +258,11 @@ def record_property(request): @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 f88244468..002eb62a5 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -126,7 +126,7 @@ class LsofFdLeakChecker(object): 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 5140741a3..4f6f09537 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 @@ class WarningReport(object): 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 @@ class TerminalReporter(object): 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 @@ class TerminalReporter(object): 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 c82e1dccf..c4cafcaab 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -759,11 +759,16 @@ def test_rewritten(): 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 04b4ee2d7..3928548a8 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -1024,10 +1024,7 @@ def test_record_attribute(testdir): 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 f0a172196..eb4928a46 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -40,14 +40,12 @@ def pyfile_with_warnings(testdir, request): @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_collection_warnings(testdir): [ "*== %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 17/44] 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 957fc30f0..e904a98de 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 ed73a69e8..eac997308 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 18/44] 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 eac997308..6db100fd7 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 aa6b66446..000000000 --- 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 002eb62a5..ea0ccf136 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -642,10 +642,12 @@ class Testdir(object): 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 092a5a430..407cfb100 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 c5a066662..e29e6116e 100644 --- a/src/pytest.py +++ b/src/pytest.py @@ -24,6 +24,7 @@ from _pytest.warning_types import ( PytestWarning, PytestDeprecationWarning, RemovedInPytest4Warning, + PytestExerimentalApiWarning, ) set_trace = __pytestPDB.set_trace @@ -53,8 +54,9 @@ __all__ = [ "Module", "Package", "param", - "PytestWarning", "PytestDeprecationWarning", + "PytestExerimentalApiWarning", + "PytestWarning", "raises", "register_assert_rewrite", "RemovedInPytest4Warning", diff --git a/tox.ini b/tox.ini index 4b5eae066..a071b5bf4 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 19/44] 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 3c60f8803..2a2e3f810 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 6db100fd7..89d064f16 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 27e13d932..03bad31be 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 ea0ccf136..4140dfc50 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -645,7 +645,7 @@ class Testdir(object): 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 407cfb100..4422363b1 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 e29e6116e..e173fd3d4 100644 --- a/src/pytest.py +++ b/src/pytest.py @@ -24,7 +24,7 @@ from _pytest.warning_types import ( PytestWarning, PytestDeprecationWarning, RemovedInPytest4Warning, - PytestExerimentalApiWarning, + PytestExperimentalApiWarning, ) set_trace = __pytestPDB.set_trace @@ -55,7 +55,7 @@ __all__ = [ "Package", "param", "PytestDeprecationWarning", - "PytestExerimentalApiWarning", + "PytestExperimentalApiWarning", "PytestWarning", "raises", "register_assert_rewrite", diff --git a/tox.ini b/tox.ini index a071b5bf4..0b6e89aa6 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 20/44] 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 9ac216332..0fb7fb732 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -659,11 +659,6 @@ class Class(PyCollector): 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 21/44] 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 986343fd3..0b67bd8f1 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 22/44] 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 3bb10ee89..e9ee74b1a 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 23/44] 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 4f16750b2..5859dd509 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -755,7 +755,7 @@ class AssertionRewriter(ast.NodeVisitor): 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 24/44] 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 5859dd509..1868f0f7a 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -750,7 +750,7 @@ class AssertionRewriter(ast.NodeVisitor): 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 2c7f4b33d..6a2a1ed38 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -1077,16 +1077,25 @@ def test_diff_newline_at_end(monkeypatch, testdir): @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 25/44] 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 dbe953406..f27a04549 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -6,6 +6,7 @@ ignores the external pytest-cache """ from __future__ import absolute_import, division, print_function from collections import OrderedDict +import warnings import py import six @@ -47,7 +48,6 @@ class Cache(object): 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 26/44] 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 e1f126af0..dfee41bdf 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 82bf1d98d..d7a07503d 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -9,9 +9,14 @@ from __future__ import absolute_import, division, print_function 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 @@ MARK_PARAMETERSET_UNPACKING = RemovedInPytest4Warning( "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 @@ COLLECTOR_MAKEITEM = RemovedInPytest4Warning( "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 476acab02..cbfda9a82 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1257,6 +1257,8 @@ class FixtureManager(object): 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 @@ class FixtureManager(object): 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 776cd935c..e2579860b 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 0e0ba96e5..52a780ead 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -65,7 +65,7 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")): 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 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")): 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 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")): 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 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")): 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 e9ee74b1a..7a2b48ce2 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -144,12 +144,9 @@ class Node(object): 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 @@ class Node(object): ) ) - 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 4140dfc50..62127651a 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -126,7 +126,7 @@ class LsofFdLeakChecker(object): 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 @@ class Testdir(object): 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 0fb7fb732..fa14b7a33 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -660,9 +660,10 @@ class Class(PyCollector): 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 @@ class Generator(FunctionMixin, PyCollector): ) 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 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): 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 4422363b1..8861f6f2b 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -37,3 +37,6 @@ class PytestExperimentalApiWarning(PytestWarning, FutureWarning): apiname=apiname ) ) + + +PYTESTER_COPY_EXAMPLE = PytestExperimentalApiWarning.simple("testdir.copy_example") diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 0b67bd8f1..2d73def0f 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 6b374083f..428ac464c 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -526,7 +526,7 @@ class TestInvocationVariants(object): 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 ec53bf7eb..a74ce662d 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -289,6 +289,7 @@ def test_call_fixture_function_deprecated(): 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 test_pycollector_makeitem_is_deprecated(): 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 5d9282435..e1dfb6d8b 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -217,7 +217,7 @@ class TestMetafunc(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 @@ class TestMetafunc(object): ), ] 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 @@ class TestMetafunc(object): (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 @@ class TestMetafunc(object): 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 12aded416..0a6567521 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -1041,7 +1041,11 @@ class TestKeywordSelection(object): ) @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 eee3ac8e9..d62d7d78a 100644 --- a/testing/test_nodes.py +++ b/testing/test_nodes.py @@ -19,3 +19,14 @@ from _pytest import nodes 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 27/44] 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 2d73def0f..4ef4e7f0e 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 28/44] 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 89d064f16..a460ac121 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 29/44] 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 c9d174229..1048994d3 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -40,6 +40,7 @@ class TestNewAPI(object): 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 @@ class TestNewAPI(object): 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 30/44] 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 1868f0f7a..8c7944593 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -750,7 +750,7 @@ class AssertionRewriter(ast.NodeVisitor): 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 31/44] 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 1048994d3..6d425f95b 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -40,7 +40,9 @@ class TestNewAPI(object): 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 32/44] 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 2a2e3f810..c2a028303 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 d7a07503d..5e2c58962 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -54,7 +54,7 @@ MARK_PARAMETERSET_UNPACKING = RemovedInPytest4Warning( ) 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 e2579860b..7fa49bc28 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 52a780ead..8e8937d59 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -95,7 +95,7 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")): 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 7a2b48ce2..53d22dc22 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -136,7 +136,42 @@ class Node(object): 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 @@ class Node(object): """ 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 @@ class Node(object): ) ) - 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 @@ class Node(object): ) 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 62127651a..a50999172 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -126,7 +126,7 @@ class LsofFdLeakChecker(object): 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 fa14b7a33..10a3b1ec3 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -659,7 +659,7 @@ class Class(PyCollector): 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 @@ class Class(PyCollector): ) 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 @@ class Generator(FunctionMixin, PyCollector): ) 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 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): 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 c0630d688..fac780a05 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -831,7 +831,7 @@ class TestLegacyWarning(object): ===*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 0a6567521..f50902eb1 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -1042,7 +1042,7 @@ class TestKeywordSelection(object): @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 d62d7d78a..9219f45e5 100644 --- a/testing/test_nodes.py +++ b/testing/test_nodes.py @@ -29,4 +29,4 @@ def test_std_warn_not_pytestwarning(testdir): """ ) 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 33/44] 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 dac36b306..3e647d1a1 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 4ef4e7f0e..e0206883a 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 eb4928a46..119b67cee 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -302,20 +302,48 @@ def test_filterwarnings_mark_registration(testdir): @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 34/44] 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 f27a04549..791cf3a33 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -6,7 +6,6 @@ ignores the external pytest-cache """ from __future__ import absolute_import, division, print_function from collections import OrderedDict -import warnings import py import six @@ -34,6 +33,7 @@ See [the docs](https://docs.pytest.org/en/latest/cache.html) for more informatio @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 @@ class Cache(object): 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 35/44] 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 8c7944593..1f485eb89 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -755,8 +755,8 @@ class AssertionRewriter(ast.NodeVisitor): 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 dfee41bdf..993e091f2 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -419,11 +419,9 @@ class PytestPluginManager(PluginManager): 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 @@ class Config(object): 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 5e2c58962..dea8bbde8 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -72,7 +72,7 @@ METAFUNC_ADD_CALL = RemovedInPytest4Warning( "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 cbfda9a82..068e6814c 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1284,8 +1284,10 @@ class FixtureManager(object): 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 10a3b1ec3..3ce70064e 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 a74ce662d..fbaca4e30 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -202,7 +202,7 @@ def test_pytest_plugins_in_non_top_level_conftest_deprecated(testdir): ) 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_pytest_plugins_in_non_top_level_conftest_deprecated_no_top_level_confte 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_pytest_plugins_in_non_top_level_conftest_deprecated_no_false_positives( ) 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 36/44] 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 53d22dc22..f70ad6a54 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -136,7 +136,7 @@ class Node(object): 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 @@ class Node(object): :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 fac780a05..8d67d7e9d 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -809,18 +809,23 @@ class TestLegacyWarning(object): ) @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 37/44] 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 3ce70064e..9d6e23840 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1107,8 +1107,7 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): :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 38/44] 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 9d6e23840..051650272 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 e1dfb6d8b..b8cd68b11 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -244,7 +244,7 @@ class TestMetafunc(object): ), ] 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 @@ class TestMetafunc(object): 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 39/44] 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 3e647d1a1..1a9326149 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 e0206883a..770d6b2a6 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 119b67cee..11ca1e8f4 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -341,9 +341,9 @@ def test_warning_captured_hook(testdir): 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 40/44] 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 f81d27889..82bd66c55 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -7,7 +7,7 @@ from _pytest.recwarn import WarningsRecorder def test_recwarn_functional(testdir): - reprec = testdir.inline_runsource( + testdir.makepyfile( """ import warnings def test_method(recwarn): @@ -16,8 +16,8 @@ def test_recwarn_functional(testdir): 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 41/44] 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 b8cd68b11..36ef5041d 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -217,7 +217,7 @@ class TestMetafunc(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 42/44] 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 f70ad6a54..29d1f0a87 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -136,7 +136,7 @@ class Node(object): 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 @@ class Node(object): 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 43/44] 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 f50902eb1..9dad7a165 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -1039,14 +1039,19 @@ class TestKeywordSelection(object): ), ], ) -@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 44/44] 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 000000000..bf0ba0897 --- /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 770d6b2a6..6c4b921fa 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 11ca1e8f4..3f748d666 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -374,6 +374,22 @@ def test_collection_warnings(testdir): ) +@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):