From b5e11a7b218a621cf1070f4f06807a4bbe28f513 Mon Sep 17 00:00:00 2001 From: Vyron Vasileiadis Date: Wed, 29 Jul 2026 00:20:01 +0300 Subject: [PATCH 1/2] gh-154840: Keep the color pair out of curses complexchar.attr On a wide build getcchar() reports the packed color pair in the attributes, so attr carried it and saturated at 255, although the documentation says the pair is stored separately and is not limited to a color_pair() value. Mask A_COLOR out, as the narrow branch of the same function already does. repr() reads through the same function and is fixed with it. --- Lib/test/test_curses.py | 8 +++++++- Modules/_cursesmodule.c | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 9f7f8535b6d9a80..29d083250e2caac 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -584,9 +584,15 @@ 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) + # attr never carries the color pair, not even a pair that does not fit + # in a color_pair() value. + self.assertEqual(curses.complexchar('A', 0, 1).attr, 0) + self.assertEqual(curses.complexchar('A', 0, 300).attr, 0) + self.assertEqual(curses.complexchar('A', curses.A_BOLD, 1).attr, + curses.A_BOLD) def test_getbkgrnd(self): # getbkgrnd() returns the background as a complexchar (getbkgd() can diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 01ea3c43cce2e6f..678c3c154ff8678 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -889,6 +889,7 @@ curses_cell_attr_pair(cursesmodule_state *state, const curses_cell_t *cell, PyErr_SetString(state->error, "getcchar() returned ERR"); return -1; } + *attr &= ~(attr_t)A_COLOR; return 0; #else *attr = *cell & A_ATTRIBUTES & ~(attr_t)A_COLOR; From 309d61840eeea79316008c16c7a3de97ddd5ec64 Mon Sep 17 00:00:00 2001 From: Vyron Vasileiadis Date: Wed, 5 Aug 2026 23:33:16 +0300 Subject: [PATCH 2/2] Mask the color pair in curses_getcchar() and move the new tests getcchar() is the one place that reads the pair out of a cell, so the mask belongs there. inch() no longer needs its own. The complexchar checks move to test_complexchar. --- Lib/test/test_curses.py | 14 ++++++++------ Modules/_cursesmodule.c | 6 ++++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 48fa32f3cd06c7f..b6c90e3f56916dc 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -537,6 +537,14 @@ 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, not even a pair that does not fit + # in a color_pair() value. + self.assertEqual(curses.complexchar('A', 0, 1).attr, 0) + self.assertEqual(curses.complexchar('A', 0, 300).attr, 0) + self.assertEqual(curses.complexchar('A', curses.A_BOLD, 1).attr, + curses.A_BOLD) + self.assertEqual(curses.complexchar('A', 0, 1).pair, 1) + self.assertEqual(curses.complexchar('A', 0, 300).pair, 300) # Immutable rendition. self.assertRaises(AttributeError, setattr, cc, 'attr', 1) self.assertRaises(AttributeError, setattr, cc, 'pair', 1) @@ -595,12 +603,6 @@ def test_in_wch_color(self): self.assertEqual(cc.attr, curses.A_BOLD) self.assertEqual(cc.pair, 1) self.assertEqual(curses.complexchar('A', 0, 1).pair, 1) - # attr never carries the color pair, not even a pair that does not fit - # in a color_pair() value. - self.assertEqual(curses.complexchar('A', 0, 1).attr, 0) - self.assertEqual(curses.complexchar('A', 0, 300).attr, 0) - self.assertEqual(curses.complexchar('A', curses.A_BOLD, 1).attr, - curses.A_BOLD) def test_getbkgrnd(self): # getbkgrnd() returns the background as a complexchar (getbkgd() can diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index d31c1984e4e3b42..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; } @@ -884,7 +887,6 @@ curses_cell_attr_pair(cursesmodule_state *state, const curses_cell_t *cell, PyErr_SetString(state->error, "getcchar() returned ERR"); return -1; } - *attr &= ~(attr_t)A_COLOR; return 0; #else *attr = *cell & A_ATTRIBUTES & ~(attr_t)A_COLOR; @@ -3675,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);