warnings.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. # -*- coding: utf-8 -*-
  2. from __future__ import absolute_import
  3. from __future__ import division
  4. from __future__ import print_function
  5. import sys
  6. import warnings
  7. from contextlib import contextmanager
  8. import pytest
  9. from _pytest import compat
  10. SHOW_PYTEST_WARNINGS_ARG = "-Walways::pytest.RemovedInPytest4Warning"
  11. def _setoption(wmod, arg):
  12. """
  13. Copy of the warning._setoption function but does not escape arguments.
  14. """
  15. parts = arg.split(":")
  16. if len(parts) > 5:
  17. raise wmod._OptionError("too many fields (max 5): %r" % (arg,))
  18. while len(parts) < 5:
  19. parts.append("")
  20. action, message, category, module, lineno = [s.strip() for s in parts]
  21. action = wmod._getaction(action)
  22. category = wmod._getcategory(category)
  23. if lineno:
  24. try:
  25. lineno = int(lineno)
  26. if lineno < 0:
  27. raise ValueError
  28. except (ValueError, OverflowError):
  29. raise wmod._OptionError("invalid lineno %r" % (lineno,))
  30. else:
  31. lineno = 0
  32. wmod.filterwarnings(action, message, category, module, lineno)
  33. def pytest_addoption(parser):
  34. group = parser.getgroup("pytest-warnings")
  35. group.addoption(
  36. "-W",
  37. "--pythonwarnings",
  38. action="append",
  39. help="set which warnings to report, see -W option of python itself.",
  40. )
  41. parser.addini(
  42. "filterwarnings",
  43. type="linelist",
  44. help="Each line specifies a pattern for "
  45. "warnings.filterwarnings. "
  46. "Processed after -W and --pythonwarnings.",
  47. )
  48. def pytest_configure(config):
  49. config.addinivalue_line(
  50. "markers",
  51. "filterwarnings(warning): add a warning filter to the given test. "
  52. "see https://docs.pytest.org/en/latest/warnings.html#pytest-mark-filterwarnings ",
  53. )
  54. @contextmanager
  55. def catch_warnings_for_item(config, ihook, when, item):
  56. """
  57. Context manager that catches warnings generated in the contained execution block.
  58. ``item`` can be None if we are not in the context of an item execution.
  59. Each warning captured triggers the ``pytest_warning_captured`` hook.
  60. """
  61. cmdline_filters = config.getoption("pythonwarnings") or []
  62. inifilters = config.getini("filterwarnings")
  63. with warnings.catch_warnings(record=True) as log:
  64. if not sys.warnoptions:
  65. # if user is not explicitly configuring warning filters, show deprecation warnings by default (#2908)
  66. warnings.filterwarnings("always", category=DeprecationWarning)
  67. warnings.filterwarnings("always", category=PendingDeprecationWarning)
  68. warnings.filterwarnings("error", category=pytest.RemovedInPytest4Warning)
  69. # filters should have this precedence: mark, cmdline options, ini
  70. # filters should be applied in the inverse order of precedence
  71. for arg in inifilters:
  72. _setoption(warnings, arg)
  73. for arg in cmdline_filters:
  74. warnings._setoption(arg)
  75. if item is not None:
  76. for mark in item.iter_markers(name="filterwarnings"):
  77. for arg in mark.args:
  78. _setoption(warnings, arg)
  79. yield
  80. for warning_message in log:
  81. ihook.pytest_warning_captured.call_historic(
  82. kwargs=dict(warning_message=warning_message, when=when, item=item)
  83. )
  84. def warning_record_to_str(warning_message):
  85. """Convert a warnings.WarningMessage to a string.
  86. This takes lot of unicode shenaningans into account for Python 2.
  87. When Python 2 support is dropped this function can be greatly simplified.
  88. """
  89. warn_msg = warning_message.message
  90. unicode_warning = False
  91. if compat._PY2 and any(isinstance(m, compat.UNICODE_TYPES) for m in warn_msg.args):
  92. new_args = []
  93. for m in warn_msg.args:
  94. new_args.append(
  95. compat.ascii_escaped(m) if isinstance(m, compat.UNICODE_TYPES) else m
  96. )
  97. unicode_warning = list(warn_msg.args) != new_args
  98. warn_msg.args = new_args
  99. msg = warnings.formatwarning(
  100. warn_msg,
  101. warning_message.category,
  102. warning_message.filename,
  103. warning_message.lineno,
  104. warning_message.line,
  105. )
  106. if unicode_warning:
  107. warnings.warn(
  108. "Warning is using unicode non convertible to ascii, "
  109. "converting to a safe representation:\n {!r}".format(compat.safe_str(msg)),
  110. UnicodeWarning,
  111. )
  112. return msg
  113. @pytest.hookimpl(hookwrapper=True, tryfirst=True)
  114. def pytest_runtest_protocol(item):
  115. with catch_warnings_for_item(
  116. config=item.config, ihook=item.ihook, when="runtest", item=item
  117. ):
  118. yield
  119. @pytest.hookimpl(hookwrapper=True, tryfirst=True)
  120. def pytest_collection(session):
  121. config = session.config
  122. with catch_warnings_for_item(
  123. config=config, ihook=config.hook, when="collect", item=None
  124. ):
  125. yield
  126. @pytest.hookimpl(hookwrapper=True)
  127. def pytest_terminal_summary(terminalreporter):
  128. config = terminalreporter.config
  129. with catch_warnings_for_item(
  130. config=config, ihook=config.hook, when="config", item=None
  131. ):
  132. yield
  133. def _issue_warning_captured(warning, hook, stacklevel):
  134. """
  135. This function should be used instead of calling ``warnings.warn`` directly when we are in the "configure" stage:
  136. at this point the actual options might not have been set, so we manually trigger the pytest_warning_captured
  137. hook so we can display this warnings in the terminal. This is a hack until we can sort out #2891.
  138. :param warning: the warning instance.
  139. :param hook: the hook caller
  140. :param stacklevel: stacklevel forwarded to warnings.warn
  141. """
  142. with warnings.catch_warnings(record=True) as records:
  143. warnings.simplefilter("always", type(warning))
  144. warnings.warn(warning, stacklevel=stacklevel)
  145. hook.pytest_warning_captured.call_historic(
  146. kwargs=dict(warning_message=records[0], when="config", item=None)
  147. )