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:
Serhiy Storchaka 2026-05-03 13:24:57 +03:00 committed by GitHub
parent 8a7eddaa84
commit a3435d5ccc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 199 additions and 35 deletions

View file

@ -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

View file

@ -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`.)

View file

@ -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)

View file

@ -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__":

View file

@ -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):

View file

@ -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):

View file

@ -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.