From 951119852976757df08ebc0c0a58f8b91e723008 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 11 Aug 2026 11:15:48 +0300 Subject: [PATCH 1/3] Fix description of -W message field in man page (GH-111371) --- Misc/python.man | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/python.man b/Misc/python.man index 5e4f3c3debe935..238d3209971017 100644 --- a/Misc/python.man +++ b/Misc/python.man @@ -299,7 +299,7 @@ the remaining fields. The .I message -field must match the whole printed warning message; this match is +field must match the start of the warning message; this match is case-insensitive. The From 198a83581cfdcf9f6c4050e3d1567ab49af754eb Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 11 Aug 2026 11:20:56 +0300 Subject: [PATCH 2/3] gh-155411: Fix test.support.subTests() for asynchronous tests (GH-155412) An asynchronous test was wrapped in a synchronous function, which discarded the coroutine without awaiting it, so the test did not run at all and was reported as successful. --- Lib/test/support/__init__.py | 31 +++++++--- Lib/test/test_support.py | 60 +++++++++++++++++++ ...-08-09-14-00-00.gh-issue-155411.Qw8Lm2.rst | 3 + 3 files changed, 85 insertions(+), 9 deletions(-) create mode 100644 Misc/NEWS.d/next/Tests/2026-08-09-14-00-00.gh-issue-155411.Qw8Lm2.rst diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index c1460b806806f8..f98da49171dc3b 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -1098,16 +1098,29 @@ def subTests(arg_names, arg_values, /, *, _do_cleanups=False): def decorator(func): if isinstance(func, type): raise TypeError('subTests() can only decorate methods, not classes') - @functools.wraps(func) - def wrapper(self, /, *args, **kwargs): + + def iter_subtest_kwargs(): for values in arg_values: - if single_param: - values = (values,) - subtest_kwargs = dict(zip(arg_names, values)) - with self.subTest(**subtest_kwargs): - func(self, *args, **kwargs, **subtest_kwargs) - if _do_cleanups: - self.doCleanups() + yield dict(zip(arg_names, (values,) if single_param else values)) + + # A synchronous wrapper would discard the coroutine without awaiting + # it, so an asynchronous test would not run at all. + if inspect.iscoroutinefunction(func): + @functools.wraps(func) + async def wrapper(self, /, *args, **kwargs): + for subtest_kwargs in iter_subtest_kwargs(): + with self.subTest(**subtest_kwargs): + await func(self, *args, **kwargs, **subtest_kwargs) + if _do_cleanups: + self.doCleanups() + else: + @functools.wraps(func) + def wrapper(self, /, *args, **kwargs): + for subtest_kwargs in iter_subtest_kwargs(): + with self.subTest(**subtest_kwargs): + func(self, *args, **kwargs, **subtest_kwargs) + if _do_cleanups: + self.doCleanups() return wrapper return decorator diff --git a/Lib/test/test_support.py b/Lib/test/test_support.py index 7c59bb38aaee9a..84b91bb00cdbe4 100644 --- a/Lib/test/test_support.py +++ b/Lib/test/test_support.py @@ -1247,5 +1247,65 @@ def test_skipped_without_subprocess_support(self): self.assertEqual(calls, []) +class TestSubTests(unittest.TestCase): + + def run_test(self, cls): + result = unittest.TestResult() + cls('test_it').run(result) + return result + + def test_sync(self): + ran = [] + + class Sample(unittest.TestCase): + @support.subTests('a', [1, 2, 3]) + def test_it(self, a): + ran.append(a) + self.assertNotEqual(a, 2) + + result = self.run_test(Sample) + self.assertEqual(ran, [1, 2, 3]) + self.assertEqual(result.testsRun, 1) + self.assertEqual(len(result.failures), 1) + self.assertEndsWith(result.failures[0][0].id(), 'test_it (a=2)') + + # Running an asyncio event loop needs a working socket. + @support.requires_working_socket() + def test_async(self): + # An asynchronous test must be awaited: a synchronous wrapper would + # make it silently not run at all. + ran = [] + + class Sample(unittest.IsolatedAsyncioTestCase): + @support.subTests('a', [1, 2, 3]) + async def test_it(self, a): + ran.append(a) + self.assertNotEqual(a, 2) + + result = self.run_test(Sample) + self.assertEqual(ran, [1, 2, 3]) + self.assertEqual(result.testsRun, 1) + self.assertEqual(len(result.failures), 1) + self.assertEndsWith(result.failures[0][0].id(), 'test_it (a=2)') + + def test_multiple_parameters(self): + ran = [] + + class Sample(unittest.TestCase): + @support.subTests('a,b', [(1, 'x'), (2, 'y')]) + def test_it(self, a, b): + ran.append((a, b)) + + result = self.run_test(Sample) + self.assertTrue(result.wasSuccessful(), result.errors) + self.assertEqual(ran, [(1, 'x'), (2, 'y')]) + + def test_cannot_decorate_class(self): + with self.assertRaises(TypeError): + @support.subTests('a', [1]) + class Sample(unittest.TestCase): + pass + + if __name__ == '__main__': unittest.main() diff --git a/Misc/NEWS.d/next/Tests/2026-08-09-14-00-00.gh-issue-155411.Qw8Lm2.rst b/Misc/NEWS.d/next/Tests/2026-08-09-14-00-00.gh-issue-155411.Qw8Lm2.rst new file mode 100644 index 00000000000000..50f68405252605 --- /dev/null +++ b/Misc/NEWS.d/next/Tests/2026-08-09-14-00-00.gh-issue-155411.Qw8Lm2.rst @@ -0,0 +1,3 @@ +Fix :func:`!test.support.subTests` for asynchronous test methods. They were +wrapped in a synchronous function, which discarded the coroutine without +awaiting it, so the test silently did not run at all. From a7541c3843640d0112857ad0c0006916d958775a Mon Sep 17 00:00:00 2001 From: Vyron Vasileiadis Date: Tue, 11 Aug 2026 11:50:14 +0300 Subject: [PATCH 3/3] gh-154840: Keep the color pair out of curses complexchar.attr (GH-154841) On a wide build getcchar() also reports the color pair in the A_COLOR bits of the attributes, where it saturates at 255, so complexchar.attr carried it. Mask those bits out in curses_getcchar(), as the narrow build already does when packing a cell. --- Lib/test/test_curses.py | 7 ++++++- Modules/_cursesmodule.c | 5 ++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index f7584a39b182d9..d87374a298fc33 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -537,6 +537,11 @@ def test_complexchar(self): self.assertEqual(str(cc), 'z') self.assertEqual(cc.attr, 0) self.assertEqual(cc.pair, 0) + # attr never carries the color pair. + self.assertEqual(curses.complexchar('A', 0, 1).attr, 0) + self.assertEqual(curses.complexchar('A', curses.A_BOLD, 1).attr, + curses.A_BOLD) + self.assertEqual(curses.complexchar('A', 0, 1).pair, 1) # Immutable rendition. self.assertRaises(AttributeError, setattr, cc, 'attr', 1) self.assertRaises(AttributeError, setattr, cc, 'pair', 1) @@ -592,7 +597,7 @@ def test_in_wch_color(self): stdscr.addch(0, 0, curses.complexchar('A', curses.A_BOLD, 1)) cc = stdscr.in_wch(0, 0) self.assertEqual(str(cc), 'A') - self.assertTrue(cc.attr & curses.A_BOLD) + self.assertEqual(cc.attr, curses.A_BOLD) self.assertEqual(cc.pair, 1) self.assertEqual(curses.complexchar('A', 0, 1).pair, 1) diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 07e924b0fc564b..383de378670ea9 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -798,6 +798,9 @@ curses_getcchar(const cchar_t *wcval, wchar_t *wstr, attr_t *attrs, int *pair) *pair = spair; } #endif + if (rtn != ERR) { + *attrs &= ~(attr_t)A_COLOR; + } return rtn; } @@ -3674,7 +3677,7 @@ _curses_window_inch_impl(PyCursesWindowObject *self, int group_right_1, byte = 0; } } - rtn = (chtype)byte | (attrs & ~(attr_t)A_COLOR) | COLOR_PAIR(pair); + rtn = (chtype)byte | attrs | COLOR_PAIR(pair); #else if (!group_right_1) { rtn = winch(self->win);