Skip to content
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

Warns #897

Merged
merged 2 commits into from
Aug 4, 2015
Merged

Warns #897

Show file tree
Hide file tree
Changes from all commits
Commits
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
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()
Copy link
Member

Choose a reason for hiding this comment

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

Minor: this fits nicely as a yield_fixture:

@pytest.yield_fixture
def recwarn():
    wrec = WarningsRecorder()
    with wrec:
        warnings.simplefilter('default')
        yield wrec

(should've seen it before, sorry! 😅)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh wow, that's cool. I didn't even know that was possible!

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).
Copy link
Member

Choose a reason for hiding this comment

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

Keep __tracebackhide__ in the body of the function, this way when it fails the traceback will point to the point of the call, not to the internal details of this function.


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)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@nicoddemus: how would I add documentation for these kinds of methods in recwarn.txt? There seems like there should be a clearer way than just adding them as .. method::.

Copy link
Member

Choose a reason for hiding this comment

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

You can use auto directives from sphinx... you can see them in action here.


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