From: Serhiy Storchaka Date: Sun, 19 Jul 2026 19:28:00 +0000 (+0300) Subject: [3.14] gh-153864: Fix curses window.insch() for non-ASCII characters on a wide build... X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=8b5f1c2479928a04813918b06582f22156a8861f;p=thirdparty%2FPython%2Fcpython.git [3.14] gh-153864: Fix curses window.insch() for non-ASCII characters on a wide build (GH-153865) (GH-154152) On a wide build, winsch() does not locale-decode a byte above 127, unlike waddch(), so insch()/mvinsch() stored the wrong character for a non-ASCII byte of an 8-bit locale ('€' 0xA4 under ISO-8859-15 became U+00A4 '¤'). Decode the byte with btowc() and insert it as a wide character, as addch() effectively does. (cherry picked from commit 223cbffca6df7e4c8c77b360a4e6ba0472255643) Co-authored-by: Claude Opus 4.8 --- diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index f4b30ac9957a..a75789cdda7b 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -326,13 +326,10 @@ class TestCurses(unittest.TestCase): stdscr.move(2, 0) stdscr.echochar(v) self.assertEqual(self._read_char(2, 0), c) - # insch() round-trips a byte only where its code point equals - # the byte value (Latin-1): on a wide build ncurses winsch - # stores a printable byte directly as a code point instead of - # decoding it through the locale. - if ord(c) < 0x100: - stdscr.insch(1, 0, v) - self.assertEqual(self._read_char(1, 0), c) + # insch() decodes the byte through the locale like addch(), so + # it round-trips the same character. + stdscr.insch(1, 0, v) + self.assertEqual(self._read_char(1, 0), c) # The same characters supplied as a str. Unlike the int path above, a # str is stored as a wide-character cell on a wide build, so every diff --git a/Misc/NEWS.d/next/Library/2026-07-17-20-56-32.gh-issue-153864.WmA9By.rst b/Misc/NEWS.d/next/Library/2026-07-17-20-56-32.gh-issue-153864.WmA9By.rst new file mode 100644 index 000000000000..79857185d7d0 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-17-20-56-32.gh-issue-153864.WmA9By.rst @@ -0,0 +1,3 @@ +On a wide :mod:`curses` build, :meth:`curses.window.insch` now inserts a +non-ASCII byte as the character it encodes in the window's encoding, +consistently with :meth:`~curses.window.addch`, instead of its code point. diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 41c20a7f96cd..12641b51c1e9 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -1871,6 +1871,27 @@ _curses_window_insch_impl(PyCursesWindowObject *self, int group_left_1, if (!PyCurses_ConvertToChtype(self, ch, &ch_)) return NULL; +#ifdef HAVE_NCURSESW + /* winsch() does not locale-decode a byte above 127 on a wide build, + unlike waddch(), so decode it here and insert it as a wide character. */ + chtype cch = ch_ & A_CHARTEXT; + if (cch > 127) { + wint_t wc = btowc((int)cch); + if (wc != WEOF) { + cchar_t wch; + wchar_t wstr[2] = { (wchar_t)wc, L'\0' }; + attr_t cattr = (attr_t)((ch_ | (attr_t)attr) & ~(chtype)A_CHARTEXT); + setcchar(&wch, wstr, cattr, PAIR_NUMBER(cattr), NULL); + if (!group_left_1) { + rtn = wins_wch(self->win, &wch); + } + else { + rtn = mvwins_wch(self->win, y, x, &wch); + } + return PyCursesCheckERR_ForWin(self, rtn, "insch"); + } + } +#endif if (!group_left_1) { rtn = winsch(self->win, ch_ | (attr_t)attr); }