diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index c1460b806806f81..f98da49171dc3b5 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_curses.py b/Lib/test/test_curses.py index f7584a39b182d99..d87374a298fc337 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/Lib/test/test_support.py b/Lib/test/test_support.py index 7c59bb38aaee9ae..84b91bb00cdbe4d 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 000000000000000..50f684052526051 --- /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. diff --git a/Misc/python.man b/Misc/python.man index 5e4f3c3debe9358..238d32099710173 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 diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 07e924b0fc564bc..383de378670ea97 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);