Skip to content

Commit

Permalink
Merge pull request #7255 from gnikonorov/issue_4049
Browse files Browse the repository at this point in the history
Add new hook pytest_warning_recorded
  • Loading branch information
RonnyPfannschmidt committed May 31, 2020
2 parents b32f4de + 2af0d1e commit c16ede5
Show file tree
Hide file tree
Showing 7 changed files with 86 additions and 20 deletions.
3 changes: 3 additions & 0 deletions changelog/7255.feature.rst
@@ -0,0 +1,3 @@
Introduced a new hook named `pytest_warning_recorded` to convey information about warnings captured by the internal `pytest` warnings plugin.

This hook is meant to replace `pytest_warning_captured`, which will be removed in a future release.
1 change: 1 addition & 0 deletions doc/en/reference.rst
Expand Up @@ -711,6 +711,7 @@ Session related reporting hooks:
.. autofunction:: pytest_fixture_setup
.. autofunction:: pytest_fixture_post_finalizer
.. autofunction:: pytest_warning_captured
.. autofunction:: pytest_warning_recorded

Central hook for reporting about test execution:

Expand Down
5 changes: 5 additions & 0 deletions src/_pytest/deprecated.py
Expand Up @@ -80,3 +80,8 @@
"The `-k 'expr:'` syntax to -k is deprecated.\n"
"Please open an issue if you use this and want a replacement."
)

WARNING_CAPTURED_HOOK = PytestDeprecationWarning(
"The pytest_warning_captured is deprecated and will be removed in a future release.\n"
"Please use pytest_warning_recorded instead."
)
42 changes: 36 additions & 6 deletions src/_pytest/hookspec.py
Expand Up @@ -8,9 +8,11 @@
from pluggy import HookspecMarker

from .deprecated import COLLECT_DIRECTORY_HOOK
from .deprecated import WARNING_CAPTURED_HOOK
from _pytest.compat import TYPE_CHECKING

if TYPE_CHECKING:
import warnings
from _pytest.config import Config
from _pytest.main import Session
from _pytest.reports import BaseReport
Expand Down Expand Up @@ -620,10 +622,12 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config):
"""


@hookspec(historic=True)
@hookspec(historic=True, warn_on_impl=WARNING_CAPTURED_HOOK)
def pytest_warning_captured(warning_message, when, item, location):
"""
Process a warning captured by the internal pytest warnings plugin.
"""(**Deprecated**) Process a warning captured by the internal pytest warnings plugin.
This hook is considered deprecated and will be removed in a future pytest version.
Use :func:`pytest_warning_recorded` instead.
:param warnings.WarningMessage warning_message:
The captured warning. This is the same object produced by :py:func:`warnings.catch_warnings`, and contains
Expand All @@ -637,9 +641,6 @@ def pytest_warning_captured(warning_message, when, item, location):
* ``"runtest"``: during test execution.
:param pytest.Item|None item:
**DEPRECATED**: This parameter is incompatible with ``pytest-xdist``, and will always receive ``None``
in a future release.
The item being executed if ``when`` is ``"runtest"``, otherwise ``None``.
:param tuple location:
Expand All @@ -648,6 +649,35 @@ def pytest_warning_captured(warning_message, when, item, location):
"""


@hookspec(historic=True)
def pytest_warning_recorded(
warning_message: "warnings.WarningMessage",
when: str,
nodeid: str,
location: Tuple[str, int, str],
):
"""
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
the same attributes as the parameters of :py:func:`warnings.showwarning`.
: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.
:param str nodeid: full id of the item
:param tuple location:
Holds information about the execution context of the captured warning (filename, linenumber, function).
``function`` evaluates to <module> when the execution context is at the module level.
"""


