Skip to content

Commit c16ede5

Browse files
Merge pull request #7255 from gnikonorov/issue_4049
Add new hook pytest_warning_recorded
2 parents b32f4de + 2af0d1e commit c16ede5

File tree

7 files changed

+86
-20
lines changed

7 files changed

+86
-20
lines changed

changelog/7255.feature.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Introduced a new hook named `pytest_warning_recorded` to convey information about warnings captured by the internal `pytest` warnings plugin.
2+
3+
This hook is meant to replace `pytest_warning_captured`, which will be removed in a future release.

doc/en/reference.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -711,6 +711,7 @@ Session related reporting hooks:
711711
.. autofunction:: pytest_fixture_setup
712712
.. autofunction:: pytest_fixture_post_finalizer
713713
.. autofunction:: pytest_warning_captured
714+
.. autofunction:: pytest_warning_recorded
714715

715716
Central hook for reporting about test execution:
716717

src/_pytest/deprecated.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,8 @@
8080
"The `-k 'expr:'` syntax to -k is deprecated.\n"
8181
"Please open an issue if you use this and want a replacement."
8282
)
83+
84+
WARNING_CAPTURED_HOOK = PytestDeprecationWarning(
85+
"The pytest_warning_captured is deprecated and will be removed in a future release.\n"
86+
"Please use pytest_warning_recorded instead."
87+
)

src/_pytest/hookspec.py

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88
from pluggy import HookspecMarker
99

1010
from .deprecated import COLLECT_DIRECTORY_HOOK
11+
from .deprecated import WARNING_CAPTURED_HOOK
1112
from _pytest.compat import TYPE_CHECKING
1213

1314
if TYPE_CHECKING:
15+
import warnings
1416
from _pytest.config import Config
1517
from _pytest.main import Session
1618
from _pytest.reports import BaseReport
@@ -620,10 +622,12 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config):
620622
"""
621623

622624

623-
@hookspec(historic=True)
625+
@hookspec(historic=True, warn_on_impl=WARNING_CAPTURED_HOOK)
624626
def pytest_warning_captured(warning_message, when, item, location):
625-
"""
626-
Process a warning captured by the internal pytest warnings plugin.
627+
"""(**Deprecated**) Process a warning captured by the internal pytest warnings plugin.
628+
629+
This hook is considered deprecated and will be removed in a future pytest version.
630+
Use :func:`pytest_warning_recorded` instead.
627631
628632
:param warnings.WarningMessage warning_message:
629633
The captured warning. This is the same object produced by :py:func:`warnings.catch_warnings`, and contains
@@ -637,9 +641,6 @@ def pytest_warning_captured(warning_message, when, item, location):
637641
* ``"runtest"``: during test execution.
638642
639643
:param pytest.Item|None item:
640-
**DEPRECATED**: This parameter is incompatible with ``pytest-xdist``, and will always receive ``None``
641-
in a future release.
642-
643644
The item being executed if ``when`` is ``"runtest"``, otherwise ``None``.
644645
645646
:param tuple location:
@@ -648,6 +649,35 @@ def pytest_warning_captured(warning_message, when, item, location):
648649
"""
649650

650651

652+
@hookspec(historic=True)
653+
def pytest_warning_recorded(
654+
warning_message: "warnings.WarningMessage",
655+
when: str,
656+
nodeid: str,
657+
location: Tuple[str, int, str],
658+
):
659+
"""
660+
Process a warning captured by the internal pytest warnings plugin.
661+
662+
:param warnings.WarningMessage warning_message:
663+
The captured warning. This is the same object produced by :py:func:`warnings.catch_warnings`, and contains
664+
the same attributes as the parameters of :py:func:`warnings.showwarning`.
665+
666+
:param str when:
667+
Indicates when the warning was captured. Possible values:
668+
669+
* ``"config"``: during pytest configuration/initialization stage.
670+
* ``"collect"``: during test collection.
671+
* ``"runtest"``: during test execution.
672+
673+
:param str nodeid: full id of the item
674+
675+
:param tuple location:
676+
Holds information about the execution context of the captured warning (filename, linenumber, function).
677+
``function`` evaluates to <module> when the execution context is at the module level.
678+
"""
679+
680+
651681
# -------------------------------------------------------------------------
652682
# doctest hooks
653683
# -------------------------------------------------------------------------

