66from contextlib import ExitStack
77import sys
88from typing import Literal
9- from typing import TYPE_CHECKING
109import warnings
1110
1211from _pytest .config import apply_warning_filters
1312from _pytest .config import Config
1413from _pytest .config import parse_warning_filter
1514from _pytest .main import Session
1615from _pytest .nodes import Item
16+ from _pytest .reports import TestReport
17+ from _pytest .runner import CallInfo
1718from _pytest .stash import StashKey
1819from _pytest .terminal import TerminalReporter
1920from _pytest .tracemalloc import tracemalloc_message
2021import pytest
2122
2223
23- if TYPE_CHECKING :
24- from _pytest .reports import TestReport
25- from _pytest .runner import CallInfo
26-
2724# StashKey for storing warning log on items
2825warning_captured_log_key = StashKey [list [warnings .WarningMessage ]]()
2926
30- # Key name for storing warning flag in report.user_properties
31- HAS_WARNINGS_KEY = "has_warnings"
27+ # Track which nodeids have warnings (for pytest_report_teststatus)
28+ _nodeids_with_warnings : set [ str ] = set ()
3229
3330
3431@contextmanager
@@ -59,7 +56,6 @@ def catch_warnings_for_item(
5956 apply_warning_filters (config_filters , cmdline_filters )
6057
6158 # apply filters from "filterwarnings" marks
62- nodeid = "" if item is None else item .nodeid
6359 if item is not None :
6460 for mark in item .iter_markers (name = "filterwarnings" ):
6561 for arg in mark .args :
@@ -68,22 +64,9 @@ def catch_warnings_for_item(
6864 if record and log is not None :
6965 item .stash [warning_captured_log_key ] = log
7066
71- try :
72- yield
73- finally :
74- if record :
75- # mypy can't infer that record=True means log is not None; help it.
76- assert log is not None
77-
78- for warning_message in log :
79- ihook .pytest_warning_recorded .call_historic (
80- kwargs = dict (
81- warning_message = warning_message ,
82- nodeid = nodeid ,
83- when = when ,
84- location = None ,
85- )
86- )
67+ yield
68+ # Note: pytest_warning_recorded hooks are now dispatched from
69+ # pytest_runtest_makereport for better timing and integration
8770
8871
8972def warning_record_to_str (warning_message : warnings .WarningMessage ) -> str :
@@ -109,15 +92,35 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
10992def pytest_runtest_makereport (
11093 item : Item , call : CallInfo [None ]
11194) -> Generator [None , TestReport , None ]:
112- """Attach warning information to test reports for terminal coloring ."""
95+ """Process warnings from stash and dispatch pytest_warning_recorded hooks ."""
11396 outcome = yield
11497 report : TestReport = outcome .get_result ()
11598
116- # Only mark warnings during the call phase, not setup/teardown
117- if report .passed and report .when == "call" :
99+ if report .when == "call" :
118100 warning_log = item .stash .get (warning_captured_log_key , None )
119- if warning_log is not None and len (warning_log ) > 0 :
120- report .user_properties .append ((HAS_WARNINGS_KEY , True ))
101+ if warning_log :
102+ _nodeids_with_warnings .add (item .nodeid )
103+ # Set attribute on report for xdist compatibility
104+ report .has_warnings = True # type: ignore[attr-defined]
105+
106+ for warning_message in warning_log :
107+ item .ihook .pytest_warning_recorded .call_historic (
108+ kwargs = dict (
109+ warning_message = warning_message ,
110+ nodeid = item .nodeid ,
111+ when = "runtest" ,
112+ location = None ,
113+ )
114+ )
115+
116+
117+ @pytest .hookimpl ()
118+ def pytest_report_teststatus (report : TestReport , config : Config ):
119+ """Provide yellow markup for passed tests that have warnings."""
120+ if report .passed and report .when == "call" :
121+ if hasattr (report , "has_warnings" ) and report .has_warnings :
122+ # Return (category, shortletter, verbose_word) with yellow markup
123+ return "passed" , "." , ("PASSED" , {"yellow" : True })
121124
122125
123126@pytest .hookimpl (wrapper = True , tryfirst = True )
0 commit comments