1 import sys

2 import warnings

3 from contextlib import contextmanager

4 from typing import Generator

5 from typing import Optional

6 from typing import TYPE_CHECKING

7

8 import pytest

9 from _pytest.config import apply_warning_filters

10 from _pytest.config import Config

11 from _pytest.config import parse_warning_filter

12 from _pytest.main import Session

13 from _pytest.nodes import Item

14 from _pytest.terminal import TerminalReporter

15

16 if TYPE_CHECKING:

17 from typing_extensions import Literal

18

19

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

21 config.addinivalue_line(

22 "markers",

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

24 "see https://docs.pytest.org/en/stable/how-to/capture-warnings.html#pytest-mark-filterwarnings ",

25 )

26

27

28@contextmanager

29 def catch_warnings_for_item(

30 config: Config,

31 ihook,

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

33 item: Optional[Item],

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

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

36

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

38

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

40 """

41 config_filters = config.getini("filterwarnings")

42 cmdline_filters = config.known_args_namespace.pythonwarnings or []

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

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

45 assert log is not None

46

47 if not sys.warnoptions:

48 # If user is not explicitly configuring warning filters, show deprecation warnings by default (#2908).

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

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

51

52 apply_warning_filters(config_filters, cmdline_filters)

53

54 # apply filters from "filterwarnings" marks

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

56 if item is not None:

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

58 for arg in mark.args:

59 warnings.filterwarnings(*parse_warning_filter(arg, escape=False))

60

61 yield

62

63 for warning_message in log:

64 ihook.pytest_warning_recorded.call_historic(

65 kwargs=dict(

66 warning_message=warning_message,

67 nodeid=nodeid,

68 when=when,

69 location=None,

70 )

71 )

72

73

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

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

76 warn_msg = warning_message.message

77 msg = warnings.formatwarning(

78 str(warn_msg),

79 warning_message.category,

80 warning_message.filename,

81 warning_message.lineno,

82 warning_message.line,

83 )

84 if warning_message.source is not None:

85 try:

86 import tracemalloc

87 except ImportError:

88 pass

89 else:

90 tb = tracemalloc.get_object_traceback(warning_message.source)

91 if tb is not None:

92 formatted_tb = "\n".join(tb.format())

93 # Use a leading new line to better separate the (large) output

94 # from the traceback to the previous warning text.

95 msg += f"\nObject allocated at:\n{formatted_tb}"

96 else:

97 # No need for a leading new line.

98 url = "https://docs.pytest.org/en/stable/how-to/capture-warnings.html#resource-warnings"

99 msg += "Enable tracemalloc to get traceback where the object was allocated.\n"

100 msg += f"See {url} for more info."

101 return msg

102

103

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

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

106 with catch_warnings_for_item(

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

108 ):

109 yield

110

111

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

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

114 config = session.config

115 with catch_warnings_for_item(

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

117 ):

118 yield

119

120

121@pytest.hookimpl(hookwrapper=True)

122 def pytest_terminal_summary(

123 terminalreporter: TerminalReporter,

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

125 config = terminalreporter.config

126 with catch_warnings_for_item(

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

128 ):

129 yield

130

131

132@pytest.hookimpl(hookwrapper=True)

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

134 config = session.config

135 with catch_warnings_for_item(

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

137 ):

138 yield

139

140

141@pytest.hookimpl(hookwrapper=True)

142 def pytest_load_initial_conftests(

143 early_config: "Config",

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

145 with catch_warnings_for_item(

146 config=early_config, ihook=early_config.hook, when="config", item=None

147 ):

148 yield