warnings.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  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. import pytest
  8. from _pytest.config import apply_warning_filters
  9. from _pytest.config import Config
  10. from _pytest.config import parse_warning_filter
  11. from _pytest.main import Session
  12. from _pytest.nodes import Item
  13. from _pytest.terminal import TerminalReporter
  14. if TYPE_CHECKING:
  15. from typing_extensions import Literal
  16. def pytest_configure(config: Config) -> None:
  17. config.addinivalue_line(
  18. "markers",
  19. "filterwarnings(warning): add a warning filter to the given test. "
  20. "see https://docs.pytest.org/en/stable/how-to/capture-warnings.html#pytest-mark-filterwarnings ",
  21. )
  22. @contextmanager
  23. def catch_warnings_for_item(
  24. config: Config,
  25. ihook,
  26. when: "Literal['config', 'collect', 'runtest']",
  27. item: Optional[Item],
  28. ) -> Generator[None, None, None]:
  29. """Context manager that catches warnings generated in the contained execution block.
  30. ``item`` can be None if we are not in the context of an item execution.
  31. Each warning captured triggers the ``pytest_warning_recorded`` hook.
  32. """
  33. config_filters = config.getini("filterwarnings")
  34. cmdline_filters = config.known_args_namespace.pythonwarnings or []
  35. with warnings.catch_warnings(record=True) as log:
  36. # mypy can't infer that record=True means log is not None; help it.
  37. assert log is not None
  38. if not sys.warnoptions:
  39. # If user is not explicitly configuring warning filters, show deprecation warnings by default (#2908).
  40. warnings.filterwarnings("always", category=DeprecationWarning)
  41. warnings.filterwarnings("always", category=PendingDeprecationWarning)
  42. apply_warning_filters(config_filters, cmdline_filters)
  43. # apply filters from "filterwarnings" marks
  44. nodeid = "" if item is None else item.nodeid
  45. if item is not None:
  46. for mark in item.iter_markers(name="filterwarnings"):
  47. for arg in mark.args:
  48. warnings.filterwarnings(*parse_warning_filter(arg, escape=False))
  49. yield
  50. for warning_message in log:
  51. ihook.pytest_warning_recorded.call_historic(
  52. kwargs=dict(
  53. warning_message=warning_message,
  54. nodeid=nodeid,
  55. when=when,
  56. location=None,
  57. )
  58. )
  59. def warning_record_to_str(warning_message: warnings.WarningMessage) -> str:
  60. """Convert a warnings.WarningMessage to a string."""
  61. warn_msg = warning_message.message
  62. msg = warnings.formatwarning(
  63. str(warn_msg),
  64. warning_message.category,
  65. warning_message.filename,
  66. warning_message.lineno,
  67. warning_message.line,
  68. )
  69. if warning_message.source is not None:
  70. try:
  71. import tracemalloc
  72. except ImportError:
  73. pass
  74. else:
  75. tb = tracemalloc.get_object_traceback(warning_message.source)
  76. if tb is not None:
  77. formatted_tb = "\n".join(tb.format())
  78. # Use a leading new line to better separate the (large) output
  79. # from the traceback to the previous warning text.
  80. msg += f"\nObject allocated at:\n{formatted_tb}"
  81. else:
  82. # No need for a leading new line.
  83. url = "https://docs.pytest.org/en/stable/how-to/capture-warnings.html#resource-warnings"
  84. msg += "Enable tracemalloc to get traceback where the object was allocated.\n"
  85. msg += f"See {url} for more info."
  86. return msg
  87. @pytest.hookimpl(hookwrapper=True, tryfirst=True)
  88. def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
  89. with catch_warnings_for_item(
  90. config=item.config, ihook=item.ihook, when="runtest", item=item
  91. ):
  92. yield
  93. @pytest.hookimpl(hookwrapper=True, tryfirst=True)
  94. def pytest_collection(session: Session) -> Generator[None, None, None]:
  95. config = session.config
  96. with catch_warnings_for_item(
  97. config=config, ihook=config.hook, when="collect", item=None
  98. ):
  99. yield
  100. @pytest.hookimpl(hookwrapper=True)
  101. def pytest_terminal_summary(
  102. terminalreporter: TerminalReporter,
  103. ) -> Generator[None, None, None]:
  104. config = terminalreporter.config
  105. with catch_warnings_for_item(
  106. config=config, ihook=config.hook, when="config", item=None
  107. ):
  108. yield
  109. @pytest.hookimpl(hookwrapper=True)
  110. def pytest_sessionfinish(session: Session) -> Generator[None, None, None]:
  111. config = session.config
  112. with catch_warnings_for_item(
  113. config=config, ihook=config.hook, when="config", item=None
  114. ):
  115. yield
  116. @pytest.hookimpl(hookwrapper=True)
  117. def pytest_load_initial_conftests(
  118. early_config: "Config",
  119. ) -> Generator[None, None, None]:
  120. with catch_warnings_for_item(
  121. config=early_config, ihook=early_config.hook, when="config", item=None
  122. ):
  123. yield