mirror of
https://github.com/python/cpython.git
synced 2026-05-04 09:31:02 +00:00
gh-143231: Do not swallow not matched warnings in assertWarns*() (GH-149229)
unittest.TestCase methods assertWarns() and assertWarnsRegex() no longer swallow warnings that do not match the specified category or regex. Nested context managers are now supported.
This commit is contained in:
parent
8a7eddaa84
commit
a3435d5ccc
7 changed files with 199 additions and 35 deletions
|
|
@ -1095,6 +1095,13 @@ Test cases
|
|||
self.assertIn('myfile.py', cm.filename)
|
||||
self.assertEqual(320, cm.lineno)
|
||||
|
||||
The context managers can be nested to test that multiple different
|
||||
warnings are emitted::
|
||||
|
||||
with (self.assertWarns(SomeWarning),
|
||||
self.assertWarns(OtherWarning)):
|
||||
do_something()
|
||||
|
||||
This method works regardless of the warning filters in place when it
|
||||
is called.
|
||||
|
||||
|
|
@ -1103,6 +1110,10 @@ Test cases
|
|||
.. versionchanged:: 3.3
|
||||
Added the *msg* keyword argument when used as a context manager.
|
||||
|
||||
.. versionchanged:: next
|
||||
Warnings that do not match the specified category are no longer
|
||||
swallowed.
|
||||
Nested context managers are now supported.
|
||||
|
||||
.. method:: assertWarnsRegex(warning, regex, callable, *args, **kwds)
|
||||
assertWarnsRegex(warning, regex, *, msg=None)
|
||||
|
|
@ -1121,11 +1132,23 @@ Test cases
|
|||
with self.assertWarnsRegex(RuntimeWarning, 'unsafe frobnicating'):
|
||||
frobnicate('/etc/passwd')
|
||||
|
||||
The context managers can be nested to test that multiple different
|
||||
warnings are emitted::
|
||||
|
||||
with (self.assertWarns(SomeWarning, regex1),
|
||||
self.assertWarns(OtherWarning, regex2)):
|
||||
do_something()
|
||||
|
||||
.. versionadded:: 3.2
|
||||
|
||||
.. versionchanged:: 3.3
|
||||
Added the *msg* keyword argument when used as a context manager.
|
||||
|
||||
.. versionchanged:: next
|
||||
Warnings that do not match the specified category or regex are
|
||||
no longer swallowed.
|
||||
Nested context managers are now supported.
|
||||
|
||||
.. method:: assertLogs(logger=None, level=None, formatter=None)
|
||||
|
||||
A context manager to test that at least one message is logged on
|
||||
|
|
|
|||
|
|
@ -1471,10 +1471,16 @@ unicodedata
|
|||
unittest
|
||||
--------
|
||||
|
||||
* :func:`unittest.TestCase.assertLogs` will now accept a formatter
|
||||
* :meth:`unittest.TestCase.assertLogs` will now accept a formatter
|
||||
to control how messages are formatted.
|
||||
(Contributed by Garry Cairns in :gh:`134567`.)
|
||||
|
||||
* :meth:`unittest.TestCase.assertWarns` and
|
||||
:meth:`unittest.TestCase.assertWarnsRegex` no longer swallow warnings that
|
||||
do not match the specified category or regex.
|
||||
Nested context managers are now supported.
|
||||
(Contributed by Serhiy Storchaka in :gh:`143231`.)
|
||||
|
||||
|
||||
urllib.parse
|
||||
------------
|
||||
|
|
@ -2352,3 +2358,11 @@ that may require changes to your code.
|
|||
with argument ``altchars=b'-_'`` (this works with older Python versions)
|
||||
to make padding required.
|
||||
(Contributed by Serhiy Storchaka in :gh:`73613`.)
|
||||
|
||||
* Since :meth:`unittest.TestCase.assertWarns` and
|
||||
:meth:`unittest.TestCase.assertWarnsRegex` no longer swallow warnings that
|
||||
do not match the specified category or regex, your tests may start leaking
|
||||
some warnings that were previously masked.
|
||||
Use warning filters to silence them or additional :meth:`!assertWarns*`
|
||||
to catch and check them.
|
||||
(Contributed by Serhiy Storchaka in :gh:`143231`.)
|
||||
|
|
|
|||
|
|
@ -504,17 +504,25 @@ def check(z, x, y):
|
|||
with self.assertWarnsRegex(DeprecationWarning,
|
||||
"argument 'imag' must be a real number, not complex"):
|
||||
check(complex(0.0, 4.25j), -4.25, 0.0)
|
||||
with self.assertWarnsRegex(DeprecationWarning,
|
||||
"argument 'real' must be a real number, not complex"):
|
||||
with (self.assertWarnsRegex(DeprecationWarning,
|
||||
"argument 'real' must be a real number, not complex"),
|
||||
self.assertWarnsRegex(DeprecationWarning,
|
||||
"argument 'imag' must be a real number, not complex")):
|
||||
check(complex(4.25+0j, 0j), 4.25, 0.0)
|
||||
with self.assertWarnsRegex(DeprecationWarning,
|
||||
"argument 'real' must be a real number, not complex"):
|
||||
with (self.assertWarnsRegex(DeprecationWarning,
|
||||
"argument 'real' must be a real number, not complex"),
|
||||
self.assertWarnsRegex(DeprecationWarning,
|
||||
"argument 'imag' must be a real number, not complex")):
|
||||
check(complex(4.25j, 0j), 0.0, 4.25)
|
||||
with self.assertWarnsRegex(DeprecationWarning,
|
||||
"argument 'real' must be a real number, not complex"):
|
||||
with (self.assertWarnsRegex(DeprecationWarning,
|
||||
"argument 'real' must be a real number, not complex"),
|
||||
self.assertWarnsRegex(DeprecationWarning,
|
||||
"argument 'imag' must be a real number, not complex")):
|
||||
check(complex(0j, 4.25+0j), 0.0, 4.25)
|
||||
with self.assertWarnsRegex(DeprecationWarning,
|
||||
"argument 'real' must be a real number, not complex"):
|
||||
with (self.assertWarnsRegex(DeprecationWarning,
|
||||
"argument 'real' must be a real number, not complex"),
|
||||
self.assertWarnsRegex(DeprecationWarning,
|
||||
"argument 'imag' must be a real number, not complex")):
|
||||
check(complex(0j, 4.25j), -4.25, 0.0)
|
||||
|
||||
check(complex(real=4.25), 4.25, 0.0)
|
||||
|
|
|
|||
|
|
@ -406,11 +406,12 @@ def testAssertWarnsRegex(self):
|
|||
# test warning raised but with wrong message
|
||||
def raise_wrong_message():
|
||||
warnings.warn('foo')
|
||||
self.assertMessagesCM('assertWarnsRegex', (UserWarning, 'regex'),
|
||||
raise_wrong_message,
|
||||
['^"regex" does not match "foo"$', '^oops$',
|
||||
'^"regex" does not match "foo"$',
|
||||
'^"regex" does not match "foo" : oops$'])
|
||||
with self.assertWarnsRegex(UserWarning, 'foo'):
|
||||
self.assertMessagesCM('assertWarnsRegex', (UserWarning, 'regex'),
|
||||
raise_wrong_message,
|
||||
['^"regex" does not match "foo"$', '^oops$',
|
||||
'^"regex" does not match "foo"$',
|
||||
'^"regex" does not match "foo" : oops$'])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -1631,11 +1631,11 @@ def testAssertRaisesRegexNoExceptionType(self):
|
|||
self.assertRaisesRegex((ValueError, object), 'expect')
|
||||
|
||||
def testAssertWarnsCallable(self):
|
||||
def _runtime_warn():
|
||||
warnings.warn("foo", RuntimeWarning)
|
||||
def _runtime_warn(categories=(RuntimeWarning,)):
|
||||
for category in categories:
|
||||
warnings.warn("foo", category)
|
||||
# Success when the right warning is triggered, even several times
|
||||
self.assertWarns(RuntimeWarning, _runtime_warn)
|
||||
self.assertWarns(RuntimeWarning, _runtime_warn)
|
||||
self.assertWarns(RuntimeWarning, _runtime_warn, (RuntimeWarning, RuntimeWarning))
|
||||
# A tuple of warning classes is accepted
|
||||
self.assertWarns((DeprecationWarning, RuntimeWarning), _runtime_warn)
|
||||
# *args and **kwargs also work
|
||||
|
|
@ -1648,22 +1648,35 @@ def _runtime_warn():
|
|||
with self.assertRaises(TypeError):
|
||||
self.assertWarns(RuntimeWarning, None)
|
||||
# Failure when another warning is triggered
|
||||
with warnings.catch_warnings():
|
||||
with warnings.catch_warnings(record=True) as log:
|
||||
# Force default filter (in case tests are run with -We)
|
||||
warnings.simplefilter("default", RuntimeWarning)
|
||||
with self.assertRaises(self.failureException):
|
||||
self.assertWarns(DeprecationWarning, _runtime_warn)
|
||||
self.assertWarns(DeprecationWarning, _runtime_warn,
|
||||
(RuntimeWarning, RuntimeWarning))
|
||||
self.assertEqual(len(log), 1, log)
|
||||
self.assertIsInstance(log[0].message, RuntimeWarning)
|
||||
# Filters for other warnings are not modified
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error", RuntimeWarning)
|
||||
with self.assertRaises(RuntimeWarning):
|
||||
self.assertWarns(DeprecationWarning, _runtime_warn)
|
||||
# Warnings that do not match the category are not swallowed.
|
||||
with self.assertWarns(RuntimeWarning):
|
||||
with self.assertRaises(self.failureException):
|
||||
self.assertWarns(DeprecationWarning, _runtime_warn)
|
||||
with self.assertWarns(RuntimeWarning):
|
||||
self.assertWarns(DeprecationWarning, _runtime_warn,
|
||||
(RuntimeWarning, DeprecationWarning))
|
||||
with self.assertWarns(RuntimeWarning):
|
||||
self.assertWarns(DeprecationWarning, _runtime_warn,
|
||||
(DeprecationWarning, RuntimeWarning))
|
||||
|
||||
def testAssertWarnsContext(self):
|
||||
# Believe it or not, it is preferable to duplicate all tests above,
|
||||
# to make sure the __warningregistry__ $@ is circumvented correctly.
|
||||
def _runtime_warn():
|
||||
warnings.warn("foo", RuntimeWarning)
|
||||
def _runtime_warn(category=RuntimeWarning):
|
||||
warnings.warn("foo", category)
|
||||
_runtime_warn_lineno = inspect.getsourcelines(_runtime_warn)[1]
|
||||
with self.assertWarns(RuntimeWarning) as cm:
|
||||
_runtime_warn()
|
||||
|
|
@ -1694,18 +1707,58 @@ def _runtime_warn():
|
|||
with self.assertWarns(RuntimeWarning, foobar=42):
|
||||
pass
|
||||
# Failure when another warning is triggered
|
||||
with warnings.catch_warnings():
|
||||
with warnings.catch_warnings(record=True) as log:
|
||||
# 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()
|
||||
_runtime_warn()
|
||||
self.assertEqual(len(log), 1, log)
|
||||
self.assertIsInstance(log[0].message, RuntimeWarning)
|
||||
with warnings.catch_warnings(record=True) as log:
|
||||
# Force default filter (in case tests are run with -We)
|
||||
warnings.simplefilter("error", RuntimeWarning)
|
||||
warnings.filterwarnings("default", category=RuntimeWarning,
|
||||
module=__name__)
|
||||
with self.assertRaises(self.failureException):
|
||||
with self.assertWarns(DeprecationWarning):
|
||||
_runtime_warn()
|
||||
_runtime_warn()
|
||||
self.assertEqual(len(log), 1, log)
|
||||
self.assertIsInstance(log[0].message, RuntimeWarning)
|
||||
# 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()
|
||||
# Warnings that do not match the category are not swallowed.
|
||||
with self.assertWarns(RuntimeWarning):
|
||||
with self.assertRaises(self.failureException):
|
||||
with self.assertWarns(DeprecationWarning):
|
||||
_runtime_warn()
|
||||
with self.assertWarns(RuntimeWarning):
|
||||
with self.assertWarns(DeprecationWarning):
|
||||
_runtime_warn()
|
||||
_runtime_warn(DeprecationWarning)
|
||||
with self.assertWarns(RuntimeWarning):
|
||||
with self.assertWarns(DeprecationWarning):
|
||||
_runtime_warn(DeprecationWarning)
|
||||
_runtime_warn()
|
||||
# Filters by module name work for other warnings.
|
||||
with warnings.catch_warnings(record=True) as log:
|
||||
warnings.filterwarnings("error", category=RuntimeWarning)
|
||||
warnings.filterwarnings("default", category=RuntimeWarning,
|
||||
module=re.escape(__name__))
|
||||
warnings.filterwarnings("error", category=RuntimeWarning,
|
||||
module='test_case')
|
||||
with self.assertWarns(DeprecationWarning):
|
||||
_runtime_warn(DeprecationWarning)
|
||||
_runtime_warn()
|
||||
_runtime_warn()
|
||||
self.assertEqual(len(log), 1, log)
|
||||
self.assertIsInstance(log[0].message, RuntimeWarning)
|
||||
|
||||
def testAssertWarnsNoExceptionType(self):
|
||||
with self.assertRaises(TypeError):
|
||||
|
|
@ -1722,8 +1775,9 @@ def testAssertWarnsNoExceptionType(self):
|
|||
self.assertWarns((UserWarning, Exception))
|
||||
|
||||
def testAssertWarnsRegexCallable(self):
|
||||
def _runtime_warn(msg):
|
||||
warnings.warn(msg, RuntimeWarning)
|
||||
def _runtime_warn(*msgs):
|
||||
for msg in msgs:
|
||||
warnings.warn(msg, RuntimeWarning)
|
||||
self.assertWarnsRegex(RuntimeWarning, "o+",
|
||||
_runtime_warn, "foox")
|
||||
# Failure when no warning is triggered
|
||||
|
|
@ -1734,16 +1788,26 @@ def _runtime_warn(msg):
|
|||
with self.assertRaises(TypeError):
|
||||
self.assertWarnsRegex(RuntimeWarning, "o+", None)
|
||||
# Failure when another warning is triggered
|
||||
with warnings.catch_warnings():
|
||||
with warnings.catch_warnings(record=True) as log:
|
||||
# Force default filter (in case tests are run with -We)
|
||||
warnings.simplefilter("default", RuntimeWarning)
|
||||
with self.assertRaises(self.failureException):
|
||||
self.assertWarnsRegex(DeprecationWarning, "o+",
|
||||
_runtime_warn, "foox")
|
||||
# Failure when message doesn't match
|
||||
with self.assertRaises(self.failureException):
|
||||
_runtime_warn, "foox", "foox")
|
||||
self.assertEqual(len(log), 1, log)
|
||||
self.assertIsInstance(log[0].message, RuntimeWarning)
|
||||
# Failure when message doesn't match.
|
||||
# Warnings that do not match the regex are not swallowed.
|
||||
with self.assertWarnsRegex(RuntimeWarning, "ar"):
|
||||
with self.assertRaises(self.failureException):
|
||||
self.assertWarnsRegex(RuntimeWarning, "o+",
|
||||
_runtime_warn, "barz")
|
||||
with self.assertWarnsRegex(RuntimeWarning, "ar"):
|
||||
self.assertWarnsRegex(RuntimeWarning, "o+",
|
||||
_runtime_warn, "barz")
|
||||
_runtime_warn, "barz", "foox")
|
||||
with self.assertWarnsRegex(RuntimeWarning, "ar"):
|
||||
self.assertWarnsRegex(RuntimeWarning, "o+",
|
||||
_runtime_warn, "foox", "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
|
||||
|
|
@ -1778,16 +1842,29 @@ def _runtime_warn(msg):
|
|||
with self.assertWarnsRegex(RuntimeWarning, 'o+', foobar=42):
|
||||
pass
|
||||
# Failure when another warning is triggered
|
||||
with warnings.catch_warnings():
|
||||
with warnings.catch_warnings(record=True) as log:
|
||||
# Force default filter (in case tests are run with -We)
|
||||
warnings.simplefilter("default", RuntimeWarning)
|
||||
with self.assertRaises(self.failureException):
|
||||
with self.assertWarnsRegex(DeprecationWarning, "o+"):
|
||||
_runtime_warn("foox")
|
||||
# Failure when message doesn't match
|
||||
with self.assertRaises(self.failureException):
|
||||
_runtime_warn("foox")
|
||||
self.assertEqual(len(log), 1, log)
|
||||
self.assertIsInstance(log[0].message, RuntimeWarning)
|
||||
# Failure when message doesn't match.
|
||||
# Warnings that do not match the regex are not swallowed.
|
||||
with self.assertWarnsRegex(RuntimeWarning, "ar"):
|
||||
with self.assertRaises(self.failureException):
|
||||
with self.assertWarnsRegex(RuntimeWarning, "o+"):
|
||||
_runtime_warn("barz")
|
||||
with self.assertWarnsRegex(RuntimeWarning, "ar"):
|
||||
with self.assertWarnsRegex(RuntimeWarning, "o+"):
|
||||
_runtime_warn("barz")
|
||||
_runtime_warn("foox")
|
||||
with self.assertWarnsRegex(RuntimeWarning, "ar"):
|
||||
with self.assertWarnsRegex(RuntimeWarning, "o+"):
|
||||
_runtime_warn("foox")
|
||||
_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
|
||||
|
|
@ -1797,6 +1874,19 @@ def _runtime_warn(msg):
|
|||
with self.assertRaises((RuntimeWarning, self.failureException)):
|
||||
with self.assertWarnsRegex(RuntimeWarning, "o+"):
|
||||
_runtime_warn("barz")
|
||||
# Filters by module name work for warnings with other message.
|
||||
with warnings.catch_warnings(record=True) as log:
|
||||
warnings.filterwarnings("error", category=RuntimeWarning)
|
||||
warnings.filterwarnings("default", category=RuntimeWarning,
|
||||
module=re.escape(__name__))
|
||||
warnings.filterwarnings("error", category=RuntimeWarning,
|
||||
module='test_case')
|
||||
with self.assertWarnsRegex(RuntimeWarning, "ar"):
|
||||
_runtime_warn("bar")
|
||||
_runtime_warn("foox")
|
||||
_runtime_warn("foox")
|
||||
self.assertEqual(len(log), 1, log)
|
||||
self.assertIsInstance(log[0].message, RuntimeWarning)
|
||||
|
||||
def testAssertWarnsRegexNoExceptionType(self):
|
||||
with self.assertRaises(TypeError):
|
||||
|
|
|
|||
|
|
@ -301,7 +301,7 @@ def __enter__(self):
|
|||
v.__warningregistry__ = {}
|
||||
self.warnings_manager = warnings.catch_warnings(record=True)
|
||||
self.warnings = self.warnings_manager.__enter__()
|
||||
warnings.simplefilter("always", self.expected)
|
||||
warnings.simplefilter("always")
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, tb):
|
||||
|
|
@ -314,19 +314,44 @@ def __exit__(self, exc_type, exc_value, tb):
|
|||
except AttributeError:
|
||||
exc_name = str(self.expected)
|
||||
first_matching = None
|
||||
matched = False
|
||||
non_matching_warnings = []
|
||||
for m in self.warnings:
|
||||
w = m.message
|
||||
if not isinstance(w, self.expected):
|
||||
non_matching_warnings.append(m)
|
||||
continue
|
||||
if first_matching is None:
|
||||
first_matching = w
|
||||
if (self.expected_regex is not None and
|
||||
not self.expected_regex.search(str(w))):
|
||||
non_matching_warnings.append(m)
|
||||
continue
|
||||
if matched:
|
||||
continue
|
||||
matched = True
|
||||
# store warning for later retrieval
|
||||
self.warning = w
|
||||
self.filename = m.filename
|
||||
self.lineno = m.lineno
|
||||
for m in non_matching_warnings:
|
||||
module = m.module
|
||||
module_globals = None
|
||||
registry = None
|
||||
if module is not None:
|
||||
try:
|
||||
module_globals = vars(sys.modules[module])
|
||||
except (KeyError, TypeError):
|
||||
# module == "<string>" or sys.modules[module] is None
|
||||
pass
|
||||
else:
|
||||
registry = module_globals.setdefault("__warningregistry__", {})
|
||||
warnings.warn_explicit(m.message, m.category, m.filename, m.lineno,
|
||||
module=module,
|
||||
registry=registry,
|
||||
module_globals=module_globals,
|
||||
source=m.source)
|
||||
if matched:
|
||||
return
|
||||
# Now we simply try to choose a helpful failure message
|
||||
if first_matching is not None:
|
||||
|
|
@ -338,7 +363,6 @@ def __exit__(self, exc_type, exc_value, tb):
|
|||
else:
|
||||
self._raiseFailure("{} not triggered".format(exc_name))
|
||||
|
||||
|
||||
class _AssertNotWarnsContext(_AssertWarnsContext):
|
||||
|
||||
def __exit__(self, exc_type, exc_value, tb):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
:func:`unittest.TestCase.assertWarns` and
|
||||
:func:`unittest.TestCase.assertWarnsRegex` no longer swallow warnings that
|
||||
do not match the specified category or regex.
|
||||
Nested context managers are now supported.
|
||||
Loading…
Add table
Add a link
Reference in a new issue