# -------------------------------------------------------------------------
# doctest hooks
# -------------------------------------------------------------------------
Expand Down
6 changes: 2 additions & 4 deletions src/_pytest/terminal.py
Expand Up @@ -227,7 +227,7 @@ def pytest_report_teststatus(report: TestReport) -> Tuple[str, str, str]:
@attr.s
class WarningReport:
"""
Simple structure to hold warnings information captured by ``pytest_warning_captured``.
Simple structure to hold warnings information captured by ``pytest_warning_recorded``.
:ivar str message: user friendly message about the warning
:ivar str|None nodeid: node id that generated the warning (see ``get_location``).
Expand Down Expand Up @@ -411,14 +411,12 @@ def pytest_internalerror(self, excrepr):
self.write_line("INTERNALERROR> " + line)
return 1

def pytest_warning_captured(self, warning_message, item):
# from _pytest.nodes import get_fslocation_from_item
def pytest_warning_recorded(self, warning_message, nodeid):
from _pytest.warnings import warning_record_to_str

fslocation = warning_message.filename, warning_message.lineno
message = warning_record_to_str(warning_message)

nodeid = item.nodeid if item is not None else ""
warning_report = WarningReport(
fslocation=fslocation, message=message, nodeid=nodeid
)
Expand Down
18 changes: 16 additions & 2 deletions src/_pytest/warnings.py
Expand Up @@ -81,7 +81,7 @@ def catch_warnings_for_item(config, ihook, when, item):
``item`` can be None if we are not in the context of an item execution.
Each warning captured triggers the ``pytest_warning_captured`` hook.
Each warning captured triggers the ``pytest_warning_recorded`` hook.
"""
cmdline_filters = config.getoption("pythonwarnings") or []
inifilters = config.getini("filterwarnings")
Expand All @@ -102,6 +102,7 @@ def catch_warnings_for_item(config, ihook, when, item):
for arg in cmdline_filters:
warnings.filterwarnings(*_parse_filter(arg, escape=True))

nodeid = "" if item is None else item.nodeid
if item is not None:
for mark in item.iter_markers(name="filterwarnings"):
for arg in mark.args:
Expand All @@ -113,6 +114,14 @@ def catch_warnings_for_item(config, ihook, when, item):
ihook.pytest_warning_captured.call_historic(
kwargs=dict(warning_message=warning_message, when=when, item=item)
)
ihook.pytest_warning_recorded.call_historic(
kwargs=dict(
warning_message=warning_message,
nodeid=nodeid,
when=when,
location=None,
)
)


def warning_record_to_str(warning_message):
Expand Down Expand Up @@ -166,7 +175,7 @@ def pytest_sessionfinish(session):
def _issue_warning_captured(warning, hook, stacklevel):
"""
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
at this point the actual options might not have been set, so we manually trigger the pytest_warning_recorded
hook so we can display these warnings in the terminal. This is a hack until we can sort out #2891.
:param warning: the warning instance.
Expand All @@ -185,3 +194,8 @@ def _issue_warning_captured(warning, hook, stacklevel):
warning_message=records[0], when="config", item=None, location=location
)
)
hook.pytest_warning_recorded.call_historic(
kwargs=dict(
warning_message=records[0], when="config", nodeid="", location=location
)
)
31 changes: 23 additions & 8 deletions testing/test_warnings.py
Expand Up @@ -268,21 +268,36 @@ def test_func(fix):
collected = []

class WarningCollector:
def pytest_warning_captured(self, warning_message, when, item):
imge_name = item.name if item is not None else ""
collected.append((str(warning_message.message), when, imge_name))
def pytest_warning_recorded(self, warning_message, when, nodeid, location):
collected.append((str(warning_message.message), when, nodeid, location))

result = testdir.runpytest(plugins=[WarningCollector()])
result.stdout.fnmatch_lines(["*1 passed*"])

expected = [
("config warning", "config", ""),
("collect warning", "collect", ""),
("setup warning", "runtest", "test_func"),
("call warning", "runtest", "test_func"),
("teardown warning", "runtest", "test_func"),
("setup warning", "runtest", "test_warning_captured_hook.py::test_func"),
("call warning", "runtest", "test_warning_captured_hook.py::test_func"),
("teardown warning", "runtest", "test_warning_captured_hook.py::test_func"),
]
assert collected == expected
for index in range(len(expected)):
collected_result = collected[index]
expected_result = expected[index]

assert collected_result[0] == expected_result[0], str(collected)
assert collected_result[1] == expected_result[1], str(collected)
assert collected_result[2] == expected_result[2], str(collected)

# NOTE: collected_result[3] is location, which differs based on the platform you are on
# thus, the best we can do here is assert the types of the paremeters match what we expect
# and not try and preload it in the expected array
if collected_result[3] is not None:
assert type(collected_result[3][0]) is str, str(collected)
assert type(collected_result[3][1]) is int, str(collected)
assert type(collected_result[3][2]) is str, str(collected)
else:
assert collected_result[3] is None, str(collected)


@pytest.mark.filterwarnings("always")
Expand Down Expand Up @@ -649,7 +664,7 @@ class CapturedWarnings:
captured = []

@classmethod
def pytest_warning_captured(cls, warning_message, when, item, location):
def pytest_warning_recorded(cls, warning_message, when, nodeid, location):
cls.captured.append((warning_message, location))

testdir.plugins = [CapturedWarnings()]
Expand Down

0 comments on commit c16ede5

Please sign in to comment.