Skip to content

Integrate pytest warnings #2072

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
6b135c8
Initial commit.
fschulze Jun 22, 2016
b9c4ecf
Add MANIFEST.in.
fschulze Jun 27, 2016
3feee0c
Prepare 0.1 release.
fschulze Jun 27, 2016
bc5a8c7
Fix version in readme.
fschulze Jun 27, 2016
bc94a51
Add a warning option which does not escape its arguments.
Carreau Oct 17, 2016
28621b0
Merge pull request #2 from Carreau/filter-regex
fschulze Oct 24, 2016
f229b57
Bump version, add changelog entry and move stuff around for added cov…
fschulze Oct 24, 2016
ce13806
Prepare pytest-warnings 0.2.0.
fschulze Oct 24, 2016
6ec0c3f
Bump.
fschulze Oct 24, 2016
e31421a
Moving all stuff to a subdirectory to try to retain history
nicoddemus Nov 21, 2016
9db32ae
Merge c:\pytest-warnings\ into integrate-pytest-warnings
nicoddemus Nov 21, 2016
1da1906
Rename code to _pytest.warnings and delete old files from the repository
nicoddemus Nov 21, 2016
26ca5a7
Add tests and integrated the original code into the core
nicoddemus Nov 21, 2016
bd343ef
Merge remote-tracking branch 'upstream/features' into integrate-pytes…
nicoddemus Nov 22, 2016
a7643a5
Merge branch 'features' into integrate-pytest-warnings
nicoddemus Feb 18, 2017
82785fc
Use warnings.catch_warnings instead of WarningsRecorder
nicoddemus Feb 18, 2017
e24081b
Change warning output
nicoddemus Feb 25, 2017
de09023
Also capture warnings during setup/teardown
nicoddemus Mar 4, 2017
bddb922
Rename internal option to disable_warnings
nicoddemus Mar 4, 2017
272afa9
Display node ids and the warnings generated by it
nicoddemus Mar 4, 2017
337f891
Fixed tests
nicoddemus Mar 7, 2017
0baed78
Merge remote-tracking branch 'upstream/features' into integrate-pytes…
nicoddemus Mar 16, 2017
be5db6f
Capture warnings around the entire runtestprotocol
nicoddemus Mar 17, 2017
7819409
Improve warning representation in terminal plugin and fix tests
nicoddemus Mar 17, 2017
9f85584
Merge remote-tracking branch 'upstream/features' into integrate-pytes…
nicoddemus Mar 20, 2017
3373e02
Add __future__ imports to warnings module
nicoddemus Mar 20, 2017
d027f76
Avoid displaying the same warning multiple times for an item
nicoddemus Mar 20, 2017
fa56114
Clean up warnings generated by pytest's own suite
nicoddemus Mar 21, 2017
eabe3ee
Add docs for the warnings functionality
nicoddemus Mar 21, 2017
916d272
Fix test on linux
nicoddemus Mar 21, 2017
2c73074
Fix errors related to warnings raised by xdist
nicoddemus Mar 22, 2017
74b54ac
Fix errors related to warnings raised on pypy test environment
nicoddemus Mar 22, 2017
0c1c258
Add CHANGELOG entry
nicoddemus Mar 22, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ New Features
* ``pytest.param`` can be used to declare test parameter sets with marks and test ids.
Thanks `@RonnyPfannschmidt`_ for the PR.

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


Changes
-------
Expand Down
6 changes: 3 additions & 3 deletions _pytest/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def directory_arg(path, optname):
"mark main terminal runner python fixtures debugging unittest capture skipping "
"tmpdir monkeypatch recwarn pastebin helpconfig nose assertion "
"junitxml resultlog doctest cacheprovider freeze_support "
"setuponly setupplan").split()
"setuponly setupplan warnings").split()

builtin_plugins = set(default_plugins)
builtin_plugins.add("pytester")
Expand Down Expand Up @@ -911,11 +911,11 @@ def _ensure_unconfigure(self):
fin = self._cleanup.pop()
fin()

def warn(self, code, message, fslocation=None):
def warn(self, code, message, fslocation=None, nodeid=None):
""" generate a warning for this test session. """
self.hook.pytest_logwarning.call_historic(kwargs=dict(
code=code, message=message,
fslocation=fslocation, nodeid=None))
fslocation=fslocation, nodeid=nodeid))

def get_terminal_writer(self):
return self.pluginmanager.get_plugin("terminalreporter")._tw
Expand Down
2 changes: 1 addition & 1 deletion _pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -1081,7 +1081,7 @@ def parsefactories(self, node_or_obj, nodeid=NOTSET, unittest=False):
continue
marker = defaultfuncargprefixmarker
from _pytest import deprecated
self.config.warn('C1', deprecated.FUNCARG_PREFIX.format(name=name))
self.config.warn('C1', deprecated.FUNCARG_PREFIX.format(name=name), nodeid=nodeid)
name = name[len(self._argprefix):]
elif not isinstance(marker, FixtureFunctionMarker):
# magic globals with __getattr__ might have got us a wrong
Expand Down
2 changes: 1 addition & 1 deletion _pytest/pytester.py
Original file line number Diff line number Diff line change
Expand Up @@ -1008,7 +1008,7 @@ def spawn_pytest(self, string, expect_timeout=10.0):
The pexpect child is returned.

