From eb462f3597979bae3fc431a683bd5e5ea2e55a02 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 1 May 2026 11:39:40 +0300 Subject: [PATCH 1/6] gh-143231: Do not swallow not matched warnings in assertWarns*() 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. --- Doc/library/unittest.rst | 23 ++++++ Doc/whatsnew/3.15.rst | 16 +++- Lib/test/test_unittest/test_assertions.py | 3 +- Lib/test/test_unittest/test_case.py | 82 +++++++++++++++---- Lib/unittest/case.py | 14 +++- ...-05-01-11-39-37.gh-issue-143231.0cOHET.rst | 4 + 6 files changed, 121 insertions(+), 21 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-05-01-11-39-37.gh-issue-143231.0cOHET.rst 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 a687ee5115be05..d5326d50b992b8 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -1450,10 +1450,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 ------------ @@ -2314,3 +2320,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_unittest/test_assertions.py b/Lib/test/test_unittest/test_assertions.py index 1dec947ea76d23..2cc1abce83c828 100644 --- a/Lib/test/test_unittest/test_assertions.py +++ b/Lib/test/test_unittest/test_assertions.py @@ -406,7 +406,8 @@ def testAssertWarnsRegex(self): # test warning raised but with wrong message def raise_wrong_message(): warnings.warn('foo') - self.assertMessagesCM('assertWarnsRegex', (UserWarning, 'regex'), + self.assertWarnsRegex(UserWarning, 'foo', self.assertMessagesCM, + 'assertWarnsRegex', (UserWarning, 'regex'), raise_wrong_message, ['^"regex" does not match "foo"$', '^oops$', '^"regex" does not match "foo"$', diff --git a/Lib/test/test_unittest/test_case.py b/Lib/test/test_unittest/test_case.py index cf10e956bf2bdc..f653b400d748e7 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,7 +1648,7 @@ 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): @@ -1658,12 +1658,22 @@ def _runtime_warn(): 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 +1704,33 @@ 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() + 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() def testAssertWarnsNoExceptionType(self): with self.assertRaises(TypeError): @@ -1722,8 +1747,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 +1760,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): + 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", "foox") + with self.assertWarnsRegex(RuntimeWarning, "ar"): self.assertWarnsRegex(RuntimeWarning, "o+", - _runtime_warn, "barz") + _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 +1814,27 @@ 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): + 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 diff --git a/Lib/unittest/case.py b/Lib/unittest/case.py index eba50839cd33ae..e47963e85b57f2 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,30 @@ def __exit__(self, exc_type, exc_value, tb): except AttributeError: exc_name = str(self.expected) first_matching = None + matched = False + not_matching_warnings = [] for m in self.warnings: w = m.message if not isinstance(w, self.expected): + not_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))): + not_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 not_matching_warnings: + warnings.warn_explicit(m.message, m.category, m.filename, m.lineno, + source=m.source) + if matched: return # Now we simply try to choose a helpful failure message if first_matching is not None: @@ -338,7 +349,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. From 3b04653a2a9bd608a4f49c180c6a5dbb71a41d7c Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 1 May 2026 21:26:53 +0300 Subject: [PATCH 2/6] Add missed asserts. --- Lib/test/test_unittest/test_case.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/test/test_unittest/test_case.py b/Lib/test/test_unittest/test_case.py index f653b400d748e7..47ebd1a236a4ad 100644 --- a/Lib/test/test_unittest/test_case.py +++ b/Lib/test/test_unittest/test_case.py @@ -1653,6 +1653,8 @@ def _runtime_warn(categories=(RuntimeWarning,)): warnings.simplefilter("default", RuntimeWarning) with self.assertRaises(self.failureException): self.assertWarns(DeprecationWarning, _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) From 22b27f87d6de984b4fe158df173b2b2a668d4d55 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 1 May 2026 21:34:00 +0300 Subject: [PATCH 3/6] Fix my English. --- Lib/unittest/case.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/unittest/case.py b/Lib/unittest/case.py index e47963e85b57f2..9b2d2344fe2256 100644 --- a/Lib/unittest/case.py +++ b/Lib/unittest/case.py @@ -315,17 +315,17 @@ def __exit__(self, exc_type, exc_value, tb): exc_name = str(self.expected) first_matching = None matched = False - not_matching_warnings = [] + non_matching_warnings = [] for m in self.warnings: w = m.message if not isinstance(w, self.expected): - not_matching_warnings.append(m) + 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))): - not_matching_warnings.append(m) + non_matching_warnings.append(m) continue if matched: continue @@ -334,7 +334,7 @@ def __exit__(self, exc_type, exc_value, tb): self.warning = w self.filename = m.filename self.lineno = m.lineno - for m in not_matching_warnings: + for m in non_matching_warnings: warnings.warn_explicit(m.message, m.category, m.filename, m.lineno, source=m.source) if matched: From c8908f8d819af0b697275cd76c578c58bbac14ac Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 1 May 2026 22:45:51 +0300 Subject: [PATCH 4/6] Use context manager in test_assertions. --- Lib/test/test_unittest/test_assertions.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_unittest/test_assertions.py b/Lib/test/test_unittest/test_assertions.py index 2cc1abce83c828..df95e558949aee 100644 --- a/Lib/test/test_unittest/test_assertions.py +++ b/Lib/test/test_unittest/test_assertions.py @@ -406,12 +406,12 @@ def testAssertWarnsRegex(self): # test warning raised but with wrong message def raise_wrong_message(): warnings.warn('foo') - 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$']) + 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__": From c5422c71f3d8c425dc2f295498be02081ca03699 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 2 May 2026 18:25:09 +0300 Subject: [PATCH 5/6] Fix warning filtering with actions 'default' and 'module'. Fix filtering by module name. Add the module attribute to warnings.WarningMessage. --- Lib/_py_warnings.py | 12 ++--- Lib/test/test_unittest/test_case.py | 44 ++++++++++++++++++- Lib/test/test_warnings/__init__.py | 2 +- Lib/unittest/case.py | 14 ++++++ ...-05-02-18-23-50.gh-issue-143231.oBbQb5.rst | 1 + Python/_warnings.c | 7 +-- 6 files changed, 69 insertions(+), 11 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-05-02-18-23-50.gh-issue-143231.oBbQb5.rst diff --git a/Lib/_py_warnings.py b/Lib/_py_warnings.py index 81a386c4487d95..e505736d956bd9 100644 --- a/Lib/_py_warnings.py +++ b/Lib/_py_warnings.py @@ -620,17 +620,18 @@ def warn_explicit(message, category, filename, lineno, linecache.getlines(filename, module_globals) # Print message and context - msg = _wm.WarningMessage(message, category, filename, lineno, source=source) + msg = _wm.WarningMessage(message, category, filename, lineno, + module=module, source=source) _wm._showwarnmsg(msg) class WarningMessage(object): _WARNING_DETAILS = ("message", "category", "filename", "lineno", "file", - "line", "source") + "line", "source", "module") def __init__(self, message, category, filename, lineno, file=None, - line=None, source=None): + line=None, source=None, module=None): self.message = message self.category = category self.filename = filename @@ -638,12 +639,13 @@ def __init__(self, message, category, filename, lineno, file=None, self.file = file self.line = line self.source = source + self.module = module self._category_name = category.__name__ if category else None def __str__(self): - return ("{message : %r, category : %r, filename : %r, lineno : %s, " + return ("{message : %r, category : %r, module : %r, filename : %r, lineno : %s, " "line : %r}" % (self.message, self._category_name, - self.filename, self.lineno, self.line)) + self.module, self.filename, self.lineno, self.line)) def __repr__(self): return f'<{type(self).__qualname__} {self}>' diff --git a/Lib/test/test_unittest/test_case.py b/Lib/test/test_unittest/test_case.py index 47ebd1a236a4ad..83b42918e1eff6 100644 --- a/Lib/test/test_unittest/test_case.py +++ b/Lib/test/test_unittest/test_case.py @@ -1652,7 +1652,8 @@ def _runtime_warn(categories=(RuntimeWarning,)): # 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 @@ -1712,6 +1713,18 @@ def _runtime_warn(category=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 @@ -1733,6 +1746,19 @@ def _runtime_warn(category=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): @@ -1767,7 +1793,7 @@ def _runtime_warn(*msgs): warnings.simplefilter("default", RuntimeWarning) with self.assertRaises(self.failureException): self.assertWarnsRegex(DeprecationWarning, "o+", - _runtime_warn, "foox") + _runtime_warn, "foox", "foox") self.assertEqual(len(log), 1, log) self.assertIsInstance(log[0].message, RuntimeWarning) # Failure when message doesn't match. @@ -1822,6 +1848,7 @@ def _runtime_warn(msg): with self.assertRaises(self.failureException): with self.assertWarnsRegex(DeprecationWarning, "o+"): _runtime_warn("foox") + _runtime_warn("foox") self.assertEqual(len(log), 1, log) self.assertIsInstance(log[0].message, RuntimeWarning) # Failure when message doesn't match. @@ -1847,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/test/test_warnings/__init__.py b/Lib/test/test_warnings/__init__.py index d86844c1a29a9a..4b5a9de3800496 100644 --- a/Lib/test/test_warnings/__init__.py +++ b/Lib/test/test_warnings/__init__.py @@ -342,7 +342,7 @@ def test_module_globals(self): self.assertEqual(len(w), 1) # Invalid module_globals type - with self.assertRaises(TypeError): + with self.assertRaises((TypeError, AttributeError)): self.module.warn_explicit('msg', UserWarning, "filename", 42, module_globals=True) self.assertEqual(len(w), 1) diff --git a/Lib/unittest/case.py b/Lib/unittest/case.py index 9b2d2344fe2256..a392238c85abfa 100644 --- a/Lib/unittest/case.py +++ b/Lib/unittest/case.py @@ -335,7 +335,21 @@ def __exit__(self, exc_type, exc_value, tb): 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 == "" 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 diff --git a/Misc/NEWS.d/next/Library/2026-05-02-18-23-50.gh-issue-143231.oBbQb5.rst b/Misc/NEWS.d/next/Library/2026-05-02-18-23-50.gh-issue-143231.oBbQb5.rst new file mode 100644 index 00000000000000..e4769866c2045d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-02-18-23-50.gh-issue-143231.oBbQb5.rst @@ -0,0 +1 @@ +A *module* attribute has been added to :class:`!warnings.WarningMessage`. diff --git a/Python/_warnings.c b/Python/_warnings.c index 6b8fa19ff3f606..4f6de50efa14a8 100644 --- a/Python/_warnings.c +++ b/Python/_warnings.c @@ -716,7 +716,7 @@ static int call_show_warning(PyThreadState *tstate, PyObject *category, PyObject *text, PyObject *message, PyObject *filename, int lineno, PyObject *lineno_obj, - PyObject *sourceline, PyObject *source) + PyObject *sourceline, PyObject *source, PyObject *module) { PyObject *show_fn, *msg, *res, *warnmsg_cls = NULL; PyInterpreterState *interp = tstate->interp; @@ -747,7 +747,8 @@ call_show_warning(PyThreadState *tstate, PyObject *category, } msg = PyObject_CallFunctionObjArgs(warnmsg_cls, message, category, - filename, lineno_obj, Py_None, Py_None, source, + filename, lineno_obj, Py_None, Py_None, + source ? source : Py_None, module, NULL); Py_DECREF(warnmsg_cls); if (msg == NULL) @@ -878,7 +879,7 @@ warn_explicit(PyThreadState *tstate, PyObject *category, PyObject *message, goto return_none; if (rc == 0) { if (call_show_warning(tstate, category, text, message, filename, - lineno, lineno_obj, sourceline, source) < 0) + lineno, lineno_obj, sourceline, source, module) < 0) goto cleanup; } else /* if (rc == -1) */ From 38f43ecc3ebeb34da97ce1e3e596f98339be45ef Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 2 May 2026 18:33:30 +0300 Subject: [PATCH 6/6] Fix test_complex. --- Lib/test/test_complex.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) 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)