Skip to content

Commit 2a130da

Browse files
Merge pull request #2072 from nicoddemus/integrate-pytest-warnings
Integrate pytest warnings
2 parents de8607d + 0c1c258 commit 2a130da

28 files changed

+588
-226
lines changed

CHANGELOG.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ New Features
1919
* ``pytest.param`` can be used to declare test parameter sets with marks and test ids.
2020
Thanks `@RonnyPfannschmidt`_ for the PR.
2121

22+
* The ``pytest-warnings`` plugin has been integrated into the core, so now ``pytest`` automatically
23+
captures and displays warnings at the end of the test session.
24+
Thanks `@nicoddemus`_ for the PR.
25+
2226

2327
Changes
2428
-------

_pytest/config.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def directory_arg(path, optname):
9999
"mark main terminal runner python fixtures debugging unittest capture skipping "
100100
"tmpdir monkeypatch recwarn pastebin helpconfig nose assertion "
101101
"junitxml resultlog doctest cacheprovider freeze_support "
102-
"setuponly setupplan").split()
102+
"setuponly setupplan warnings").split()
103103

104104
builtin_plugins = set(default_plugins)
105105
builtin_plugins.add("pytester")
@@ -911,11 +911,11 @@ def _ensure_unconfigure(self):
911911
fin = self._cleanup.pop()
912912
fin()
913913

914-
def warn(self, code, message, fslocation=None):
914+
def warn(self, code, message, fslocation=None, nodeid=None):
915915
""" generate a warning for this test session. """
916916
self.hook.pytest_logwarning.call_historic(kwargs=dict(
917917
code=code, message=message,
918-
fslocation=fslocation, nodeid=None))
918+
fslocation=fslocation, nodeid=nodeid))
919919

920920
def get_terminal_writer(self):
921921
return self.pluginmanager.get_plugin("terminalreporter")._tw

_pytest/fixtures.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1081,7 +1081,7 @@ def parsefactories(self, node_or_obj, nodeid=NOTSET, unittest=False):
10811081
continue
10821082
marker = defaultfuncargprefixmarker
10831083
from _pytest import deprecated
1084-
self.config.warn('C1', deprecated.FUNCARG_PREFIX.format(name=name))
1084+
self.config.warn('C1', deprecated.FUNCARG_PREFIX.format(name=name), nodeid=nodeid)
10851085
name = name[len(self._argprefix):]
10861086
elif not isinstance(marker, FixtureFunctionMarker):
10871087
# magic globals with __getattr__ might have got us a wrong

_pytest/pytester.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1008,7 +1008,7 @@ def spawn_pytest(self, string, expect_timeout=10.0):
10081008
The pexpect child is returned.
10091009
10101010
"""
1011-
basetemp = self.tmpdir.mkdir("pexpect")
1011+
basetemp = self.tmpdir.mkdir("temp-pexpect")
10121012
invoke = " ".join(map(str, self._getpytestargs()))
10131013
cmd = "%s --basetemp=%s %s" % (invoke, basetemp, string)
10141014
return self.spawn(cmd, expect_timeout=expect_timeout)

_pytest/python.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -936,7 +936,7 @@ def _idval(val, argname, idx, idfn, config=None):
936936
import warnings
937937
msg = "Raised while trying to determine id of parameter %s at position %d." % (argname, idx)
938938
msg += '\nUpdate your code as this will raise an error in pytest-4.0.'
939-
warnings.warn(msg)
939+
warnings.warn(msg, DeprecationWarning)
940940
if s:
941941
return _escape_strings(s)
942942

_pytest/recwarn.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,12 @@ def deprecated_call(func=None, *args, **kwargs):
5656

5757
def warn_explicit(message, category, *args, **kwargs):
5858
categories.append(category)
59-
old_warn_explicit(message, category, *args, **kwargs)
6059

6160
def warn(message, category=None, *args, **kwargs):
6261
if isinstance(message, Warning):
6362
categories.append(message.__class__)
6463
else:
6564
categories.append(category)
66-
old_warn(message, category, *args, **kwargs)
6765

6866
old_warn = warnings.warn
6967
old_warn_explicit = warnings.warn_explicit

_pytest/runner.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -554,14 +554,21 @@ def importorskip(modname, minversion=None):
554554
__version__ attribute. If no minversion is specified the a skip
555555
is only triggered if the module can not be imported.
556556
"""
557+
import warnings
557558
__tracebackhide__ = True
558559
compile(modname, '', 'eval') # to catch syntaxerrors
559560
should_skip = False
560-
try:
561-
__import__(modname)
562-
except ImportError:
563-
# Do not raise chained exception here(#1485)
564-
should_skip = True
561+
562+
with warnings.catch_warnings():
563+
# make sure to ignore ImportWarnings that might happen because
564+
# of existing directories with the same name we're trying to
565+
# import but without a __init__.py file
566+
warnings.simplefilter('ignore')
567+
try:
568+
__import__(modname)
569+
except ImportError:
570+
# Do not raise chained exception here(#1485)
571+
should_skip = True
565572
if should_skip:
566573
raise Skipped("could not import %r" %(modname,), allow_module_level=True)
567574
mod = sys.modules[modname]

