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
This commit is contained in:
Bruno Oliveira 2018-08-25 22:15:22 -03:00
parent ffd47ceefc
commit 3fcc4cdbd5
5 changed files with 59 additions and 35 deletions

View File

@ -536,13 +536,13 @@ def pytest_logwarning(message, code, nodeid, fslocation):
@hookspec(historic=True) @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. 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 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: :param str when:
Indicates when the warning was captured. Possible values: Indicates when the warning was captured. Possible values:

View File

@ -138,9 +138,7 @@ class Node(object):
""" generate a warning with the given code and message for this """ generate a warning with the given code and message for this
item. """ item. """
assert isinstance(code, str) assert isinstance(code, str)
fslocation = getattr(self, "location", None) fslocation = get_fslocation_from_item(self)
if fslocation is None:
fslocation = getattr(self, "fspath", None)
self.ihook.pytest_logwarning.call_historic( self.ihook.pytest_logwarning.call_historic(
kwargs=dict( kwargs=dict(
code=code, message=message, nodeid=self.nodeid, fslocation=fslocation code=code, message=message, nodeid=self.nodeid, fslocation=fslocation
@ -310,6 +308,18 @@ class Node(object):
repr_failure = _repr_failure_py 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): class Collector(Node):
""" Collector instances create children through collect() """ Collector instances create children through collect()
and thus iteratively build a tree. and thus iteratively build a tree.

View File

@ -9,6 +9,7 @@ import platform
import sys import sys
import time import time
import attr
import pluggy import pluggy
import py import py
import six import six
@ -184,23 +185,20 @@ def pytest_report_teststatus(report):
return report.outcome, letter, report.outcome.upper() return report.outcome, letter, report.outcome.upper()
@attr.s
class WarningReport(object): class WarningReport(object):
""" """
Simple structure to hold warnings information captured by ``pytest_logwarning``. Simple structure to hold warnings information captured by ``pytest_logwarning``.
"""
def __init__(self, code, message, nodeid=None, fslocation=None): :ivar str message: user friendly message about the warning
""" :ivar str|None nodeid: node id that generated the warning (see ``get_location``).
:param code: unused :ivar tuple|py.path.local fslocation:
: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``). file system location of the source of the warning (see ``get_location``).
""" """
self.code = code
self.message = message message = attr.ib()
self.nodeid = nodeid nodeid = attr.ib(default=None)
self.fslocation = fslocation fslocation = attr.ib(default=None)
def get_location(self, config): def get_location(self, config):
""" """
@ -327,13 +325,25 @@ class TerminalReporter(object):
self.write_line("INTERNALERROR> " + line) self.write_line("INTERNALERROR> " + line)
return 1 return 1
def pytest_logwarning(self, code, fslocation, message, nodeid): def pytest_logwarning(self, fslocation, message, nodeid):
warnings = self.stats.setdefault("warnings", []) warnings = self.stats.setdefault("warnings", [])
warning = WarningReport( warning = WarningReport(fslocation=fslocation, message=message, nodeid=nodeid)
code=code, fslocation=fslocation, message=message, nodeid=nodeid
)
warnings.append(warning) 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): def pytest_plugin_registered(self, plugin):
if self.config.option.traceconfig: if self.config.option.traceconfig:
msg = "PLUGIN registered: %s" % (plugin,) msg = "PLUGIN registered: %s" % (plugin,)

View File

@ -58,7 +58,7 @@ def pytest_configure(config):
@contextmanager @contextmanager
def deprecated_catch_warnings_for_item(item): def catch_warnings_for_item(item):
""" """
catches the warnings generated during setup/call/teardown execution catches the warnings generated during setup/call/teardown execution
of the given item and after it is done posts them as warnings to this 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 yield
for warning in log: for warning_message in log:
item.ihook.pytest_warning_captured.call_historic( 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.message
"""
warn_msg = warning.message
unicode_warning = False unicode_warning = False
if compat._PY2 and any(isinstance(m, compat.UNICODE_TYPES) for m in warn_msg.args): if compat._PY2 and any(isinstance(m, compat.UNICODE_TYPES) for m in warn_msg.args):
new_args = [] new_args = []
@ -102,18 +102,22 @@ def deprecated_emit_warning(item, warning):
warn_msg.args = new_args warn_msg.args = new_args
msg = warnings.formatwarning( 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: if unicode_warning:
warnings.warn( warnings.warn(
"Warning is using unicode non convertible to ascii, " "Warning is using unicode non convertible to ascii, "
"converting to a safe representation:\n %s" % msg, "converting to a safe representation:\n %s" % msg,
UnicodeWarning, UnicodeWarning,
) )
return msg
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(hookwrapper=True)
def pytest_runtest_protocol(item): def pytest_runtest_protocol(item):
with deprecated_catch_warnings_for_item(item): with catch_warnings_for_item(item):
yield yield

View File

@ -310,8 +310,8 @@ def test_warning_captured_hook(testdir, pyfile_with_warnings):
collected = [] collected = []
class WarningCollector: class WarningCollector:
def pytest_warning_captured(self, warning, when, item): def pytest_warning_captured(self, warning_message, when, item):
collected.append((warning.category, when, item.name)) collected.append((warning_message.category, when, item.name))
result = testdir.runpytest(plugins=[WarningCollector()]) result = testdir.runpytest(plugins=[WarningCollector()])
result.stdout.fnmatch_lines(["*1 passed*"]) result.stdout.fnmatch_lines(["*1 passed*"])