[3.14] gh-153864: Fix curses window.insch() for non-ASCII characters on a wide build (GH-153865) (GH-154152)

serhiy-storchaka <[email protected]>
Newsgroups gmane.comp.python.cvs
Message-ID <[email protected]>
https://github.com/python/cpython/commit/8b5f1c2479928a04813918b06582f22156a8861f
commit: 8b5f1c2479928a04813918b06582f22156a8861f
branch: 3.14
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-07-19T22:28:00+03:00
summary:

[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 <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-07-17-20-56-32.gh-issue-153864.WmA9By.rst
M Lib/test/test_curses.py
M Modules/_cursesmodule.c

diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py
index f4b30ac9957a2a..a75789cdda7b46 100644
--- a/Lib/test/test_curses.py
+++ b/Lib/test/test_curses.py
@@ -326,13 +326,10 @@ def test_output_character(self):
                 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 00000000000000..79857185d7d0c7
--- /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 41c20a7f96cd9f..12641b51c1e90d 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);
     }

_______________________________________________
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]
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.