Merge pull request #2072 from nicoddemus/integrate-pytest-warnings
Integrate pytest warnings
This commit is contained in:
commit
2a130daae6
|
@ -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
|
||||
-------
|
||||
|
|
|
@ -99,7 +99,7 @@ default_plugins = (
|
|||
"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")
|
||||
|
@ -911,11 +911,11 @@ class Config(object):
|
|||
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
|
||||
|
|
|
@ -1081,7 +1081,7 @@ class FixtureManager(object):
|
|||
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
|
||||
|
|
|
@ -1008,7 +1008,7 @@ class Testdir(object):
|
|||
The pexpect child is returned.
|
||||
|
||||
"""
|
||||
basetemp = self.tmpdir.mkdir("pexpect")
|
||||
basetemp = self.tmpdir.mkdir("temp-pexpect")
|
||||
invoke = " ".join(map(str, self._getpytestargs()))
|
||||
cmd = "%s --basetemp=%s %s" % (invoke, basetemp, string)
|
||||
return self.spawn(cmd, expect_timeout=expect_timeout)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -554,9 +554,16 @@ 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
|
||||
|
||||
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:
|
||||
|
|
|
@ -4,6 +4,7 @@ This is a good source for looking at the various reporting hooks.
|
|||
"""
|
||||
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
|
||||
|
@ -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).")
|
||||
|
@ -59,9 +60,9 @@ def pytest_configure(config):
|
|||
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:
|
||||
|
@ -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):
|
||||
|
@ -168,8 +196,6 @@ class TerminalReporter(object):
|
|||
|
||||
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)
|
||||
|
@ -440,13 +466,21 @@ class TerminalReporter(object):
|
|||
|
||||
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")
|
||||
|
||||
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:
|
||||
self._tw.line("W%s %s %s" % (w.code,
|
||||
w.fslocation, w.message))
|
||||
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":
|
||||
|
@ -549,7 +583,6 @@ def flatten(l):
|
|||
def build_summary_stats_line(stats):
|
||||
keys = ("failed passed skipped deselected "
|
||||
"xfailed xpassed warnings error").split()
|
||||
key_translation = {'warnings': 'pytest-warnings'}
|
||||
unknown_key_seen = False
|
||||
for key in stats.keys():
|
||||
if key not in keys:
|
||||
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -18,7 +18,7 @@ Full pytest documentation
|
|||
monkeypatch
|
||||
tmpdir
|
||||
capture
|
||||
recwarn
|
||||
warnings
|
||||
doctest
|
||||
mark
|
||||
skipping
|
||||
|
|
|
@ -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`.
|
||||
|
|
|
@ -1,142 +1,3 @@
|
|||
.. _`asserting warnings`:
|
||||
:orphan:
|
||||
|
||||
.. _assertwarnings:
|
||||
|
||||
Asserting Warnings
|
||||
=====================================================
|
||||
|
||||
.. _`asserting warnings with the warns function`:
|
||||
|
||||
.. _warns:
|
||||
|
||||
Asserting warnings with the warns function
|
||||
-----------------------------------------------
|
||||
|
||||
.. versionadded:: 2.8
|
||||
|
||||
You can check that code raises a particular warning using ``pytest.warns``,
|
||||
which works in a similar manner to :ref:`raises <assertraises>`::
|
||||
|
||||
import warnings
|
||||
import pytest
|
||||
|
||||
def test_warning():
|
||||
with pytest.warns(UserWarning):
|
||||
warnings.warn("my warning", UserWarning)
|
||||
|
||||
The test will fail if the warning in question is not raised.
|
||||
|
||||
You can also call ``pytest.warns`` on a function or code string::
|
||||
|
||||
pytest.warns(expected_warning, func, *args, **kwargs)
|
||||
pytest.warns(expected_warning, "func(*args, **kwargs)")
|
||||
|
||||
The function also returns a list of all raised warnings (as
|
||||
``warnings.WarningMessage`` objects), which you can query for
|
||||
additional information::
|
||||
|
||||
with pytest.warns(RuntimeWarning) as record:
|
||||
warnings.warn("another warning", RuntimeWarning)
|
||||
|
||||
# check that only one warning was raised
|
||||
assert len(record) == 1
|
||||
# check that the message matches
|
||||
assert record[0].message.args[0] == "another warning"
|
||||
|
||||
Alternatively, you can examine raised warnings in detail using the
|
||||
:ref:`recwarn <recwarn>` fixture (see below).
|
||||
|
||||
.. note::
|
||||
``DeprecationWarning`` and ``PendingDeprecationWarning`` are treated
|
||||
differently; see :ref:`ensuring_function_triggers`.
|
||||
|
||||
.. _`recording warnings`:
|
||||
|
||||
.. _recwarn:
|
||||
|
||||
Recording warnings
|
||||
------------------------
|
||||
|
||||
You can record raised warnings either using ``pytest.warns`` or with
|
||||
the ``recwarn`` fixture.
|
||||
|
||||
To record with ``pytest.warns`` without asserting anything about the warnings,
|
||||
pass ``None`` as the expected warning type::
|
||||
|
||||
with pytest.warns(None) as record:
|
||||
warnings.warn("user", UserWarning)
|
||||
warnings.warn("runtime", RuntimeWarning)
|
||||
|
||||
assert len(record) == 2
|
||||
assert str(record[0].message) == "user"
|
||||
assert str(record[1].message) == "runtime"
|
||||
|
||||
The ``recwarn`` fixture will record warnings for the whole function::
|
||||
|
||||
import warnings
|
||||
|
||||
def test_hello(recwarn):
|
||||
warnings.warn("hello", UserWarning)
|
||||
assert len(recwarn) == 1
|
||||
w = recwarn.pop(UserWarning)
|
||||
assert issubclass(w.category, UserWarning)
|
||||
assert str(w.message) == "hello"
|
||||
assert w.filename
|
||||
assert w.lineno
|
||||
|
||||
Both ``recwarn`` and ``pytest.warns`` return the same interface for recorded
|
||||
warnings: a WarningsRecorder instance. To view the recorded warnings, you can
|
||||
iterate over this instance, call ``len`` on it to get the number of recorded
|
||||
warnings, or index into it to get a particular recorded warning. It also
|
||||
provides these methods:
|
||||
|
||||
.. autoclass:: _pytest.recwarn.WarningsRecorder()
|
||||
:members:
|
||||
|
||||
Each recorded warning has the attributes ``message``, ``category``,
|
||||
``filename``, ``lineno``, ``file``, and ``line``. The ``category`` is the
|
||||
class of the warning. The ``message`` is the warning itself; calling
|
||||
``str(message)`` will return the actual message of the warning.
|
||||
|
||||
.. note::
|
||||
:class:`RecordedWarning` was changed from a plain class to a namedtuple in pytest 3.1
|
||||
|
||||
.. note::
|
||||
``DeprecationWarning`` and ``PendingDeprecationWarning`` are treated
|
||||
differently; see :ref:`ensuring_function_triggers`.
|
||||
|
||||
.. _`ensuring a function triggers a deprecation warning`:
|
||||
|
||||
.. _ensuring_function_triggers:
|
||||
|
||||
Ensuring a function triggers a deprecation warning
|
||||
-------------------------------------------------------
|
||||
|
||||
You can also call a global helper for checking
|
||||
that a certain function call triggers a ``DeprecationWarning`` or
|
||||
``PendingDeprecationWarning``::
|
||||
|
||||
import pytest
|
||||
|
||||
def test_global():
|
||||
pytest.deprecated_call(myfunction, 17)
|
||||
|
||||
By default, ``DeprecationWarning`` and ``PendingDeprecationWarning`` will not be
|
||||
caught when using ``pytest.warns`` or ``recwarn`` because default Python warnings filters hide
|
||||
them. If you wish to record them in your own code, use the
|
||||
command ``warnings.simplefilter('always')``::
|
||||
|
||||
import warnings
|
||||
import pytest
|
||||
|
||||
def test_deprecation(recwarn):
|
||||
warnings.simplefilter('always')
|
||||
warnings.warn("deprecated", DeprecationWarning)
|
||||
assert len(recwarn) == 1
|
||||
assert recwarn.pop(DeprecationWarning)
|
||||
|
||||
You can also use it as a contextmanager::
|
||||
|
||||
def test_global():
|
||||
with pytest.deprecated_call():
|
||||
myobject.deprecated_method()
|
||||
This page has been moved, please see :ref:`assertwarnings`.
|
||||
|
|
|
@ -0,0 +1,218 @@
|
|||
.. _`warnings`:
|
||||
|
||||
Warnings Capture
|
||||
================
|
||||
|
||||
.. versionadded:: 3.1
|
||||
|
||||
Starting from version ``3.1``, pytest now automatically catches all warnings during test execution
|
||||
and displays them at the end of the session::
|
||||
|
||||
# content of test_show_warnings.py
|
||||
import warnings
|
||||
|
||||
def deprecated_function():
|
||||
warnings.warn("this function is deprecated, use another_function()", DeprecationWarning)
|
||||
return 1
|
||||
|
||||
def test_one():
|
||||
assert deprecated_function() == 1
|
||||
|
||||
Running pytest now produces this output::
|
||||
|
||||
$ pytest test_show_warnings.py
|
||||
.
|
||||
============================== warnings summary ===============================
|
||||
test_show_warning.py::test_one
|
||||
C:\pytest\.tmp\test_show_warning.py:4: DeprecationWarning: this function is deprecated, use another_function()
|
||||
warnings.warn("this function is deprecated, use another_function()", DeprecationWarning)
|
||||
|
||||
-- Docs: http://doc.pytest.org/en/latest/warnings.html
|
||||
1 passed, 1 warnings in 0.01 seconds
|
||||
|
||||
The ``-W`` flag can be passed to control which warnings will be displayed or even turn
|
||||
them into errors::
|
||||
|
||||
$ pytest -q test_show_warning.py -W error::DeprecationWarning
|
||||
F
|
||||
================================== FAILURES ===================================
|
||||
__________________________________ test_one ___________________________________
|
||||
|
||||
def test_one():
|
||||
> assert deprecated_function() == 1
|
||||
|
||||
test_show_warning.py:8:
|
||||
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
||||
|
||||
def deprecated_function():
|
||||
> warnings.warn("this function is deprecated, use another_function()", DeprecationWarning)
|
||||
E DeprecationWarning: this function is deprecated, use another_function()
|
||||
|
||||
test_show_warning.py:4: DeprecationWarning
|
||||
1 failed in 0.02 seconds
|
||||
|
||||
The same option can be set in the ``pytest.ini`` file using the ``filterwarnings`` ini option.
|
||||
For example, the configuration below will ignore all deprecation warnings, but will transform
|
||||
all other warnings into errors.
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[pytest]
|
||||
filterwarnings =
|
||||
error
|
||||
ignore::DeprecationWarning
|
||||
|
||||
|
||||
When a warning matches more than one option in the list, the action for the last matching option
|
||||
is performed.
|
||||
|
||||
Both ``-W`` command-line option and ``filterwarnings`` ini option are based on Python's own
|
||||
`-W option`_ and `warnings.simplefilter`_, so please refer to those sections in the Python
|
||||
documentation for other examples and advanced usage.
|
||||
|
||||
*Credits go to Florian Schulze for the reference implementation in the* `pytest-warnings`_
|
||||
*plugin.*
|
||||
|
||||
.. _`-W option`: https://docs.python.org/3/using/cmdline.html?highlight=#cmdoption-W
|
||||
.. _warnings.simplefilter: https://docs.python.org/3/library/warnings.html#warnings.simplefilter
|
||||
.. _`pytest-warnings`: https://github.com/fschulze/pytest-warnings
|
||||
|
||||
.. _`asserting warnings`:
|
||||
|
||||
.. _assertwarnings:
|
||||
|
||||
.. _`asserting warnings with the warns function`:
|
||||
|
||||
.. _warns:
|
||||
|
||||
Asserting warnings with the warns function
|
||||
-----------------------------------------------
|
||||
|
||||
.. versionadded:: 2.8
|
||||
|
||||
You can check that code raises a particular warning using ``pytest.warns``,
|
||||
which works in a similar manner to :ref:`raises <assertraises>`::
|
||||
|
||||
import warnings
|
||||
import pytest
|
||||
|
||||
def test_warning():
|
||||
with pytest.warns(UserWarning):
|
||||
warnings.warn("my warning", UserWarning)
|
||||
|
||||
The test will fail if the warning in question is not raised.
|
||||
|
||||
You can also call ``pytest.warns`` on a function or code string::
|
||||
|
||||
pytest.warns(expected_warning, func, *args, **kwargs)
|
||||
pytest.warns(expected_warning, "func(*args, **kwargs)")
|
||||
|
||||
The function also returns a list of all raised warnings (as
|
||||
``warnings.WarningMessage`` objects), which you can query for
|
||||
additional information::
|
||||
|
||||
with pytest.warns(RuntimeWarning) as record:
|
||||
warnings.warn("another warning", RuntimeWarning)
|
||||
|
||||
# check that only one warning was raised
|
||||
assert len(record) == 1
|
||||
# check that the message matches
|
||||
assert record[0].message.args[0] == "another warning"
|
||||
|
||||
Alternatively, you can examine raised warnings in detail using the
|
||||
:ref:`recwarn <recwarn>` fixture (see below).
|
||||
|
||||
.. note::
|
||||
``DeprecationWarning`` and ``PendingDeprecationWarning`` are treated
|
||||
differently; see :ref:`ensuring_function_triggers`.
|
||||
|
||||
.. _`recording warnings`:
|
||||
|
||||
.. _recwarn:
|
||||
|
||||
Recording warnings
|
||||
------------------------
|
||||
|
||||
You can record raised warnings either using ``pytest.warns`` or with
|
||||
the ``recwarn`` fixture.
|
||||
|
||||
To record with ``pytest.warns`` without asserting anything about the warnings,
|
||||
pass ``None`` as the expected warning type::
|
||||
|
||||
with pytest.warns(None) as record:
|
||||
warnings.warn("user", UserWarning)
|
||||
warnings.warn("runtime", RuntimeWarning)
|
||||
|
||||
assert len(record) == 2
|
||||
assert str(record[0].message) == "user"
|
||||
assert str(record[1].message) == "runtime"
|
||||
|
||||
The ``recwarn`` fixture will record warnings for the whole function::
|
||||
|
||||
import warnings
|
||||
|
||||
def test_hello(recwarn):
|
||||
warnings.warn("hello", UserWarning)
|
||||
assert len(recwarn) == 1
|
||||
w = recwarn.pop(UserWarning)
|
||||
assert issubclass(w.category, UserWarning)
|
||||
assert str(w.message) == "hello"
|
||||
assert w.filename
|
||||
assert w.lineno
|
||||
|
||||
Both ``recwarn`` and ``pytest.warns`` return the same interface for recorded
|
||||
warnings: a WarningsRecorder instance. To view the recorded warnings, you can
|
||||
iterate over this instance, call ``len`` on it to get the number of recorded
|
||||
warnings, or index into it to get a particular recorded warning. It also
|
||||
provides these methods:
|
||||
|
||||
.. autoclass:: _pytest.recwarn.WarningsRecorder()
|
||||
:members:
|
||||
|
||||
Each recorded warning has the attributes ``message``, ``category``,
|
||||
``filename``, ``lineno``, ``file``, and ``line``. The ``category`` is the
|
||||
class of the warning. The ``message`` is the warning itself; calling
|
||||
``str(message)`` will return the actual message of the warning.
|
||||
|
||||
.. note::
|
||||
:class:`RecordedWarning` was changed from a plain class to a namedtuple in pytest 3.1
|
||||
|
||||
.. note::
|
||||
``DeprecationWarning`` and ``PendingDeprecationWarning`` are treated
|
||||
differently; see :ref:`ensuring_function_triggers`.
|
||||
|
||||
.. _`ensuring a function triggers a deprecation warning`:
|
||||
|
||||
.. _ensuring_function_triggers:
|
||||
|
||||
Ensuring a function triggers a deprecation warning
|
||||
-------------------------------------------------------
|
||||
|
||||
You can also call a global helper for checking
|
||||
that a certain function call triggers a ``DeprecationWarning`` or
|
||||
``PendingDeprecationWarning``::
|
||||
|
||||
import pytest
|
||||
|
||||
def test_global():
|
||||
pytest.deprecated_call(myfunction, 17)
|
||||
|
||||
By default, ``DeprecationWarning`` and ``PendingDeprecationWarning`` will not be
|
||||
caught when using ``pytest.warns`` or ``recwarn`` because default Python warnings filters hide
|
||||
them. If you wish to record them in your own code, use the
|
||||
command ``warnings.simplefilter('always')``::
|
||||
|
||||
import warnings
|
||||
import pytest
|
||||
|
||||
def test_deprecation(recwarn):
|
||||
warnings.simplefilter('always')
|
||||
warnings.warn("deprecated", DeprecationWarning)
|
||||
assert len(recwarn) == 1
|
||||
assert recwarn.pop(DeprecationWarning)
|
||||
|
||||
You can also use it as a contextmanager::
|
||||
|
||||
def test_global():
|
||||
with pytest.deprecated_call():
|
||||
myobject.deprecated_method()
|
|
@ -339,10 +339,16 @@ class TestGeneralUsage(object):
|
|||
"*ERROR*test_b.py::b*",
|
||||
])
|
||||
|
||||
@pytest.mark.usefixtures('recwarn')
|
||||
def test_namespace_import_doesnt_confuse_import_hook(self, testdir):
|
||||
# Ref #383. Python 3.3's namespace package messed with our import hooks
|
||||
# Importing a module that didn't exist, even if the ImportError was
|
||||
# gracefully handled, would make our test crash.
|
||||
"""
|
||||
Ref #383. Python 3.3's namespace package messed with our import hooks
|
||||
Importing a module that didn't exist, even if the ImportError was
|
||||
gracefully handled, would make our test crash.
|
||||
|
||||
Use recwarn here to silence this warning in Python 2.6 and 2.7:
|
||||
ImportWarning: Not importing directory '...\not_a_package': missing __init__.py
|
||||
"""
|
||||
testdir.mkdir('not_a_package')
|
||||
p = testdir.makepyfile("""
|
||||
try:
|
||||
|
@ -524,6 +530,7 @@ class TestInvocationVariants(object):
|
|||
])
|
||||
|
||||
def test_cmdline_python_package(self, testdir, monkeypatch):
|
||||
import warnings
|
||||
monkeypatch.delenv('PYTHONDONTWRITEBYTECODE', False)
|
||||
path = testdir.mkpydir("tpkg")
|
||||
path.join("test_hello.py").write("def test_hello(): pass")
|
||||
|
@ -546,6 +553,10 @@ class TestInvocationVariants(object):
|
|||
return what
|
||||
empty_package = testdir.mkpydir("empty_package")
|
||||
monkeypatch.setenv('PYTHONPATH', join_pythonpath(empty_package))
|
||||
# the path which is not a package raises a warning on pypy;
|
||||
# no idea why only pypy and not normal python warn about it here
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('ignore', ImportWarning)
|
||||
result = testdir.runpytest("--pyargs", ".")
|
||||
assert result.ret == 0
|
||||
result.stdout.fnmatch_lines([
|
||||
|
|
|
@ -27,7 +27,7 @@ def test_funcarg_prefix_deprecation(testdir):
|
|||
""")
|
||||
result = testdir.runpytest('-ra')
|
||||
result.stdout.fnmatch_lines([
|
||||
('WC1 None pytest_funcarg__value: '
|
||||
('*pytest_funcarg__value: '
|
||||
'declaring fixtures using "pytest_funcarg__" prefix is deprecated '
|
||||
'and scheduled to be removed in pytest 4.0. '
|
||||
'Please remove the prefix and use the @pytest.fixture decorator instead.'),
|
||||
|
|
|
@ -113,9 +113,9 @@ class TestClass(object):
|
|||
pass
|
||||
""")
|
||||
result = testdir.runpytest("-rw")
|
||||
result.stdout.fnmatch_lines_random("""
|
||||
WC1*test_class_with_init_warning.py*__init__*
|
||||
""")
|
||||
result.stdout.fnmatch_lines([
|
||||
"*cannot collect test class 'TestClass1' because it has a __init__ constructor",
|
||||
])
|
||||
|
||||
def test_class_subclassobject(self, testdir):
|
||||
testdir.getmodulecol("""
|
||||
|
@ -1243,8 +1243,8 @@ def test_dont_collect_non_function_callable(testdir):
|
|||
result = testdir.runpytest('-rw')
|
||||
result.stdout.fnmatch_lines([
|
||||
'*collected 1 item*',
|
||||
'WC2 *',
|
||||
'*1 passed, 1 pytest-warnings in *',
|
||||
"*cannot collect 'test_a' because it is not a function*",
|
||||
'*1 passed, 1 warnings in *',
|
||||
])
|
||||
|
||||
|
||||
|
|
|
@ -545,9 +545,21 @@ class TestRequestBasic(object):
|
|||
return l.pop()
|
||||
def test_func(something): pass
|
||||
""")
|
||||
import contextlib
|
||||
if getfixmethod == 'getfuncargvalue':
|
||||
warning_expectation = pytest.warns(DeprecationWarning)
|
||||
else:
|
||||
# see #1830 for a cleaner way to accomplish this
|
||||
@contextlib.contextmanager
|
||||
def expecting_no_warning(): yield
|
||||
|
||||
warning_expectation = expecting_no_warning()
|
||||
|
||||
req = item._request
|
||||
with warning_expectation:
|
||||
fixture_fetcher = getattr(req, getfixmethod)
|
||||
pytest.raises(FixtureLookupError, fixture_fetcher, "notexists")
|
||||
with pytest.raises(FixtureLookupError):
|
||||
fixture_fetcher("notexists")
|
||||
val = fixture_fetcher("something")
|
||||
assert val == 1
|
||||
val = fixture_fetcher("something")
|
||||
|
@ -560,7 +572,6 @@ class TestRequestBasic(object):
|
|||
assert item.funcargs["something"] == 1
|
||||
assert len(get_public_names(item.funcargs)) == 2
|
||||
assert "request" in item.funcargs
|
||||
#assert item.funcargs == {'something': 1, "other": 2}
|
||||
|
||||
def test_request_addfinalizer(self, testdir):
|
||||
item = testdir.getitem("""
|
||||
|
|
|
@ -347,6 +347,7 @@ class TestMetafunc(object):
|
|||
def test_foo(arg):
|
||||
pass
|
||||
""")
|
||||
with pytest.warns(DeprecationWarning):
|
||||
result = testdir.runpytest("--collect-only")
|
||||
result.stdout.fnmatch_lines([
|
||||
"<Module 'test_parametrize_ids_exception.py'>",
|
||||
|
|
|
@ -976,7 +976,10 @@ def test_assert_tuple_warning(testdir):
|
|||
assert(False, 'you shall not pass')
|
||||
""")
|
||||
result = testdir.runpytest('-rw')
|
||||
result.stdout.fnmatch_lines('WR1*:2 assertion is always true*')
|
||||
result.stdout.fnmatch_lines([
|
||||
'*test_assert_tuple_warning.py:2',
|
||||
'*assertion is always true*',
|
||||
])
|
||||
|
||||
def test_assert_indirect_tuple_no_warning(testdir):
|
||||
testdir.makepyfile("""
|
||||
|
|
|
@ -55,7 +55,7 @@ class TestNewAPI(object):
|
|||
assert result.ret == 1
|
||||
result.stdout.fnmatch_lines([
|
||||
"*could not create cache path*",
|
||||
"*1 pytest-warnings*",
|
||||
"*1 warnings*",
|
||||
])
|
||||
|
||||
def test_config_cache(self, testdir):
|
||||
|
|
|
@ -141,7 +141,7 @@ class TestConfigAPI(object):
|
|||
from __future__ import unicode_literals
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption('--hello', type='string')
|
||||
parser.addoption('--hello', type=str)
|
||||
""")
|
||||
config = testdir.parseconfig('--hello=this')
|
||||
assert config.getoption('hello') == 'this'
|
||||
|
@ -633,13 +633,14 @@ class TestWarning(object):
|
|||
pass
|
||||
""")
|
||||
result = testdir.runpytest("--disable-pytest-warnings")
|
||||
assert result.parseoutcomes()["pytest-warnings"] > 0
|
||||
assert result.parseoutcomes()["warnings"] > 0
|
||||
assert "hello" not in result.stdout.str()
|
||||
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines("""
|
||||
===*pytest-warning summary*===
|
||||
*WT1*test_warn_on_test_item*:7 hello*
|
||||
===*warnings summary*===
|
||||
*test_warn_on_test_item_from_request.py::test_hello*
|
||||
*hello*
|
||||
""")
|
||||
|
||||
class TestRootdir(object):
|
||||
|
|
|
@ -852,7 +852,10 @@ def test_record_property(testdir):
|
|||
pnodes = psnode.find_by_tag('property')
|
||||
pnodes[0].assert_attr(name="bar", value="1")
|
||||
pnodes[1].assert_attr(name="foo", value="<1")
|
||||
result.stdout.fnmatch_lines('*C3*test_record_property.py*experimental*')
|
||||
result.stdout.fnmatch_lines([
|
||||
'test_record_property.py::test_record',
|
||||
'*record_xml_property*experimental*',
|
||||
])
|
||||
|
||||
|
||||
def test_record_property_same_name(testdir):
|
||||
|
|
|
@ -34,15 +34,16 @@ class TestParser(object):
|
|||
)
|
||||
|
||||
def test_argument_type(self):
|
||||
argument = parseopt.Argument('-t', dest='abc', type='int')
|
||||
argument = parseopt.Argument('-t', dest='abc', type=int)
|
||||
assert argument.type is int
|
||||
argument = parseopt.Argument('-t', dest='abc', type='string')
|
||||
argument = parseopt.Argument('-t', dest='abc', type=str)
|
||||
assert argument.type is str
|
||||
argument = parseopt.Argument('-t', dest='abc', type=float)
|
||||
assert argument.type is float
|
||||
with pytest.warns(DeprecationWarning):
|
||||
with pytest.raises(KeyError):
|
||||
argument = parseopt.Argument('-t', dest='abc', type='choice')
|
||||
argument = parseopt.Argument('-t', dest='abc', type='choice',
|
||||
argument = parseopt.Argument('-t', dest='abc', type=str,
|
||||
choices=['red', 'blue'])
|
||||
assert argument.type is str
|
||||
|
||||
|
@ -176,8 +177,8 @@ class TestParser(object):
|
|||
elif option.type is str:
|
||||
option.default = "world"
|
||||
parser = parseopt.Parser(processopt=defaultget)
|
||||
parser.addoption("--this", dest="this", type="int", action="store")
|
||||
parser.addoption("--hello", dest="hello", type="string", action="store")
|
||||
parser.addoption("--this", dest="this", type=int, action="store")
|
||||
parser.addoption("--hello", dest="hello", type=str, action="store")
|
||||
parser.addoption("--no", dest="no", action="store_true")
|
||||
option = parser.parse([])
|
||||
assert option.hello == "world"
|
||||
|
|
|
@ -285,8 +285,8 @@ class TestPytestPluginManager(object):
|
|||
result = testdir.runpytest("-rw", "-p", "skipping1", syspathinsert=True)
|
||||
assert result.ret == EXIT_NOTESTSCOLLECTED
|
||||
result.stdout.fnmatch_lines([
|
||||
"WI1*skipped plugin*skipping1*hello*",
|
||||
"WI1*skipped plugin*skipping2*hello*",
|
||||
"*skipped plugin*skipping1*hello*",
|
||||
"*skipped plugin*skipping2*hello*",
|
||||
])
|
||||
|
||||
def test_consider_env_plugin_instantiation(self, testdir, monkeypatch, pytestpm):
|
||||
|
|
|
@ -146,7 +146,9 @@ class TestDeprecatedCall(object):
|
|||
pytest.deprecated_call(deprecated_function)
|
||||
""")
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines('*=== 2 passed in *===')
|
||||
# the 2 tests must pass, but the call to test_one() will generate a warning
|
||||
# in pytest's summary
|
||||
result.stdout.fnmatch_lines('*=== 2 passed, 1 warnings in *===')
|
||||
|
||||
|
||||
class TestWarns(object):
|
||||
|
|
|
@ -616,7 +616,7 @@ def test_getreportopt():
|
|||
class config(object):
|
||||
class option(object):
|
||||
reportchars = ""
|
||||
disablepytestwarnings = True
|
||||
disable_warnings = True
|
||||
|
||||
config.option.reportchars = "sf"
|
||||
assert getreportopt(config) == "sf"
|
||||
|
@ -625,11 +625,11 @@ def test_getreportopt():
|
|||
assert getreportopt(config) == "sfx"
|
||||
|
||||
config.option.reportchars = "sfx"
|
||||
config.option.disablepytestwarnings = False
|
||||
config.option.disable_warnings = False
|
||||
assert getreportopt(config) == "sfxw"
|
||||
|
||||
config.option.reportchars = "sfxw"
|
||||
config.option.disablepytestwarnings = False
|
||||
config.option.disable_warnings = False
|
||||
assert getreportopt(config) == "sfxw"
|
||||
|
||||
|
||||
|
@ -838,8 +838,8 @@ def test_terminal_summary_warnings_are_displayed(testdir):
|
|||
""")
|
||||
result = testdir.runpytest('-rw')
|
||||
result.stdout.fnmatch_lines([
|
||||
'*C1*internal warning',
|
||||
'*== 1 pytest-warnings in *',
|
||||
'*internal warning',
|
||||
'*== 1 warnings in *',
|
||||
])
|
||||
|
||||
|
||||
|
@ -859,8 +859,8 @@ def test_terminal_summary_warnings_are_displayed(testdir):
|
|||
("yellow", "1 weird", {"weird": (1,)}),
|
||||
("yellow", "1 passed, 1 weird", {"weird": (1,), "passed": (1,)}),
|
||||
|
||||
("yellow", "1 pytest-warnings", {"warnings": (1,)}),
|
||||
("yellow", "1 passed, 1 pytest-warnings", {"warnings": (1,),
|
||||
("yellow", "1 warnings", {"warnings": (1,)}),
|
||||
("yellow", "1 passed, 1 warnings", {"warnings": (1,),
|
||||
"passed": (1,)}),
|
||||
|
||||
("green", "5 passed", {"passed": (1,2,3,4,5)}),
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
import pytest
|
||||
|
||||
|
||||
WARNINGS_SUMMARY_HEADER = 'warnings summary'
|
||||
|
||||
@pytest.fixture
|
||||
def pyfile_with_warnings(testdir, request):
|
||||
"""
|
||||
Create a test file which calls a function in a module which generates warnings.
|
||||
"""
|
||||
testdir.syspathinsert()
|
||||
test_name = request.function.__name__
|
||||
module_name = test_name.lstrip('test_') + '_module'
|
||||
testdir.makepyfile(**{
|
||||
module_name: '''
|
||||
import warnings
|
||||
def foo():
|
||||
warnings.warn(PendingDeprecationWarning("functionality is pending deprecation"))
|
||||
warnings.warn(DeprecationWarning("functionality is deprecated"))
|
||||
return 1
|
||||
''',
|
||||
test_name: '''
|
||||
import {module_name}
|
||||
def test_func():
|
||||
assert {module_name}.foo() == 1
|
||||
'''.format(module_name=module_name)
|
||||
})
|
||||
|
||||
|
||||
def test_normal_flow(testdir, pyfile_with_warnings):
|
||||
"""
|
||||
Check that the warnings section is displayed, containing test node ids followed by
|
||||
all warnings generated by that test node.
|
||||
"""
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines([
|
||||
'*== %s ==*' % WARNINGS_SUMMARY_HEADER,
|
||||
|
||||
'*test_normal_flow.py::test_func',
|
||||
|
||||
'*normal_flow_module.py:3: PendingDeprecationWarning: functionality is pending deprecation',
|
||||
'* warnings.warn(PendingDeprecationWarning("functionality is pending deprecation"))',
|
||||
|
||||
'*normal_flow_module.py:4: DeprecationWarning: functionality is deprecated',
|
||||
'* warnings.warn(DeprecationWarning("functionality is deprecated"))',
|
||||
'* 1 passed, 2 warnings*',
|
||||
])
|
||||
assert result.stdout.str().count('test_normal_flow.py::test_func') == 1
|
||||
|
||||
|
||||
def test_setup_teardown_warnings(testdir, pyfile_with_warnings):
|
||||
testdir.makepyfile('''
|
||||
import warnings
|
||||
import pytest
|
||||
|
||||
@pytest.fixture
|
||||
def fix():
|
||||
warnings.warn(UserWarning("warning during setup"))
|
||||
yield
|
||||
warnings.warn(UserWarning("warning during teardown"))
|
||||
|
||||
def test_func(fix):
|
||||
pass
|
||||
''')
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines([
|
||||
'*== %s ==*' % WARNINGS_SUMMARY_HEADER,
|
||||
|
||||
'*test_setup_teardown_warnings.py:6: UserWarning: warning during setup',
|
||||
'*warnings.warn(UserWarning("warning during setup"))',
|
||||
|
||||
'*test_setup_teardown_warnings.py:8: UserWarning: warning during teardown',
|
||||
'*warnings.warn(UserWarning("warning during teardown"))',
|
||||
'* 1 passed, 2 warnings*',
|
||||
])
|
||||
|
||||
|
||||
@pytest.mark.parametrize('method', ['cmdline', 'ini'])
|
||||
def test_as_errors(testdir, pyfile_with_warnings, method):
|
||||
args = ('-W', 'error') if method == 'cmdline' else ()
|
||||
if method == 'ini':
|
||||
testdir.makeini('''
|
||||
[pytest]
|
||||
filterwarnings= error
|
||||
''')
|
||||
result = testdir.runpytest(*args)
|
||||
result.stdout.fnmatch_lines([
|
||||
'E PendingDeprecationWarning: functionality is pending deprecation',
|
||||
'as_errors_module.py:3: PendingDeprecationWarning',
|
||||
'* 1 failed in *',
|
||||
])
|
||||
|
||||
|
||||
@pytest.mark.parametrize('method', ['cmdline', 'ini'])
|
||||
def test_ignore(testdir, pyfile_with_warnings, method):
|
||||
args = ('-W', 'ignore') if method == 'cmdline' else ()
|
||||
if method == 'ini':
|
||||
testdir.makeini('''
|
||||
[pytest]
|
||||
filterwarnings= ignore
|
||||
''')
|
||||
|
||||
result = testdir.runpytest(*args)
|
||||
result.stdout.fnmatch_lines([
|
||||
'* 1 passed in *',
|
||||
])
|
||||
assert WARNINGS_SUMMARY_HEADER not in result.stdout.str()
|
||||
|
10
tox.ini
10
tox.ini
|
@ -176,7 +176,15 @@ python_files=test_*.py *_test.py testing/*/*.py
|
|||
python_classes=Test Acceptance
|
||||
python_functions=test
|
||||
norecursedirs = .tox ja .hg cx_freeze_source
|
||||
|
||||
filterwarnings= error
|
||||
# produced by path.local
|
||||
ignore:bad escape.*:DeprecationWarning:re
|
||||
# produced by path.readlines
|
||||
ignore:.*U.*mode is deprecated:DeprecationWarning
|
||||
# produced by pytest-xdist
|
||||
ignore:.*type argument to addoption.*:DeprecationWarning
|
||||
# produced by python >=3.5 on execnet (pytest-xdist)
|
||||
ignore:.*inspect.getargspec.*deprecated, use inspect.signature.*:DeprecationWarning
|
||||
|
||||
[flake8]
|
||||
ignore =E401,E225,E261,E128,E124,E301,E302,E121,E303,W391,E501,E231,E126,E701,E265,E241,E251,E226,E101,W191,E131,E203,E122,E123,E271,E712,E222,E127,E125,E221,W292,E111,E113,E293,E262,W293,E129,E702,E201,E272,E202,E704,E731,E402
|
||||
|
|
Loading…
Reference in New Issue