_pytest/terminal.py

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"""
55
from __future__ import absolute_import, division, print_function
66

7+
import itertools
78
from _pytest.main import EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, \
89
EXIT_USAGEERROR, EXIT_NOTESTSCOLLECTED
910
import pytest
@@ -26,11 +27,11 @@ def pytest_addoption(parser):
2627
help="show extra test summary info as specified by chars (f)ailed, "
2728
"(E)error, (s)skipped, (x)failed, (X)passed, "
2829
"(p)passed, (P)passed with output, (a)all except pP. "
29-
"The pytest warnings are displayed at all times except when "
30-
"--disable-pytest-warnings is set")
31-
group._addoption('--disable-pytest-warnings', default=False,
32-
dest='disablepytestwarnings', action='store_true',
33-
help='disable warnings summary, overrides -r w flag')
30+
"Warnings are displayed at all times except when "
31+
"--disable-warnings is set")
32+
group._addoption('--disable-warnings', '--disable-pytest-warnings', default=False,
33+
dest='disable_warnings', action='store_true',
34+
help='disable warnings summary')
3435
group._addoption('-l', '--showlocals',
3536
action="store_true", dest="showlocals", default=False,
3637
help="show locals in tracebacks (disabled by default).")
@@ -59,9 +60,9 @@ def mywriter(tags, args):
5960
def getreportopt(config):
6061
reportopts = ""
6162
reportchars = config.option.reportchars
62-
if not config.option.disablepytestwarnings and 'w' not in reportchars:
63+
if not config.option.disable_warnings and 'w' not in reportchars:
6364
reportchars += 'w'
64-
elif config.option.disablepytestwarnings and 'w' in reportchars:
65+
elif config.option.disable_warnings and 'w' in reportchars:
6566
reportchars = reportchars.replace('w', '')
6667
if reportchars:
6768
for char in reportchars:
@@ -82,13 +83,40 @@ def pytest_report_teststatus(report):
8283
letter = "f"
8384
return report.outcome, letter, report.outcome.upper()
8485

86+
8587
class WarningReport(object):
88+
"""
89+
Simple structure to hold warnings information captured by ``pytest_logwarning``.
90+
"""
8691
def __init__(self, code, message, nodeid=None, fslocation=None):
92+
"""
93+
:param code: unused
94+
:param str message: user friendly message about the warning
95+
:param str|None nodeid: node id that generated the warning (see ``get_location``).
96+
:param tuple|py.path.local fslocation:
97+
file system location of the source of the warning (see ``get_location``).
98+
"""
8799
self.code = code
88100
self.message = message
89101
self.nodeid = nodeid
90102
self.fslocation = fslocation
91103

104+
def get_location(self, config):
105+
"""
106+
Returns the more user-friendly information about the location
107+
of a warning, or None.
108+
"""
109+
if self.nodeid:
110+
return self.nodeid
111+
if self.fslocation:
112+
if isinstance(self.fslocation, tuple) and len(self.fslocation) == 2:
113+
filename, linenum = self.fslocation
114+
relpath = py.path.local(filename).relto(config.invocation_dir)
115+
return '%s:%d' % (relpath, linenum)
116+
else:
117+
return str(self.fslocation)
118+
return None
119+
92120

93121
class TerminalReporter(object):
94122
def __init__(self, config, file=None):
@@ -168,8 +196,6 @@ def pytest_internalerror(self, excrepr):
168196

169197
def pytest_logwarning(self, code, fslocation, message, nodeid):
170198
warnings = self.stats.setdefault("warnings", [])
171-
if isinstance(fslocation, tuple):
172-
fslocation = "%s:%d" % fslocation
173199
warning = WarningReport(code=code, fslocation=fslocation,
174200
message=message, nodeid=nodeid)
175201
warnings.append(warning)
@@ -440,13 +466,21 @@ def getreports(self, name):
440466

441467
def summary_warnings(self):
442468
if self.hasopt("w"):
443-
warnings = self.stats.get("warnings")
444-
if not warnings:
469+
all_warnings = self.stats.get("warnings")
470+
if not all_warnings:
445471
return
446-
self.write_sep("=", "pytest-warning summary")
447-
for w in warnings:
448-
self._tw.line("W%s %s %s" % (w.code,
449-
w.fslocation, w.message))
472+
473+
grouped = itertools.groupby(all_warnings, key=lambda wr: wr.get_location(self.config))
474+
475+
self.write_sep("=", "warnings summary", yellow=True, bold=False)
476+
for location, warnings in grouped:
477+
self._tw.line(str(location) or '<undetermined location>')
478+
for w in warnings:
479+
lines = w.message.splitlines()
480+
indented = '\n'.join(' ' + x for x in lines)
481+
self._tw.line(indented)
482+
self._tw.line()
483+
self._tw.line('-- Docs: http://doc.pytest.org/en/latest/warnings.html')
450484

451485
def summary_passes(self):
452486
if self.config.option.tbstyle != "no":
@@ -548,8 +582,7 @@ def flatten(l):
548582

549583
def build_summary_stats_line(stats):
550584
keys = ("failed passed skipped deselected "
551-
"xfailed xpassed warnings error").split()
552-
key_translation = {'warnings': 'pytest-warnings'}
585+
"xfailed xpassed warnings error").split()
553586
unknown_key_seen = False
554587
for key in stats.keys():
555588
if key not in keys:
@@ -560,8 +593,7 @@ def build_summary_stats_line(stats):
560593
for key in keys:
561594
val = stats.get(key, None)
562595
if val:
563-
key_name = key_translation.get(key, key)
564-
parts.append("%d %s" % (len(val), key_name))
596+
parts.append("%d %s" % (len(val), key))
565597

566598
if parts:
567599
line = ", ".join(parts)

_pytest/warnings.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from __future__ import absolute_import, division, print_function
2+
3+
import warnings
4+
from contextlib import contextmanager
5+
6+
import pytest
7+
8+
9+
def _setoption(wmod, arg):
10+
"""
11+
Copy of the warning._setoption function but does not escape arguments.
12+
"""
13+
parts = arg.split(':')
14+
if len(parts) > 5:
15+
raise wmod._OptionError("too many fields (max 5): %r" % (arg,))
16+
while len(parts) < 5:
17+
parts.append('')
18+
action, message, category, module, lineno = [s.strip()
19+
for s in parts]
20+
action = wmod._getaction(action)
21+
category = wmod._getcategory(category)
22+
if lineno:
23+
try:
24+
lineno = int(lineno)
25+
if lineno < 0:
26+
raise ValueError
27+
except (ValueError, OverflowError):
28+
raise wmod._OptionError("invalid lineno %r" % (lineno,))
29+
else:
30+
lineno = 0
31+
wmod.filterwarnings(action, message, category, module, lineno)
32+
33+
34+
def pytest_addoption(parser):
35+
group = parser.getgroup("pytest-warnings")
36+
group.addoption(
37+
'-W', '--pythonwarnings', action='append',
38+
help="set which warnings to report, see -W option of python itself.")
39+
parser.addini("filterwarnings", type="linelist",
40+
help="Each line specifies warning filter pattern which would be passed"
41+
"to warnings.filterwarnings. Process after -W and --pythonwarnings.")
42+
43+
44+
@contextmanager
45+
def catch_warnings_for_item(item):
46+
"""
47+
catches the warnings generated during setup/call/teardown execution
48+
of the given item and after it is done posts them as warnings to this
49+
item.
50+
"""
51+
args = item.config.getoption('pythonwarnings') or []
52+
inifilters = item.config.getini("filterwarnings")
53+
with warnings.catch_warnings(record=True) as log:
54+
warnings.simplefilter('once')
55+
for arg in args:
56+
warnings._setoption(arg)
57+
58+
for arg in inifilters:
59+
_setoption(warnings, arg)
60+
61+
yield
62+
63+
for warning in log:
64+
msg = warnings.formatwarning(
65+
warning.message, warning.category,
66+
warning.filename, warning.lineno, warning.line)
67+
item.warn("unused", msg)
68+
69+
70+
@pytest.hookimpl(hookwrapper=True)
71+
def pytest_runtest_protocol(item):
72+
with catch_warnings_for_item(item):
73+
yield

doc/en/contents.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Full pytest documentation
1818
monkeypatch
1919
tmpdir
2020
capture
21-
recwarn
21+
warnings
2222
doctest
2323
mark
2424
skipping

doc/en/customize.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,3 +240,23 @@ Builtin configuration file options
240240
By default, pytest will stop searching for ``conftest.py`` files upwards
241241
from ``pytest.ini``/``tox.ini``/``setup.cfg`` of the project if any,
242242
or up to the file-system root.
243+
244+
245+
.. confval:: filterwarnings
246+
247+
.. versionadded:: 3.1
248+
249+
Sets a list of filters and actions that should be taken for matched
250+
warnings. By default all warnings emitted during the test session
251+
will be displayed in a summary at the end of the test session.
252+
253+
.. code-block:: ini
254+
255+
# content of pytest.ini
256+
[pytest]
257+
filterwarnings =
258+
error
259+
ignore::DeprecationWarning
260+
261+
This tells pytest to ignore deprecation warnings and turn all other warnings
262+
into errors. For more information please refer to :ref:`warnings`.

0 commit comments

Comments
 (0)