warnings.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. # mypy: allow-untyped-defs
  2. from __future__ import annotations
  3. from collections.abc import Generator
  4. from contextlib import contextmanager
  5. from contextlib import ExitStack
  6. import sys
  7. from typing import Literal
  8. import warnings
  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. from _pytest.tracemalloc import tracemalloc_message
  16. import pytest
  17. @contextmanager
  18. def catch_warnings_for_item(
  19. config: Config,
  20. ihook,
  21. when: Literal["config", "collect", "runtest"],
  22. item: Item | None,
  23. *,
  24. record: bool = True,
  25. ) -> Generator[None]:
  26. """Context manager that catches warnings generated in the contained execution block.
  27. ``item`` can be None if we are not in the context of an item execution.
  28. Each warning captured triggers the ``pytest_warning_recorded`` hook.
  29. """
  30. config_filters = config.getini("filterwarnings")
  31. cmdline_filters = config.known_args_namespace.pythonwarnings or []
  32. with warnings.catch_warnings(record=record) as log:
  33. if not sys.warnoptions:
  34. # If user is not explicitly configuring warning filters, show deprecation warnings by default (#2908).
  35. warnings.filterwarnings("always", category=DeprecationWarning)
  36. warnings.filterwarnings("always", category=PendingDeprecationWarning)
  37. warnings.filterwarnings("error", category=pytest.PytestRemovedIn9Warning)
  38. apply_warning_filters(config_filters, cmdline_filters)
  39. # apply filters from "filterwarnings" marks
  40. nodeid = "" if item is None else item.nodeid
  41. if item is not None:
  42. for mark in item.iter_markers(name="filterwarnings"):
  43. for arg in mark.args:
  44. warnings.filterwarnings(*parse_warning_filter(arg, escape=False))
  45. try:
  46. yield
  47. finally:
  48. if record:
  49. # mypy can't infer that record=True means log is not None; help it.
  50. assert log is not None
  51. for warning_message in log:
  52. ihook.pytest_warning_recorded.call_historic(
  53. kwargs=dict(
  54. warning_message=warning_message,
  55. nodeid=nodeid,
  56. when=when,
  57. location=None,
  58. )
  59. )
  60. def warning_record_to_str(warning_message: warnings.WarningMessage) -> str:
  61. """Convert a warnings.WarningMessage to a string."""
  62. return warnings.formatwarning(
  63. str(warning_message.message),
  64. warning_message.category,
  65. warning_message.filename,
  66. warning_message.lineno,
  67. warning_message.line,
  68. ) + tracemalloc_message(warning_message.source)
  69. @pytest.hookimpl(wrapper=True, tryfirst=True)
  70. def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
  71. with catch_warnings_for_item(
  72. config=item.config, ihook=item.ihook, when="runtest", item=item
  73. ):
  74. return (yield)
  75. @pytest.hookimpl(wrapper=True, tryfirst=True)
  76. def pytest_collection(session: Session) -> Generator[None, object, object]:
  77. config = session.config
  78. with catch_warnings_for_item(
  79. config=config, ihook=config.hook, when="collect", item=None
  80. ):
  81. return (yield)
  82. @pytest.hookimpl(wrapper=True)
  83. def pytest_terminal_summary(
  84. terminalreporter: TerminalReporter,
  85. ) -> Generator[None]:
  86. config = terminalreporter.config
  87. with catch_warnings_for_item(
  88. config=config, ihook=config.hook, when="config", item=None
  89. ):
  90. return (yield)
  91. @pytest.hookimpl(wrapper=True)
  92. def pytest_sessionfinish(session: Session) -> Generator[None]:
  93. config = session.config
  94. with catch_warnings_for_item(
  95. config=config, ihook=config.hook, when="config", item=None
  96. ):
  97. return (yield)
  98. @pytest.hookimpl(wrapper=True)
  99. def pytest_load_initial_conftests(
  100. early_config: Config,
  101. ) -> Generator[None]:
  102. with catch_warnings_for_item(
  103. config=early_config, ihook=early_config.hook, when="config", item=None
  104. ):
  105. return (yield)
  106. def pytest_configure(config: Config) -> None:
  107. with ExitStack() as stack:
  108. stack.enter_context(
  109. catch_warnings_for_item(
  110. config=config,
  111. ihook=config.hook,
  112. when="config",
  113. item=None,
  114. # this disables recording because the terminalreporter has
  115. # finished by the time it comes to reporting logged warnings
  116. # from the end of config cleanup. So for now, this is only
  117. # useful for setting a warning filter with an 'error' action.
  118. record=False,
  119. )
  120. )
  121. config.addinivalue_line(
  122. "markers",
  123. "filterwarnings(warning): add a warning filter to the given test. "
  124. "see https://docs.pytest.org/en/stable/how-to/capture-warnings.html#pytest-mark-filterwarnings ",
  125. )
  126. config.add_cleanup(stack.pop_all().close)