"""
basetemp = self.tmpdir.mkdir("pexpect")
basetemp = self.tmpdir.mkdir("temp-pexpect")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that must have been so puzzleing

invoke = " ".join(map(str, self._getpytestargs()))
cmd = "%s --basetemp=%s %s" % (invoke, basetemp, string)
return self.spawn(cmd, expect_timeout=expect_timeout)
Expand Down
2 changes: 1 addition & 1 deletion _pytest/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -936,7 +936,7 @@ def _idval(val, argname, idx, idfn, config=None):
import warnings
msg = "Raised while trying to determine id of parameter %s at position %d." % (argname, idx)
msg += '\nUpdate your code as this will raise an error in pytest-4.0.'
warnings.warn(msg)
warnings.warn(msg, DeprecationWarning)
if s:
return _escape_strings(s)

Expand Down
2 changes: 0 additions & 2 deletions _pytest/recwarn.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,12 @@ def deprecated_call(func=None, *args, **kwargs):

def warn_explicit(message, category, *args, **kwargs):
categories.append(category)
old_warn_explicit(message, category, *args, **kwargs)

def warn(message, category=None, *args, **kwargs):
if isinstance(message, Warning):
categories.append(message.__class__)
else:
categories.append(category)
old_warn(message, category, *args, **kwargs)

old_warn = warnings.warn
old_warn_explicit = warnings.warn_explicit
Expand Down
17 changes: 12 additions & 5 deletions _pytest/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -554,14 +554,21 @@ def importorskip(modname, minversion=None):
__version__ attribute. If no minversion is specified the a skip
is only triggered if the module can not be imported.
"""
import warnings
__tracebackhide__ = True
compile(modname, '', 'eval') # to catch syntaxerrors
should_skip = False
try:
__import__(modname)
except ImportError:
# Do not raise chained exception here(#1485)
should_skip = True

with warnings.catch_warnings():
# make sure to ignore ImportWarnings that might happen because
# of existing directories with the same name we're trying to
# import but without a __init__.py file
warnings.simplefilter('ignore')
try:
__import__(modname)
except ImportError:
# Do not raise chained exception here(#1485)
should_skip = True
if should_skip:
raise Skipped("could not import %r" %(modname,), allow_module_level=True)
mod = sys.modules[modname]
Expand Down
70 changes: 51 additions & 19 deletions _pytest/terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""
from __future__ import absolute_import, division, print_function

import itertools
from _pytest.main import EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, \
EXIT_USAGEERROR, EXIT_NOTESTSCOLLECTED
import pytest
Expand All @@ -26,11 +27,11 @@ def pytest_addoption(parser):
help="show extra test summary info as specified by chars (f)ailed, "
"(E)error, (s)skipped, (x)failed, (X)passed, "
"(p)passed, (P)passed with output, (a)all except pP. "
"The pytest warnings are displayed at all times except when "
"--disable-pytest-warnings is set")
group._addoption('--disable-pytest-warnings', default=False,
dest='disablepytestwarnings', action='store_true',
help='disable warnings summary, overrides -r w flag')
"Warnings are displayed at all times except when "
"--disable-warnings is set")
group._addoption('--disable-warnings', '--disable-pytest-warnings', default=False,
dest='disable_warnings', action='store_true',
help='disable warnings summary')
group._addoption('-l', '--showlocals',
action="store_true", dest="showlocals", default=False,
help="show locals in tracebacks (disabled by default).")
Expand Down Expand Up @@ -59,9 +60,9 @@ def mywriter(tags, args):
def getreportopt(config):
reportopts = ""
reportchars = config.option.reportchars
if not config.option.disablepytestwarnings and 'w' not in reportchars:
if not config.option.disable_warnings and 'w' not in reportchars:
reportchars += 'w'
elif config.option.disablepytestwarnings and 'w' in reportchars:
elif config.option.disable_warnings and 'w' in reportchars:
reportchars = reportchars.replace('w', '')
if reportchars:
for char in reportchars:
Expand All @@ -82,13 +83,40 @@ def pytest_report_teststatus(report):
letter = "f"
return report.outcome, letter, report.outcome.upper()


class WarningReport(object):
"""
Simple structure to hold warnings information captured by ``pytest_logwarning``.
"""
def __init__(self, code, message, nodeid=None, fslocation=None):
"""
:param code: unused
:param str message: user friendly message about the warning
:param str|None nodeid: node id that generated the warning (see ``get_location``).
:param tuple|py.path.local fslocation:
file system location of the source of the warning (see ``get_location``).
"""
self.code = code
self.message = message
self.nodeid = nodeid
self.fslocation = fslocation

def get_location(self, config):
"""
Returns the more user-friendly information about the location
of a warning, or None.
"""
if self.nodeid:
return self.nodeid
if self.fslocation:
if isinstance(self.fslocation, tuple) and len(self.fslocation) == 2:
filename, linenum = self.fslocation
relpath = py.path.local(filename).relto(config.invocation_dir)
return '%s:%d' % (relpath, linenum)
else:
return str(self.fslocation)
return None


