https://github.com/python/cpython/commit/a3435d5ccc1878db52f48b0a5944cd0a78ec8fcf
commit: a3435d5ccc1878db52f48b0a5944cd0a78ec8fcf
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-05-03T10:24:57Z
summary:
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.
files:
A Misc/NEWS.d/next/Library/2026-05-01-11-39-37.gh-issue-143231.0cOHET.rst
M Doc/library/unittest.rst
M Doc/whatsnew/3.15.rst
M Lib/test/test_complex.py
M Lib/test/test_unittest/test_assertions.py
M Lib/test/test_unittest/test_case.py
M Lib/unittest/case.py
diff --git a/Doc/library/unittest.rst b/Doc/library/unittest.rst
index 6e0df0648fb8bf..d55bc9f9662360 100644
--- a/Doc/library/unittest.rst
+++ b/Doc/library/unittest.rst
@@ -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
diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index 78e464f2a5a6d8..bf7aecc4c35bd1 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -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`.)
diff --git a/Lib/test/test_complex.py b/Lib/test/test_complex.py
index bee2aceb187027..bb307191dffcc1 100644
--- a/Lib/test/test_complex.py
+++ b/Lib/test/test_complex.py
@@ -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)
diff --git a/Lib/test/test_unittest/test_assertions.py
b/Lib/test/test_unittest/test_assertions.py
index 1dec947ea76d23..df95e558949aee 100644
--- a/Lib/test/test_unittest/test_assertions.py
+++ b/Lib/test/test_unittest/test_assertions.py
@@ -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__":
diff --git a/Lib/test/test_unittest/test_case.py
b/Lib/test/test_unittest/test_case.py
index cf10e956bf2bdc..83b42918e1eff6 100644
--- a/Lib/test/test_unittest/test_case.py
+++ b/Lib/test/test_unittest/test_case.py
@@ -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,15 +1842,28 @@ 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
@@ -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):
diff --git a/Lib/unittest/case.py b/Lib/unittest/case.py
index eba50839cd33ae..a392238c85abfa 100644
--- a/Lib/unittest/case.py
+++ b/Lib/unittest/case.py
@@ -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):
diff --git
a/Misc/NEWS.d/next/Library/2026-05-01-11-39-37.gh-issue-143231.0cOHET.rst
b/Misc/NEWS.d/next/Library/2026-05-01-11-39-37.gh-issue-143231.0cOHET.rst
new file mode 100644
index 00000000000000..05c9fa79904154
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-05-01-11-39-37.gh-issue-143231.0cOHET.rst
@@ -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.
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]