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]