From 3fcc4cdbd54f135c2031f019a7503cba90dd5dd9 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 25 Aug 2018 22:15:22 -0300 Subject: [PATCH] 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*"])