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

serhiy-storchaka <[email protected]>
Newsgroups gmane.comp.python.cvs
Message-ID <[email protected]>
https://github.com/python/cpython/commit/fb9a98ab0089c0340168f62a70cfbe2945fba494
commit: fb9a98ab0089c0340168f62a70cfbe2945fba494
branch: 3.13
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-07-19T15:17:14Z
summary:

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

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 708c139505ddc89..732d03bd0761cd0 100644
--- a/Lib/test/test_curses.py
+++ b/Lib/test/test_curses.py
@@ -332,13 +332,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 000000000000000..79857185d7d0c76
--- /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 e365fc6ee9d5c55..b4c893af0117709 100644
--- a/Modules/_cursesmodule.c
+++ b/Modules/_cursesmodule.c
@@ -1704,6 +1704,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(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.