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