Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1import re 

2import sys 

3import warnings 

4from contextlib import contextmanager 

5from functools import lru_cache 

6from typing import Generator 

7from typing import Optional 

8from typing import Tuple 

9 

10import pytest 

11from _pytest.compat import TYPE_CHECKING 

12from _pytest.config import Config 

13from _pytest.config.argparsing import Parser 

14from _pytest.main import Session 

15from _pytest.nodes import Item 

16from _pytest.terminal import TerminalReporter 

17 

18if TYPE_CHECKING: 

19 from typing import Type 

20 from typing_extensions import Literal 

21 

22 

23@lru_cache(maxsize=50) 

24def _parse_filter( 

25 arg: str, *, escape: bool 

26) -> "Tuple[str, str, Type[Warning], str, int]": 

27 """Parse a warnings filter string. 

28 

29 This is copied from warnings._setoption, but does not apply the filter, 

30 only parses it, and makes the escaping optional. 

31 """ 

32 parts = arg.split(":") 

33 if len(parts) > 5: 

34 raise warnings._OptionError("too many fields (max 5): {!r}".format(arg)) 

35 while len(parts) < 5: 

36 parts.append("") 

37 action_, message, category_, module, lineno_ = [s.strip() for s in parts] 

38 action = warnings._getaction(action_) # type: str # type: ignore[attr-defined] 

39 category = warnings._getcategory( 

40 category_ 

41 ) # type: Type[Warning] # type: ignore[attr-defined] 

42 if message and escape: 

43 message = re.escape(message) 

44 if module and escape: 

45 module = re.escape(module) + r"\Z" 

46 if lineno_: 

47 try: 

48 lineno = int(lineno_) 

49 if lineno < 0: 

50 raise ValueError 

51 except (ValueError, OverflowError) as e: 

52 raise warnings._OptionError("invalid lineno {!r}".format(lineno_)) from e 

53 else: 

54 lineno = 0 

55 return (action, message, category, module, lineno) 

56 

57 

58def pytest_addoption(parser: Parser) -> None: 

59 group = parser.getgroup("pytest-warnings") 

60 group.addoption( 

61 "-W", 

62 "--pythonwarnings", 

63 action="append", 

64 help="set which warnings to report, see -W option of python itself.", 

65 ) 

66 parser.addini( 

67 "filterwarnings", 

68 type="linelist", 

69 help="Each line specifies a pattern for " 

70 "warnings.filterwarnings. " 

71 "Processed after -W/--pythonwarnings.", 

72 ) 

73 

74 

75def pytest_configure(config: Config) -> None: 

76 config.addinivalue_line( 

77 "markers", 

78 "filterwarnings(warning): add a warning filter to the given test. " 

79 "see https://docs.pytest.org/en/stable/warnings.html#pytest-mark-filterwarnings ", 

80 ) 

81 

82 

83@contextmanager 

84def catch_warnings_for_item( 

85 config: Config, 

86 ihook, 

87 when: "Literal['config', 'collect', 'runtest']", 

88 item: Optional[Item], 

89) -> Generator[None, None, None]: 

90 """ 

91 Context manager that catches warnings generated in the contained execution block. 

92 

93 ``item`` can be None if we are not in the context of an item execution. 

94 

95 Each warning captured triggers the ``pytest_warning_recorded`` hook. 

96 """ 

97 cmdline_filters = config.getoption("pythonwarnings") or [] 

98 inifilters = config.getini("filterwarnings") 

99 with warnings.catch_warnings(record=True) as log: 

100 # mypy can't infer that record=True means log is not None; help it. 

101 assert log is not None 

102 

103 if not sys.warnoptions: 

104 # if user is not explicitly configuring warning filters, show deprecation warnings by default (#2908) 

105 warnings.filterwarnings("always", category=DeprecationWarning) 

106 warnings.filterwarnings("always", category=PendingDeprecationWarning) 

107 

108 warnings.filterwarnings("error", category=pytest.PytestDeprecationWarning) 

109 

