Skip to content

Commit

Permalink
Merge pull request #897 from hunse/warns
Browse files Browse the repository at this point in the history
Warns
  • Loading branch information
nicoddemus committed Aug 4, 2015
2 parents 8fca478 + 52b4eb6 commit d761bff
Show file tree
Hide file tree
Showing 9 changed files with 405 additions and 145 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Eduardo Schettino
Eric Siegerman
Florian Bruhin
Edison Gustavo Muenz
Eric Hunsberger
Floris Bruynooghe
Graham Horler
Grig Gheorghiu
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
2.8.0.dev (compared to 2.7.X)
-----------------------------

- Add 'warns' to assert that warnings are thrown (like 'raises').
Thanks to Eric Hunsberger for the PR.

- Fix #683: Do not apply an already applied mark. Thanks ojake for the PR.

- Deal with capturing failures better so fewer exceptions get lost to
Expand Down
4 changes: 4 additions & 0 deletions CONTRIBUTING.rst
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,10 @@ but here is a simple overview:
$ git commit -a -m "<commit message>"
$ git push -u

Make sure you add a CHANGELOG message, and add yourself to AUTHORS. If you
are unsure about either of these steps, submit your pull request and we'll
help you fix it up.

#. Finally, submit a pull request through the GitHub website:

.. image:: img/pullrequest.png
Expand Down
29 changes: 14 additions & 15 deletions _pytest/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -1052,8 +1052,8 @@ def getlocation(function, curdir):

# builtin pytest.raises helper

def raises(ExpectedException, *args, **kwargs):
""" assert that a code block/function call raises @ExpectedException
def raises(expected_exception, *args, **kwargs):
""" assert that a code block/function call raises @expected_exception
and raise a failure exception otherwise.
This helper produces a ``py.code.ExceptionInfo()`` object.
Expand Down Expand Up @@ -1101,23 +1101,23 @@ def raises(ExpectedException, *args, **kwargs):
"""
__tracebackhide__ = True
if ExpectedException is AssertionError:
if expected_exception is AssertionError:
# we want to catch a AssertionError
# replace our subclass with the builtin one
# see https://github.com/pytest-dev/pytest/issues/176
from _pytest.assertion.util import BuiltinAssertionError \
as ExpectedException
as expected_exception
msg = ("exceptions must be old-style classes or"
" derived from BaseException, not %s")
if isinstance(ExpectedException, tuple):
for exc in ExpectedException:
if isinstance(expected_exception, tuple):
for exc in expected_exception:
if not inspect.isclass(exc):
raise TypeError(msg % type(exc))
elif not inspect.isclass(ExpectedException):
raise TypeError(msg % type(ExpectedException))
elif not inspect.isclass(expected_exception):
raise TypeError(msg % type(expected_exception))

if not args:
return RaisesContext(ExpectedException)
return RaisesContext(expected_exception)
elif isinstance(args[0], str):
code, = args
assert isinstance(code, str)
Expand All @@ -1130,19 +1130,19 @@ def raises(ExpectedException, *args, **kwargs):
py.builtin.exec_(code, frame.f_globals, loc)
# XXX didn'T mean f_globals == f_locals something special?
# this is destroyed here ...
except ExpectedException:
except expected_exception:
return py.code.ExceptionInfo()
else:
func = args[0]
try:
func(*args[1:], **kwargs)
except ExpectedException:
except expected_exception:
return py.code.ExceptionInfo()
pytest.fail("DID NOT RAISE")

class RaisesContext(object):
def __init__(self, ExpectedException):
self.ExpectedException = ExpectedException
def __init__(self, expected_exception):
self.expected_exception = expected_exception
self.excinfo = None

def __enter__(self):
Expand All @@ -1161,7 +1161,7 @@ def __exit__(self, *tp):
exc_type, value, traceback = tp
tp = exc_type, exc_type(value), traceback
self.excinfo.__init__(tp)
return issubclass(self.excinfo.type, self.ExpectedException)
return issubclass(self.excinfo.type, self.expected_exception)

#
# the basic pytest Function item
Expand Down Expand Up @@ -2123,4 +2123,3 @@ def get_scope_node(node, scope):
return node.session
raise ValueError("unknown scope")
return node.getparent(cls)

204 changes: 147 additions & 57 deletions _pytest/recwarn.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
""" recording warnings during test function execution. """

import inspect
import py
import sys
import warnings
import pytest


def pytest_funcarg__recwarn(request):
@pytest.yield_fixture
def recwarn(request):
"""Return a WarningsRecorder instance that provides these methods:
* ``pop(category=None)``: return last warning matching the category.
Expand All @@ -13,83 +17,169 @@ def pytest_funcarg__recwarn(request):
See http://docs.python.org/library/warnings.html for information
on warning categories.
"""
if sys.version_info >= (2,7):
oldfilters = warnings.filters[:]
warnings.simplefilter('default')
def reset_filters():
warnings.filters[:] = oldfilters
request.addfinalizer(reset_filters)
wrec = WarningsRecorder()
request.addfinalizer(wrec.finalize)
return wrec
with wrec:
warnings.simplefilter('default')
yield wrec


def pytest_namespace():
return {'deprecated_call': deprecated_call}
return {'deprecated_call': deprecated_call,
'warns': warns}


def deprecated_call(func, *args, **kwargs):
""" assert that calling ``func(*args, **kwargs)``
triggers a DeprecationWarning.
"""Assert that ``func(*args, **kwargs)`` triggers a DeprecationWarning.
"""
l = []
oldwarn_explicit = getattr(warnings, 'warn_explicit')
def warn_explicit(*args, **kwargs):
l.append(args)
oldwarn_explicit(*args, **kwargs)
oldwarn = getattr(warnings, 'warn')
def warn(*args, **kwargs):
l.append(args)
oldwarn(*args, **kwargs)

