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

serhiy-storchaka <[email protected]>
Newsgroups gmane.comp.python.cvs
Message-ID <[email protected]>
https://github.com/python/cpython/commit/46d7aa9fbd2fafb099da6a61f3865620d4dc9125
commit: 46d7aa9fbd2fafb099da6a61f3865620d4dc9125
branch: 3.15
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-07-19T12:45:26Z
summary:

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

On a wide build, winsch() does not locale-decode a byte above 127, unlike
waddch(), so insch() inserted '¤' (U+00A4) instead of '€' for byte 0xA4
under ISO-8859-15.  Decode the byte with btowc() and insert it as a wide
character, like addch().

(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 737735fbcc0032..d5cd7483d77ff0 100644
--- a/Lib/test/test_curses.py
+++ b/Lib/test/test_curses.py
@@ -329,13 +329,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 dd5af63fcc8a46..3a60749d440248 100644
--- a/Modules/_cursesmodule.c
+++ b/Modules/_cursesmodule.c
@@ -2064,6 +2064,32 @@ _curses_window_insch_impl(PyCursesWindowObject *self, int group_left_1,
         return NULL;
 
     const char *funcname;
+#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);
+            if (setcchar(&wch, wstr, cattr, PAIR_NUMBER(cattr), NULL) == ERR) {
+                curses_window_set_error(self, "setcchar", "insch");
+                return NULL;
+            }
+            if (!group_left_1) {
+                rtn = wins_wch(self->win, &wch);
+                funcname = "wins_wch";
+            }
+            else {
+                rtn = mvwins_wch(self->win, y, x, &wch);
+                funcname = "mvwins_wch";
+            }
+            return curses_window_check_err(self, rtn, funcname, "insch");
+        }
+    }
+#endif
     if (!group_left_1) {
         rtn = winsch(self->win, ch_ | (attr_t)attr);
         funcname = "winsch";

_______________________________________________
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.