110 # filters should have this precedence: mark, cmdline options, ini 

111 # filters should be applied in the inverse order of precedence 

112 for arg in inifilters: 

113 warnings.filterwarnings(*_parse_filter(arg, escape=False)) 

114 

115 for arg in cmdline_filters: 

116 warnings.filterwarnings(*_parse_filter(arg, escape=True)) 

117 

118 nodeid = "" if item is None else item.nodeid 

119 if item is not None: 

120 for mark in item.iter_markers(name="filterwarnings"): 

121 for arg in mark.args: 

122 warnings.filterwarnings(*_parse_filter(arg, escape=False)) 

123 

124 yield 

125 

126 for warning_message in log: 

127 ihook.pytest_warning_captured.call_historic( 

128 kwargs=dict( 

129 warning_message=warning_message, 

130 when=when, 

131 item=item, 

132 location=None, 

133 ) 

134 ) 

135 ihook.pytest_warning_recorded.call_historic( 

136 kwargs=dict( 

137 warning_message=warning_message, 

138 nodeid=nodeid, 

139 when=when, 

140 location=None, 

141 ) 

142 ) 

143 

144 

145def warning_record_to_str(warning_message: warnings.WarningMessage) -> str: 

146 """Convert a warnings.WarningMessage to a string.""" 

147 warn_msg = warning_message.message 

148 msg = warnings.formatwarning( 

149 str(warn_msg), 

150 warning_message.category, 

151 warning_message.filename, 

152 warning_message.lineno, 

153 warning_message.line, 

154 ) 

155 return msg 

156 

157 

158@pytest.hookimpl(hookwrapper=True, tryfirst=True) 

159def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: 

160 with catch_warnings_for_item( 

161 config=item.config, ihook=item.ihook, when="runtest", item=item 

162 ): 

163 yield 

164 

165 

166@pytest.hookimpl(hookwrapper=True, tryfirst=True) 

167def pytest_collection(session: Session) -> Generator[None, None, None]: 

168 config = session.config 

169 with catch_warnings_for_item( 

170 config=config, ihook=config.hook, when="collect", item=None 

171 ): 

172 yield 

173 

174 

175@pytest.hookimpl(hookwrapper=True) 

176def pytest_terminal_summary( 

177 terminalreporter: TerminalReporter, 

178) -> Generator[None, None, None]: 

179 config = terminalreporter.config 

180 with catch_warnings_for_item( 

181 config=config, ihook=config.hook, when="config", item=None 

182 ): 

183 yield 

184 

185 

186@pytest.hookimpl(hookwrapper=True) 

187def pytest_sessionfinish(session: Session) -> Generator[None, None, None]: 

188 config = session.config 

189 with catch_warnings_for_item( 

190 config=config, ihook=config.hook, when="config", item=None 

191 ): 

192 yield 

193 

194 

195def _issue_warning_captured(warning: Warning, hook, stacklevel: int) -> None: 

196 """ 

197 This function should be used instead of calling ``warnings.warn`` directly when we are in the "configure" stage: 

198 at this point the actual options might not have been set, so we manually trigger the pytest_warning_recorded 

199 hook so we can display these warnings in the terminal. This is a hack until we can sort out #2891. 

200 

201 :param warning: the warning instance. 

202 :param hook: the hook caller 

203 :param stacklevel: stacklevel forwarded to warnings.warn 

204 """ 

205 with warnings.catch_warnings(record=True) as records: 

206 warnings.simplefilter("always", type(warning)) 

207 warnings.warn(warning, stacklevel=stacklevel) 

208 frame = sys._getframe(stacklevel - 1) 

209 location = frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name 

210 hook.pytest_warning_captured.call_historic( 

211 kwargs=dict( 

212 warning_message=records[0], when="config", item=None, location=location 

213 ) 

214 ) 

215 hook.pytest_warning_recorded.call_historic( 

216 kwargs=dict( 

217 warning_message=records[0], when="config", nodeid="", location=location 

218 ) 

219 )