warnings.warn_explicit = warn_explicit
warnings.warn = warn
try:
wrec = WarningsRecorder()
with wrec:
warnings.simplefilter('always') # ensure all warnings are triggered
ret = func(*args, **kwargs)
finally:
warnings.warn_explicit = oldwarn_explicit
warnings.warn = oldwarn
if not l:

if not any(r.category is DeprecationWarning for r in wrec):
__tracebackhide__ = True
raise AssertionError("%r did not produce DeprecationWarning" %(func,))
raise AssertionError("%r did not produce DeprecationWarning" % (func,))

return ret


class RecordedWarning:
def __init__(self, message, category, filename, lineno, line):
def warns(expected_warning, *args, **kwargs):
"""Assert that code raises a particular class of warning.
Specifically, the input @expected_warning can be a warning class or
tuple of warning classes, and the code must return that warning
(if a single class) or one of those warnings (if a tuple).
This helper produces a list of ``warnings.WarningMessage`` objects,
one for each warning raised.
This function can be used as a context manager, or any of the other ways
``pytest.raises`` can be used::
>>> with warns(RuntimeWarning):
... warnings.warn("my warning", RuntimeWarning)
"""
wcheck = WarningsChecker(expected_warning)
if not args:
return wcheck
elif isinstance(args[0], str):
code, = args
assert isinstance(code, str)
frame = sys._getframe(1)
loc = frame.f_locals.copy()
loc.update(kwargs)

with wcheck:
code = py.code.Source(code).compile()
py.builtin.exec_(code, frame.f_globals, loc)
else:
func = args[0]
with wcheck:
return func(*args[1:], **kwargs)


class RecordedWarning(object):
def __init__(self, message, category, filename, lineno, file, line):
self.message = message
self.category = category
self.filename = filename
self.lineno = lineno
self.file = file
self.line = line

class WarningsRecorder:
def __init__(self):
self.list = []
def showwarning(message, category, filename, lineno, line=0):
self.list.append(RecordedWarning(
message, category, filename, lineno, line))
try:
self.old_showwarning(message, category,
filename, lineno, line=line)
except TypeError:
# < python2.6
self.old_showwarning(message, category, filename, lineno)
self.old_showwarning = warnings.showwarning
warnings.showwarning = showwarning

class WarningsRecorder(object):
"""A context manager to record raised warnings.
Adapted from `warnings.catch_warnings`.
"""

def __init__(self, module=None):
self._module = sys.modules['warnings'] if module is None else module
self._entered = False
self._list = []

@property
def list(self):
"""The list of recorded warnings."""
return self._list

def __getitem__(self, i):
"""Get a recorded warning by index."""
return self._list[i]

def __iter__(self):
"""Iterate through the recorded warnings."""
return iter(self._list)

def __len__(self):
"""The number of recorded warnings."""
return len(self._list)

def pop(self, cls=Warning):
""" pop the first recorded warning, raise exception if not exists."""
for i, w in enumerate(self.list):
"""Pop the first recorded warning, raise exception if not exists."""
for i, w in enumerate(self._list):
if issubclass(w.category, cls):
return self.list.pop(i)
return self._list.pop(i)
__tracebackhide__ = True
assert 0, "%r not found in %r" %(cls, self.list)

#def resetregistry(self):
# warnings.onceregistry.clear()
# warnings.__warningregistry__.clear()
raise AssertionError("%r not found in warning list" % cls)

def clear(self):
self.list[:] = []
"""Clear the list of recorded warnings."""
self._list[:] = []

def __enter__(self):
if self._entered:
__tracebackhide__ = True
raise RuntimeError("Cannot enter %r twice" % self)
self._entered = True
self._filters = self._module.filters
self._module.filters = self._filters[:]
self._showwarning = self._module.showwarning

def showwarning(message, category, filename, lineno,
file=None, line=None):
self._list.append(RecordedWarning(
message, category, filename, lineno, file, line))

# still perform old showwarning functionality
self._showwarning(message, category, filename, lineno,
file=file, line=line)

self._module.showwarning = showwarning
return self

def __exit__(self, *exc_info):
if not self._entered:
__tracebackhide__ = True
raise RuntimeError("Cannot exit %r without entering first" % self)
self._module.filters = self._filters
self._module.showwarning = self._showwarning


class WarningsChecker(WarningsRecorder):
def __init__(self, expected_warning=None, module=None):
super(WarningsChecker, self).__init__(module=module)

msg = ("exceptions must be old-style classes or "
"derived from Warning, not %s")
if isinstance(expected_warning, tuple):
for exc in expected_warning:
if not inspect.isclass(exc):
raise TypeError(msg % type(exc))
elif inspect.isclass(expected_warning):
expected_warning = (expected_warning,)
elif expected_warning is not None:
raise TypeError(msg % type(expected_warning))

self.expected_warning = expected_warning

def __exit__(self, *exc_info):
super(WarningsChecker, self).__exit__(*exc_info)

def finalize(self):
warnings.showwarning = self.old_showwarning
# only check if we're not currently handling an exception
if all(a is None for a in exc_info):
if self.expected_warning is not None:
if not any(r.category in self.expected_warning for r in self):
__tracebackhide__ = True
pytest.fail("DID NOT WARN")
10 changes: 10 additions & 0 deletions doc/en/assert.txt
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,16 @@ like documenting unfixed bugs (where the test describes what "should" happen)
or bugs in dependencies.


.. _`assertwarns`:

Assertions about expected warnings
-----------------------------------------

.. versionadded:: 2.8

You can check that code raises a particular warning using
:ref:`pytest.warns <warns>`.


.. _newreport:

Expand Down
Loading

0 comments on commit d761bff

Please sign in to comment.