src/_pytest/terminal.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ def pytest_report_teststatus(report: TestReport) -> Tuple[str, str, str]:
227227
@attr.s
228228
class WarningReport:
229229
"""
230-
Simple structure to hold warnings information captured by ``pytest_warning_captured``.
230+
Simple structure to hold warnings information captured by ``pytest_warning_recorded``.
231231
232232
:ivar str message: user friendly message about the warning
233233
:ivar str|None nodeid: node id that generated the warning (see ``get_location``).
@@ -411,14 +411,12 @@ def pytest_internalerror(self, excrepr):
411411
self.write_line("INTERNALERROR> " + line)
412412
return 1
413413

414-
def pytest_warning_captured(self, warning_message, item):
415-
# from _pytest.nodes import get_fslocation_from_item
414+
def pytest_warning_recorded(self, warning_message, nodeid):
416415
from _pytest.warnings import warning_record_to_str
417416

418417
fslocation = warning_message.filename, warning_message.lineno
419418
message = warning_record_to_str(warning_message)
420419

421-
nodeid = item.nodeid if item is not None else ""
422420
warning_report = WarningReport(
423421
fslocation=fslocation, message=message, nodeid=nodeid
424422
)

src/_pytest/warnings.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def catch_warnings_for_item(config, ihook, when, item):
8181
8282
``item`` can be None if we are not in the context of an item execution.
8383
84-
Each warning captured triggers the ``pytest_warning_captured`` hook.
84+
Each warning captured triggers the ``pytest_warning_recorded`` hook.
8585
"""
8686
cmdline_filters = config.getoption("pythonwarnings") or []
8787
inifilters = config.getini("filterwarnings")
@@ -102,6 +102,7 @@ def catch_warnings_for_item(config, ihook, when, item):
102102
for arg in cmdline_filters:
103103
warnings.filterwarnings(*_parse_filter(arg, escape=True))
104104

105+
nodeid = "" if item is None else item.nodeid
105106
if item is not None:
106107
for mark in item.iter_markers(name="filterwarnings"):
107108
for arg in mark.args:
@@ -113,6 +114,14 @@ def catch_warnings_for_item(config, ihook, when, item):
113114
ihook.pytest_warning_captured.call_historic(
114115
kwargs=dict(warning_message=warning_message, when=when, item=item)
115116
)
117+
ihook.pytest_warning_recorded.call_historic(
118+
kwargs=dict(
119+
warning_message=warning_message,
120+
nodeid=nodeid,
121+
when=when,
122+
location=None,
123+
)
124+
)
116125

117126

118127
def warning_record_to_str(warning_message):
@@ -166,7 +175,7 @@ def pytest_sessionfinish(session):
166175
def _issue_warning_captured(warning, hook, stacklevel):
167176
"""
168177
This function should be used instead of calling ``warnings.warn`` directly when we are in the "configure" stage:
169-
at this point the actual options might not have been set, so we manually trigger the pytest_warning_captured
178+
at this point the actual options might not have been set, so we manually trigger the pytest_warning_recorded
170179
hook so we can display these warnings in the terminal. This is a hack until we can sort out #2891.
171180
172181
:param warning: the warning instance.
@@ -185,3 +194,8 @@ def _issue_warning_captured(warning, hook, stacklevel):
185194
warning_message=records[0], when="config", item=None, location=location
186195
)
187196
)
197+
hook.pytest_warning_recorded.call_historic(
198+
kwargs=dict(
199+
warning_message=records[0], when="config", nodeid="", location=location
200+
)
201+
)

testing/test_warnings.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -268,21 +268,36 @@ def test_func(fix):
268268
collected = []
269269

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

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

278277
expected = [
279278
("config warning", "config", ""),
280279
("collect warning", "collect", ""),
281-
("setup warning", "runtest", "test_func"),
282-
("call warning", "runtest", "test_func"),
283-
("teardown warning", "runtest", "test_func"),
280+
("setup warning", "runtest", "test_warning_captured_hook.py::test_func"),
281+
("call warning", "runtest", "test_warning_captured_hook.py::test_func"),
282+
("teardown warning", "runtest", "test_warning_captured_hook.py::test_func"),
284283
]
285-
assert collected == expected
284+
for index in range(len(expected)):
285+
collected_result = collected[index]
286+
expected_result = expected[index]
287+
288+
assert collected_result[0] == expected_result[0], str(collected)
289+
assert collected_result[1] == expected_result[1], str(collected)
290+
assert collected_result[2] == expected_result[2], str(collected)
291+
292+
# NOTE: collected_result[3] is location, which differs based on the platform you are on
293+
# thus, the best we can do here is assert the types of the paremeters match what we expect
294+
# and not try and preload it in the expected array
295+
if collected_result[3] is not None:
296+
assert type(collected_result[3][0]) is str, str(collected)
297+
assert type(collected_result[3][1]) is int, str(collected)
298+
assert type(collected_result[3][2]) is str, str(collected)
299+
else:
300+
assert collected_result[3] is None, str(collected)
286301

287302

288303
@pytest.mark.filterwarnings("always")
@@ -649,7 +664,7 @@ class CapturedWarnings:
649664
captured = []
650665

651666
@classmethod
652-
def pytest_warning_captured(cls, warning_message, when, item, location):
667+
def pytest_warning_recorded(cls, warning_message, when, nodeid, location):
653668
cls.captured.append((warning_message, location))
654669

655670
testdir.plugins = [CapturedWarnings()]

0 commit comments

Comments
 (0)