https://github.com/python/cpython/commit/5be012561c7e36acd634a7abf726b52d54a4e408 commit: 5be012561c7e36acd634a7abf726b52d54a4e408 branch: 3.15 author: Serhiy Storchaka <[email protected]> committer: serhiy-storchaka <[email protected]> date: 2026-07-26T12:30:49+03:00 summary:
[3.15] gh-153862: Fix spurious color pair in curses window.inch() on a wide build (GH-154703) (GH-154720) winch() returns the whole code point, so inch() replacing only its low 8 bits left the high bits in the color field. Rebuild from getcchar()'s attributes and color pair. (cherry picked from commit b6188749d69e9609a83ee0d526013920da314ab2) Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * gh-153862: Suggest instr() for non-encodable characters read by inch() A character that does not fit in a single byte has a character byte of 0 in the value returned by inch(); document reading such characters with instr(). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]> files: M Doc/library/curses.rst M Lib/test/test_curses.py M Modules/_cursesmodule.c diff --git a/Doc/library/curses.rst b/Doc/library/curses.rst index f46c89ec25a0c0..993cfe62f2ac00 100644 --- a/Doc/library/curses.rst +++ b/Doc/library/curses.rst @@ -1081,6 +1081,9 @@ Window objects and the color pair with :func:`pair_number`. The character byte is the locale-encoded byte of the cell's character, consistent with :meth:`instr`. + On a wide-character build, a character that does not fit in a single byte + in the current locale has a character byte of ``0``; + use :meth:`instr` to read such characters. .. method:: window.insch(ch[, attr]) diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 8de61279a070f8..8b05d2df10b343 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -520,7 +520,7 @@ def test_read_from_window(self): with self.subTest(ch=ch): stdscr.addstr(2, 0, ch) self.assertEqual(stdscr.instr(2, 0, 1), b) - self.assertEqual(stdscr.inch(2, 0) & curses.A_CHARTEXT, b[0]) + self.assertEqual(stdscr.inch(2, 0), b[0]) def test_coordinate_errors(self): # Addressing a cell outside the window raises curses.error. diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 0440d412495e6f..014d8ce598e961 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -2102,34 +2102,6 @@ _curses_window_insch_impl(PyCursesWindowObject *self, int group_left_1, return curses_window_check_err(self, rtn, funcname, "insch"); } -#ifdef HAVE_NCURSESW -/* winch() returns the low 8 bits of the character's code point with no locale - conversion, unlike instr(), so recover the locale byte from the wide cell - when the character maps to exactly one byte, keeping the attribute and color - bits in RTN. A character with no single-byte form is left to winch(). */ -static chtype -curses_cell_locale_byte(chtype rtn, const cchar_t *cell) -{ - wchar_t wstr[CCHARW_MAX + 1]; - attr_t attrs; - short pair; - /* getcchar() is not guaranteed to write the text of an empty cell. */ - wstr[0] = L'\0'; - if (getcchar(cell, wstr, &attrs, &pair, NULL) == ERR - || wstr[0] == L'\0' || wstr[1] != L'\0') - { - return rtn; - } - /* wctob() mirrors ncurses' own _nc_to_char(): the single-byte form, or EOF - when the character has none in this locale. */ - int byte = wctob(wstr[0]); - if (byte != EOF) { - rtn = (rtn & ~(chtype)A_CHARTEXT) | (unsigned char)byte; - } - return rtn; -} -#endif - /*[clinic input] _curses.window.inch @@ -2154,7 +2126,42 @@ _curses_window_inch_impl(PyCursesWindowObject *self, int group_right_1, { chtype rtn; const char *funcname; - +#ifdef HAVE_NCURSESW + /* ncursesw's winch() returns the character's whole code point instead of + its locale byte, overflowing the chtype's 8-bit character field into the + color and attribute bits; read the wide cell and rebuild it instead. */ + cchar_t cell = {0}; + int rc; + if (!group_right_1) { + rc = win_wch(self->win, &cell); + funcname = "win_wch"; + } + else { + rc = mvwin_wch(self->win, y, x, &cell); + funcname = "mvwin_wch"; + } + if (rc == ERR) { + curses_window_set_error(self, funcname, "inch"); + return NULL; + } + wchar_t wstr[CCHARW_MAX + 1]; + attr_t attrs; + short pair; + /* getcchar() is not guaranteed to write the text of an empty cell. */ + wstr[0] = L'\0'; + if (getcchar(&cell, wstr, &attrs, &pair, NULL) == ERR) { + curses_window_set_error(self, "getcchar", "inch"); + return NULL; + } + int byte = 0; + if (wstr[0] != L'\0' && wstr[1] == L'\0') { + byte = wctob(wstr[0]); + if (byte == EOF) { + byte = 0; + } + } + rtn = (chtype)byte | (attrs & ~(attr_t)A_COLOR) | COLOR_PAIR(pair); +#else if (!group_right_1) { rtn = winch(self->win); funcname = "winch"; @@ -2167,13 +2174,6 @@ _curses_window_inch_impl(PyCursesWindowObject *self, int group_right_1, curses_window_set_error(self, funcname, "inch"); return NULL; } -#ifdef HAVE_NCURSESW - cchar_t cell = {0}; - if ((group_right_1 ? mvwin_wch(self->win, y, x, &cell) - : win_wch(self->win, &cell)) != ERR) - { - rtn = curses_cell_locale_byte(rtn, &cell); - } #endif return PyLong_FromUnsignedLong(rtn); } _______________________________________________ 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]
