Merge pull request #7255 from gnikonorov/issue_4049
Add new hook pytest_warning_recorded
This commit is contained in:
commit
c16ede5ce6
|
@ -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.
|
|
@ -711,6 +711,7 @@ Session related reporting hooks:
|
||||||
.. autofunction:: pytest_fixture_setup
|
.. autofunction:: pytest_fixture_setup
|
||||||
.. autofunction:: pytest_fixture_post_finalizer
|
.. autofunction:: pytest_fixture_post_finalizer
|
||||||
.. autofunction:: pytest_warning_captured
|
.. autofunction:: pytest_warning_captured
|
||||||
|
.. autofunction:: pytest_warning_recorded
|
||||||
|
|
||||||
Central hook for reporting about test execution:
|
Central hook for reporting about test execution:
|
||||||
|
|
||||||
|
|
|
@ -80,3 +80,8 @@ MINUS_K_COLON = PytestDeprecationWarning(
|
||||||
"The `-k 'expr:'` syntax to -k is deprecated.\n"
|
"The `-k 'expr:'` syntax to -k is deprecated.\n"
|
||||||
"Please open an issue if you use this and want a replacement."
|
"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."
|
||||||
|
)
|
||||||
|
|
|
@ -8,9 +8,11 @@ from typing import Union
|
||||||
from pluggy import HookspecMarker
|
from pluggy import HookspecMarker
|
||||||
|
|
||||||
from .deprecated import COLLECT_DIRECTORY_HOOK
|
from .deprecated import COLLECT_DIRECTORY_HOOK
|
||||||
|
from .deprecated import WARNING_CAPTURED_HOOK
|
||||||
from _pytest.compat import TYPE_CHECKING
|
from _pytest.compat import TYPE_CHECKING
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
import warnings
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
from _pytest.main import Session
|
from _pytest.main import Session
|
||||||
from _pytest.reports import BaseReport
|
from _pytest.reports import BaseReport
|
||||||
|
@ -620,8 +622,40 @@ 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):
|
def pytest_warning_captured(warning_message, when, item, location):
|
||||||
|
"""(**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
|
||||||
|
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 pytest.Item|None item:
|
||||||
|
The item being executed if ``when`` is ``"runtest"``, otherwise ``None``.
|
||||||
|
|
||||||
|
: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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@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.
|
Process a warning captured by the internal pytest warnings plugin.
|
||||||
|
|
||||||
|
@ -636,11 +670,7 @@ def pytest_warning_captured(warning_message, when, item, location):
|
||||||
* ``"collect"``: during test collection.
|
* ``"collect"``: during test collection.
|
||||||
* ``"runtest"``: during test execution.
|
* ``"runtest"``: during test execution.
|
||||||
|
|
||||||
:param pytest.Item|None item:
|
:param str nodeid: full id of the 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:
|
:param tuple location:
|
||||||
Holds information about the execution context of the captured warning (filename, linenumber, function).
|
Holds information about the execution context of the captured warning (filename, linenumber, function).
|
||||||
|
|
|
@ -227,7 +227,7 @@ def pytest_report_teststatus(report: TestReport) -> Tuple[str, str, str]:
|
||||||
@attr.s
|
@attr.s
|
||||||
class WarningReport:
|
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 message: user friendly message about the warning
|
||||||
:ivar str|None nodeid: node id that generated the warning (see ``get_location``).
|
:ivar str|None nodeid: node id that generated the warning (see ``get_location``).
|
||||||
|
@ -411,14 +411,12 @@ class TerminalReporter:
|
||||||
self.write_line("INTERNALERROR> " + line)
|
self.write_line("INTERNALERROR> " + line)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
def pytest_warning_captured(self, warning_message, item):
|
def pytest_warning_recorded(self, warning_message, nodeid):
|
||||||
# from _pytest.nodes import get_fslocation_from_item
|
|
||||||
from _pytest.warnings import warning_record_to_str
|
from _pytest.warnings import warning_record_to_str
|
||||||
|
|
||||||
fslocation = warning_message.filename, warning_message.lineno
|
fslocation = warning_message.filename, warning_message.lineno
|
||||||
message = warning_record_to_str(warning_message)
|
message = warning_record_to_str(warning_message)
|
||||||
|
|
||||||
nodeid = item.nodeid if item is not None else ""
|
|
||||||
warning_report = WarningReport(
|
warning_report = WarningReport(
|
||||||
fslocation=fslocation, message=message, nodeid=nodeid
|
fslocation=fslocation, message=message, nodeid=nodeid
|
||||||
)
|
)
|
||||||
|
|
|
@ -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.
|
``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 []
|
cmdline_filters = config.getoption("pythonwarnings") or []
|
||||||
inifilters = config.getini("filterwarnings")
|
inifilters = config.getini("filterwarnings")
|
||||||
|
@ -102,6 +102,7 @@ def catch_warnings_for_item(config, ihook, when, item):
|
||||||
for arg in cmdline_filters:
|
for arg in cmdline_filters:
|
||||||
warnings.filterwarnings(*_parse_filter(arg, escape=True))
|
warnings.filterwarnings(*_parse_filter(arg, escape=True))
|
||||||
|
|
||||||
|
nodeid = "" if item is None else item.nodeid
|
||||||
if item is not None:
|
if item is not None:
|
||||||
for mark in item.iter_markers(name="filterwarnings"):
|
for mark in item.iter_markers(name="filterwarnings"):
|
||||||
for arg in mark.args:
|
for arg in mark.args:
|
||||||
|
@ -113,6 +114,14 @@ def catch_warnings_for_item(config, ihook, when, item):
|
||||||
ihook.pytest_warning_captured.call_historic(
|
ihook.pytest_warning_captured.call_historic(
|
||||||
kwargs=dict(warning_message=warning_message, when=when, item=item)
|
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):
|
def warning_record_to_str(warning_message):
|
||||||
|
@ -166,7 +175,7 @@ def pytest_sessionfinish(session):
|
||||||
def _issue_warning_captured(warning, hook, stacklevel):
|
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:
|
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.
|
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.
|
:param warning: the warning instance.
|
||||||
|
@ -185,3 +194,8 @@ def _issue_warning_captured(warning, hook, stacklevel):
|
||||||
warning_message=records[0], when="config", item=None, location=location
|
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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
|
@ -268,9 +268,8 @@ def test_warning_captured_hook(testdir):
|
||||||
collected = []
|
collected = []
|
||||||
|
|
||||||
class WarningCollector:
|
class WarningCollector:
|
||||||
def pytest_warning_captured(self, warning_message, when, item):
|
def pytest_warning_recorded(self, warning_message, when, nodeid, location):
|
||||||
imge_name = item.name if item is not None else ""
|
collected.append((str(warning_message.message), when, nodeid, location))
|
||||||
collected.append((str(warning_message.message), when, imge_name))
|
|
||||||
|
|
||||||
result = testdir.runpytest(plugins=[WarningCollector()])
|
result = testdir.runpytest(plugins=[WarningCollector()])
|
||||||
result.stdout.fnmatch_lines(["*1 passed*"])
|
result.stdout.fnmatch_lines(["*1 passed*"])
|
||||||
|
@ -278,11 +277,27 @@ def test_warning_captured_hook(testdir):
|
||||||
expected = [
|
expected = [
|
||||||
("config warning", "config", ""),
|
("config warning", "config", ""),
|
||||||
("collect warning", "collect", ""),
|
("collect warning", "collect", ""),
|
||||||
("setup warning", "runtest", "test_func"),
|
("setup warning", "runtest", "test_warning_captured_hook.py::test_func"),
|
||||||
("call warning", "runtest", "test_func"),
|
("call warning", "runtest", "test_warning_captured_hook.py::test_func"),
|
||||||
("teardown warning", "runtest", "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")
|
@pytest.mark.filterwarnings("always")
|
||||||
|
@ -649,7 +664,7 @@ class TestStackLevel:
|
||||||
captured = []
|
captured = []
|
||||||
|
|
||||||
@classmethod
|
@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))
|
cls.captured.append((warning_message, location))
|
||||||
|
|
||||||
testdir.plugins = [CapturedWarnings()]
|
testdir.plugins = [CapturedWarnings()]
|
||||||
|
|
Loading…
Reference in New Issue