class TerminalReporter(object):
def __init__(self, config, file=None):
Expand Down Expand Up @@ -168,8 +196,6 @@ def pytest_internalerror(self, excrepr):

def pytest_logwarning(self, code, fslocation, message, nodeid):
warnings = self.stats.setdefault("warnings", [])
if isinstance(fslocation, tuple):
fslocation = "%s:%d" % fslocation
warning = WarningReport(code=code, fslocation=fslocation,
message=message, nodeid=nodeid)
warnings.append(warning)
Expand Down Expand Up @@ -440,13 +466,21 @@ def getreports(self, name):

def summary_warnings(self):
if self.hasopt("w"):
warnings = self.stats.get("warnings")
if not warnings:
all_warnings = self.stats.get("warnings")
if not all_warnings:
return
self.write_sep("=", "pytest-warning summary")
for w in warnings:
self._tw.line("W%s %s %s" % (w.code,
w.fslocation, w.message))

grouped = itertools.groupby(all_warnings, key=lambda wr: wr.get_location(self.config))

self.write_sep("=", "warnings summary", yellow=True, bold=False)
for location, warnings in grouped:
self._tw.line(str(location) or '<undetermined location>')
for w in warnings:
lines = w.message.splitlines()
indented = '\n'.join(' ' + x for x in lines)
self._tw.line(indented)
self._tw.line()
self._tw.line('-- Docs: http://doc.pytest.org/en/latest/warnings.html')

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

def build_summary_stats_line(stats):
keys = ("failed passed skipped deselected "
"xfailed xpassed warnings error").split()
key_translation = {'warnings': 'pytest-warnings'}
"xfailed xpassed warnings error").split()
unknown_key_seen = False
for key in stats.keys():
if key not in keys:
Expand All @@ -560,8 +593,7 @@ def build_summary_stats_line(stats):
for key in keys:
val = stats.get(key, None)
if val:
key_name = key_translation.get(key, key)
parts.append("%d %s" % (len(val), key_name))
parts.append("%d %s" % (len(val), key))

if parts:
line = ", ".join(parts)
Expand Down
73 changes: 73 additions & 0 deletions _pytest/warnings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from __future__ import absolute_import, division, print_function

import warnings
from contextlib import contextmanager

import pytest


def _setoption(wmod, arg):
"""
Copy of the warning._setoption function but does not escape arguments.
"""
parts = arg.split(':')
if len(parts) > 5:
raise wmod._OptionError("too many fields (max 5): %r" % (arg,))
while len(parts) < 5:
parts.append('')
action, message, category, module, lineno = [s.strip()
for s in parts]
action = wmod._getaction(action)
category = wmod._getcategory(category)
if lineno:
try:
lineno = int(lineno)
if lineno < 0:
raise ValueError
except (ValueError, OverflowError):
raise wmod._OptionError("invalid lineno %r" % (lineno,))
else:
lineno = 0
wmod.filterwarnings(action, message, category, module, lineno)


def pytest_addoption(parser):
group = parser.getgroup("pytest-warnings")
group.addoption(
'-W', '--pythonwarnings', action='append',
help="set which warnings to report, see -W option of python itself.")
parser.addini("filterwarnings", type="linelist",
help="Each line specifies warning filter pattern which would be passed"
"to warnings.filterwarnings. Process after -W and --pythonwarnings.")


@contextmanager
def catch_warnings_for_item(item):
"""
catches the warnings generated during setup/call/teardown execution
of the given item and after it is done posts them as warnings to this
item.
"""
args = item.config.getoption('pythonwarnings') or []
inifilters = item.config.getini("filterwarnings")
with warnings.catch_warnings(record=True) as log:
warnings.simplefilter('once')
for arg in args:
warnings._setoption(arg)

for arg in inifilters:
_setoption(warnings, arg)

yield

for warning in log:
msg = warnings.formatwarning(
warning.message, warning.category,
warning.filename, warning.lineno, warning.line)
item.warn("unused", msg)


@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_protocol(item):
with catch_warnings_for_item(item):
yield
2 changes: 1 addition & 1 deletion doc/en/contents.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Full pytest documentation
monkeypatch
tmpdir
capture
recwarn
warnings
doctest
mark
skipping
Expand Down
20 changes: 20 additions & 0 deletions doc/en/customize.rst
Original file line number Diff line number Diff line change
Expand Up @@ -240,3 +240,23 @@ Builtin configuration file options
By default, pytest will stop searching for ``conftest.py`` files upwards
from ``pytest.ini``/``tox.ini``/``setup.cfg`` of the project if any,
or up to the file-system root.


.. confval:: filterwarnings

.. versionadded:: 3.1

Sets a list of filters and actions that should be taken for matched
warnings. By default all warnings emitted during the test session
will be displayed in a summary at the end of the test session.

.. code-block:: ini

# content of pytest.ini
[pytest]
filterwarnings =
error
ignore::DeprecationWarning

This tells pytest to ignore deprecation warnings and turn all other warnings
into errors. For more information please refer to :ref:`warnings`.
Loading