Issue #9754: Similarly to assertRaises and assertRaisesRegexp, unittest
test cases now also have assertWarns and assertWarnsRegexp methods to check that a given warning type was triggered by the code under test.
This commit is contained in:
parent
972ee13e03
commit
4bc12ef47d
|
@ -1083,6 +1083,59 @@ Test cases
|
|||
.. versionadded:: 3.1
|
||||
|
||||
|
||||
.. method:: assertWarns(warning, callable, *args, **kwds)
|
||||
assertWarns(warning)
|
||||
|
||||
Test that a warning is triggered when *callable* is called with any
|
||||
positional or keyword arguments that are also passed to
|
||||
:meth:`assertWarns`. The test passes if *warning* is triggered and
|
||||
fails if it isn't. Also, any unexpected exception is an error.
|
||||
To catch any of a group of warnings, a tuple containing the warning
|
||||
classes may be passed as *warnings*.
|
||||
|
||||
If only the *warning* argument is given, returns a context manager so
|
||||
that the code under test can be written inline rather than as a function::
|
||||
|
||||
with self.assertWarns(SomeWarning):
|
||||
do_something()
|
||||
|
||||
The context manager will store the caught warning object in its
|
||||
:attr:`warning` attribute, and the source line which triggered the
|
||||
warnings in the :attr:`filename` and :attr:`lineno` attributes.
|
||||
This can be useful if the intention is to perform additional checks
|
||||
on the exception raised::
|
||||
|
||||
with self.assertWarns(SomeWarning) as cm:
|
||||
do_something()
|
||||
|
||||
self.assertIn('myfile.py', cm.filename)
|
||||
self.assertEqual(320, cm.lineno)
|
||||
|
||||
This method works regardless of the warning filters in place when it
|
||||
is called.
|
||||
|
||||
.. versionadded:: 3.2
|
||||
|
||||
|
||||
.. method:: assertWarnsRegexp(warning, regexp[, callable, ...])
|
||||
|
||||
Like :meth:`assertWarns` but also tests that *regexp* matches on the
|
||||
message of the triggered warning. *regexp* may be a regular expression
|
||||
object or a string containing a regular expression suitable for use
|
||||
by :func:`re.search`. Example::
|
||||
|
||||
self.assertWarnsRegexp(DeprecationWarning,
|
||||
r'legacy_function\(\) is deprecated',
|
||||
legacy_function, 'XYZ')
|
||||
|
||||
or::
|
||||
|
||||
with self.assertWarnsRegexp(RuntimeWarning, 'unsafe frobnicating'):
|
||||
frobnicate('/etc/passwd')
|
||||
|
||||
.. versionadded:: 3.2
|
||||
|
||||
|
||||
.. method:: assertIsNone(expr, msg=None)
|
||||
|
||||
This signals a test failure if *expr* is not None.
|
||||
|
|
|
@ -90,8 +90,7 @@ def expectedFailure(func):
|
|||
return wrapper
|
||||
|
||||
|
||||
class _AssertRaisesContext(object):
|
||||
"""A context manager used to implement TestCase.assertRaises* methods."""
|
||||
class _AssertRaisesBaseContext(object):
|
||||
|
||||
def __init__(self, expected, test_case, callable_obj=None,
|
||||
expected_regexp=None):
|
||||
|
@ -104,8 +103,14 @@ class _AssertRaisesContext(object):
|
|||
self.obj_name = str(callable_obj)
|
||||
else:
|
||||
self.obj_name = None
|
||||
if isinstance(expected_regexp, (bytes, str)):
|
||||
expected_regexp = re.compile(expected_regexp)
|
||||
self.expected_regexp = expected_regexp
|
||||
|
||||
|
||||
class _AssertRaisesContext(_AssertRaisesBaseContext):
|
||||
"""A context manager used to implement TestCase.assertRaises* methods."""
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
|
@ -130,14 +135,62 @@ class _AssertRaisesContext(object):
|
|||
return True
|
||||
|
||||
expected_regexp = self.expected_regexp
|
||||
if isinstance(expected_regexp, (bytes, str)):
|
||||
expected_regexp = re.compile(expected_regexp)
|
||||
if not expected_regexp.search(str(exc_value)):
|
||||
raise self.failureException('"%s" does not match "%s"' %
|
||||
(expected_regexp.pattern, str(exc_value)))
|
||||
return True
|
||||
|
||||
|
||||
class _AssertWarnsContext(_AssertRaisesBaseContext):
|
||||
"""A context manager used to implement TestCase.assertWarns* methods."""
|
||||
|
||||
def __enter__(self):
|
||||
# The __warningregistry__'s need to be in a pristine state for tests
|
||||
# to work properly.
|
||||
for v in sys.modules.values():
|
||||
if getattr(v, '__warningregistry__', None):
|
||||
v.__warningregistry__ = {}
|
||||
self.warnings_manager = warnings.catch_warnings(record=True)
|
||||
self.warnings = self.warnings_manager.__enter__()
|
||||
warnings.simplefilter("always", self.expected)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, tb):
|
||||
self.warnings_manager.__exit__(exc_type, exc_value, tb)
|
||||
if exc_type is not None:
|
||||
# let unexpected exceptions pass through
|
||||
return
|
||||
try:
|
||||
exc_name = self.expected.__name__
|
||||
except AttributeError:
|
||||
exc_name = str(self.expected)
|
||||
first_matching = None
|
||||
for m in self.warnings:
|
||||
w = m.message
|
||||
if not isinstance(w, self.expected):
|
||||
continue
|
||||
if first_matching is None:
|
||||
first_matching = w
|
||||
if (self.expected_regexp is not None and
|
||||
not self.expected_regexp.search(str(w))):
|
||||
continue
|
||||
# store warning for later retrieval
|
||||
self.warning = w
|
||||
self.filename = m.filename
|
||||
self.lineno = m.lineno
|
||||
return
|
||||
# Now we simply try to choose a helpful failure message
|
||||
if first_matching is not None:
|
||||
raise self.failureException('"%s" does not match "%s"' %
|
||||
(self.expected_regexp.pattern, str(first_matching)))
|
||||
if self.obj_name:
|
||||
raise self.failureException("{0} not triggered by {1}"
|
||||
.format(exc_name, self.obj_name))
|
||||
else:
|
||||
raise self.failureException("{0} not triggered"
|
||||
.format(exc_name))
|
||||
|
||||
|
||||
class TestCase(object):
|
||||
"""A class whose instances are single test cases.
|
||||
|
||||
|
@ -464,6 +517,37 @@ class TestCase(object):
|
|||
with context:
|
||||
callableObj(*args, **kwargs)
|
||||
|
||||
def assertWarns(self, expected_warning, callable_obj=None, *args, **kwargs):
|
||||
"""Fail unless a warning of class warnClass is triggered
|
||||
by callableObj when invoked with arguments args and keyword
|
||||
arguments kwargs. If a different type of warning is
|
||||
triggered, it will not be handled: depending on the other
|
||||
warning filtering rules in effect, it might be silenced, printed
|
||||
out, or raised as an exception.
|
||||
|
||||
If called with callableObj omitted or None, will return a
|
||||
context object used like this::
|
||||
|
||||
with self.assertWarns(SomeWarning):
|
||||
do_something()
|
||||
|
||||
The context manager keeps a reference to the first matching
|
||||
warning as the 'warning' attribute; similarly, the 'filename'
|
||||
and 'lineno' attributes give you information about the line
|
||||
of Python code from which the warning was triggered.
|
||||
This allows you to inspect the warning after the assertion::
|
||||
|
||||
with self.assertWarns(SomeWarning) as cm:
|
||||
do_something()
|
||||
the_warning = cm.warning
|
||||
self.assertEqual(the_warning.some_attribute, 147)
|
||||
"""
|
||||
context = _AssertWarnsContext(expected_warning, self, callable_obj)
|
||||
if callable_obj is None:
|
||||
return context
|
||||
with context:
|
||||
callable_obj(*args, **kwargs)
|
||||
|
||||
def _getAssertEqualityFunc(self, first, second):
|
||||
"""Get a detailed comparison function for the types of the two args.
|
||||
|
||||
|
@ -1019,6 +1103,28 @@ class TestCase(object):
|
|||
with context:
|
||||
callable_obj(*args, **kwargs)
|
||||
|
||||
def assertWarnsRegexp(self, expected_warning, expected_regexp,
|
||||
callable_obj=None, *args, **kwargs):
|
||||
"""Asserts that the message in a triggered warning matches a regexp.
|
||||
Basic functioning is similar to assertWarns() with the addition
|
||||
that only warnings whose messages also match the regular expression
|
||||
are considered successful matches.
|
||||
|
||||
Args:
|
||||
expected_warning: Warning class expected to be triggered.
|
||||
expected_regexp: Regexp (re pattern object or string) expected
|
||||
to be found in error message.
|
||||
callable_obj: Function to be called.
|
||||
args: Extra args.
|
||||
kwargs: Extra kwargs.
|
||||
"""
|
||||
context = _AssertWarnsContext(expected_warning, self, callable_obj,
|
||||
expected_regexp)
|
||||
if callable_obj is None:
|
||||
return context
|
||||
with context:
|
||||
callable_obj(*args, **kwargs)
|
||||
|
||||
def assertRegexpMatches(self, text, expected_regexp, msg=None):
|
||||
"""Fail the test unless the text matches the regular expression."""
|
||||
if isinstance(expected_regexp, (str, bytes)):
|
||||
|
|
|
@ -2,6 +2,8 @@ import difflib
|
|||
import pprint
|
||||
import re
|
||||
import sys
|
||||
import warnings
|
||||
import inspect
|
||||
|
||||
from copy import deepcopy
|
||||
from test import support
|
||||
|
@ -917,6 +919,138 @@ test case
|
|||
self.assertIsInstance(e, ExceptionMock)
|
||||
self.assertEqual(e.args[0], v)
|
||||
|
||||
def testAssertWarnsCallable(self):
|
||||
def _runtime_warn():
|
||||
warnings.warn("foo", RuntimeWarning)
|
||||
# Success when the right warning is triggered, even several times
|
||||
self.assertWarns(RuntimeWarning, _runtime_warn)
|
||||
self.assertWarns(RuntimeWarning, _runtime_warn)
|
||||
# A tuple of warning classes is accepted
|
||||
self.assertWarns((DeprecationWarning, RuntimeWarning), _runtime_warn)
|
||||
# *args and **kwargs also work
|
||||
self.assertWarns(RuntimeWarning,
|
||||
warnings.warn, "foo", category=RuntimeWarning)
|
||||
# Failure when no warning is triggered
|
||||
with self.assertRaises(self.failureException):
|
||||
self.assertWarns(RuntimeWarning, lambda: 0)
|
||||
# Failure when another warning is triggered
|
||||
with warnings.catch_warnings():
|
||||
# Force default filter (in case tests are run with -We)
|
||||
warnings.simplefilter("default", RuntimeWarning)
|
||||
with self.assertRaises(self.failureException):
|
||||
self.assertWarns(DeprecationWarning, _runtime_warn)
|
||||
# Filters for other warnings are not modified
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error", RuntimeWarning)
|
||||
with self.assertRaises(RuntimeWarning):
|
||||
self.assertWarns(DeprecationWarning, _runtime_warn)
|
||||
|
||||
def testAssertWarnsContext(self):
|
||||
# Believe it or not, it is preferrable to duplicate all tests above,
|
||||
# to make sure the __warningregistry__ $@ is circumvented correctly.
|
||||
def _runtime_warn():
|
||||
warnings.warn("foo", RuntimeWarning)
|
||||
_runtime_warn_lineno = inspect.getsourcelines(_runtime_warn)[1]
|
||||
with self.assertWarns(RuntimeWarning) as cm:
|
||||
_runtime_warn()
|
||||
# A tuple of warning classes is accepted
|
||||
with self.assertWarns((DeprecationWarning, RuntimeWarning)) as cm:
|
||||
_runtime_warn()
|
||||
# The context manager exposes various useful attributes
|
||||
self.assertIsInstance(cm.warning, RuntimeWarning)
|
||||
self.assertEqual(cm.warning.args[0], "foo")
|
||||
self.assertIn("test_case.py", cm.filename)
|
||||
self.assertEqual(cm.lineno, _runtime_warn_lineno + 1)
|
||||
# Same with several warnings
|
||||
with self.assertWarns(RuntimeWarning):
|
||||
_runtime_warn()
|
||||
_runtime_warn()
|
||||
with self.assertWarns(RuntimeWarning):
|
||||
warnings.warn("foo", category=RuntimeWarning)
|
||||
# Failure when no warning is triggered
|
||||
with self.assertRaises(self.failureException):
|
||||
with self.assertWarns(RuntimeWarning):
|
||||
pass
|
||||
# Failure when another warning is triggered
|
||||
with warnings.catch_warnings():
|
||||
# Force default filter (in case tests are run with -We)
|
||||
warnings.simplefilter("default", RuntimeWarning)
|
||||
with self.assertRaises(self.failureException):
|
||||
with self.assertWarns(DeprecationWarning):
|
||||
_runtime_warn()
|
||||
# Filters for other warnings are not modified
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error", RuntimeWarning)
|
||||
with self.assertRaises(RuntimeWarning):
|
||||
with self.assertWarns(DeprecationWarning):
|
||||
_runtime_warn()
|
||||
|
||||
def testAssertWarnsRegexpCallable(self):
|
||||
def _runtime_warn(msg):
|
||||
warnings.warn(msg, RuntimeWarning)
|
||||
self.assertWarnsRegexp(RuntimeWarning, "o+",
|
||||
_runtime_warn, "foox")
|
||||
# Failure when no warning is triggered
|
||||
with self.assertRaises(self.failureException):
|
||||
self.assertWarnsRegexp(RuntimeWarning, "o+",
|
||||
lambda: 0)
|
||||
# Failure when another warning is triggered
|
||||
with warnings.catch_warnings():
|
||||
# Force default filter (in case tests are run with -We)
|
||||
warnings.simplefilter("default", RuntimeWarning)
|
||||
with self.assertRaises(self.failureException):
|
||||
self.assertWarnsRegexp(DeprecationWarning, "o+",
|
||||
_runtime_warn, "foox")
|
||||
# Failure when message doesn't match
|
||||
with self.assertRaises(self.failureException):
|
||||
self.assertWarnsRegexp(RuntimeWarning, "o+",
|
||||
_runtime_warn, "barz")
|
||||
# A little trickier: we ask RuntimeWarnings to be raised, and then
|
||||
# check for some of them. It is implementation-defined whether
|
||||
# non-matching RuntimeWarnings are simply re-raised, or produce a
|
||||
# failureException.
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error", RuntimeWarning)
|
||||
with self.assertRaises((RuntimeWarning, self.failureException)):
|
||||
self.assertWarnsRegexp(RuntimeWarning, "o+",
|
||||
_runtime_warn, "barz")
|
||||
|
||||
def testAssertWarnsRegexpContext(self):
|
||||
# Same as above, but with assertWarnsRegexp as a context manager
|
||||
def _runtime_warn(msg):
|
||||
warnings.warn(msg, RuntimeWarning)
|
||||
_runtime_warn_lineno = inspect.getsourcelines(_runtime_warn)[1]
|
||||
with self.assertWarnsRegexp(RuntimeWarning, "o+") as cm:
|
||||
_runtime_warn("foox")
|
||||
self.assertIsInstance(cm.warning, RuntimeWarning)
|
||||
self.assertEqual(cm.warning.args[0], "foox")
|
||||
self.assertIn("test_case.py", cm.filename)
|
||||
self.assertEqual(cm.lineno, _runtime_warn_lineno + 1)
|
||||
# Failure when no warning is triggered
|
||||
with self.assertRaises(self.failureException):
|
||||
with self.assertWarnsRegexp(RuntimeWarning, "o+"):
|
||||
pass
|
||||
# Failure when another warning is triggered
|
||||
with warnings.catch_warnings():
|
||||
# Force default filter (in case tests are run with -We)
|
||||
warnings.simplefilter("default", RuntimeWarning)
|
||||
with self.assertRaises(self.failureException):
|
||||
with self.assertWarnsRegexp(DeprecationWarning, "o+"):
|
||||
_runtime_warn("foox")
|
||||
# Failure when message doesn't match
|
||||
with self.assertRaises(self.failureException):
|
||||
with self.assertWarnsRegexp(RuntimeWarning, "o+"):
|
||||
_runtime_warn("barz")
|
||||
# A little trickier: we ask RuntimeWarnings to be raised, and then
|
||||
# check for some of them. It is implementation-defined whether
|
||||
# non-matching RuntimeWarnings are simply re-raised, or produce a
|
||||
# failureException.
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error", RuntimeWarning)
|
||||
with self.assertRaises((RuntimeWarning, self.failureException)):
|
||||
with self.assertWarnsRegexp(RuntimeWarning, "o+"):
|
||||
_runtime_warn("barz")
|
||||
|
||||
def testSynonymAssertMethodNames(self):
|
||||
"""Test undocumented method name synonyms.
|
||||
|
||||
|
|
|
@ -13,6 +13,10 @@ Core and Builtins
|
|||
Library
|
||||
-------
|
||||
|
||||
- Issue #9754: Similarly to assertRaises and assertRaisesRegexp, unittest
|
||||
test cases now also have assertWarns and assertWarnsRegexp methods to
|
||||
check that a given warning type was triggered by the code under test.
|
||||
|
||||
- Issue #5506: BytesIO objects now have a getbuffer() method exporting a
|
||||
view of their contents without duplicating them. The view is both readable
|
||||
and writable.
|
||||
|
|
Loading…
Reference in New Issue