https://github.com/python/cpython/commit/1933f77772b8bcbbb308a0efc9767c096015e688
commit: 1933f77772b8bcbbb308a0efc9767c096015e688
branch: 3.14
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-07-19T15:19:15Z
summary:
[3.14] gh-153862: Fix curses window.inch() for non-ASCII characters on a wide
build (GH-153863) (GH-154151)
On a wide build, winch() returns the low 8 bits of the character's code
point with no locale conversion, so inch()/mvinch() disagreed with instr()
for a non-ASCII character of an 8-bit locale ('€' under ISO-8859-15 gave
0xAC instead of 0xA4). Re-encode the cell to its locale byte via wctob(),
as ncurses does for getbkgd().
(cherry picked from commit a83aaa91490d5885949eb0780a37b15963b6d0f3)
Co-authored-by: Claude Opus 4.8 <[email protected]>
files:
A Misc/NEWS.d/next/Library/2026-07-17-20-31-00.gh-issue-153862.X3kjoY.rst
M Doc/library/curses.rst
M Lib/test/test_curses.py
M Modules/_cursesmodule.c
diff --git a/Doc/library/curses.rst b/Doc/library/curses.rst
index 77c9397faebaf1..853ff6ab7332a6 100644
--- a/Doc/library/curses.rst
+++ b/Doc/library/curses.rst
@@ -1082,6 +1082,8 @@ Window objects
The bottom 8 bits are the character proper and the upper bits are the
attributes;
extract them with the :data:`A_CHARTEXT` and :data:`A_ATTRIBUTES` bit-masks,
and the color pair with :func:`pair_number`.
+ The character byte is the locale-encoded byte of the cell's character,
+ consistent with :meth:`instr`.
.. method:: window.insch(ch[, attr])
diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py
index e15817ce2e7b0f..f4b30ac9957a2a 100644
--- a/Lib/test/test_curses.py
+++ b/Lib/test/test_curses.py
@@ -506,10 +506,9 @@ def test_read_from_window(self):
self.assertRaises(ValueError, stdscr.instr, -2)
self.assertRaises(ValueError, stdscr.instr, 0, 2, -2)
# A non-ASCII character of an 8-bit locale reads back as its encoded
- # byte (see _encodable for the set). instr() returns the locale bytes
- # for any single-byte character; inch() packs the text into a chtype,
so
- # on a wide build it only round-trips a Latin-1 codepoint (byte ==
- # codepoint).
+ # byte (see _encodable for the set). Both instr() and inch() return
the
+ # locale byte for any character that fits the locale's single-byte
+ # encoding.
encoding = stdscr.encoding
for ch in ('A', 'é', '¤', '€', 'є'):
try:
@@ -521,8 +520,7 @@ def test_read_from_window(self):
with self.subTest(ch=ch):
stdscr.addstr(2, 0, ch)
self.assertEqual(stdscr.instr(2, 0, 1), b)
- if ord(ch) < 0x100:
- self.assertEqual(stdscr.inch(2, 0) & curses.A_CHARTEXT,
b[0])
+ self.assertEqual(stdscr.inch(2, 0) & curses.A_CHARTEXT, b[0])
def test_coordinate_errors(self):
# Addressing a cell outside the window raises curses.error.
diff --git
a/Misc/NEWS.d/next/Library/2026-07-17-20-31-00.gh-issue-153862.X3kjoY.rst
b/Misc/NEWS.d/next/Library/2026-07-17-20-31-00.gh-issue-153862.X3kjoY.rst
new file mode 100644
index 00000000000000..aa646dc6c49174
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-07-17-20-31-00.gh-issue-153862.X3kjoY.rst
@@ -0,0 +1,3 @@
+On a wide :mod:`curses` build, :meth:`curses.window.inch` now returns the
+locale-encoded byte of a non-ASCII character, matching
+:meth:`~curses.window.instr`, instead of the low byte of its code point.
diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c
index 51c4854aa5da52..41c20a7f96cd9f 100644
--- a/Modules/_cursesmodule.c
+++ b/Modules/_cursesmodule.c
@@ -1881,6 +1881,34 @@ _curses_window_insch_impl(PyCursesWindowObject *self,
int group_left_1,
return PyCursesCheckERR_ForWin(self, rtn, "insch");
}
+#ifdef HAVE_NCURSESW
+/* winch() returns the low 8 bits of the character's code point with no locale
+ conversion, unlike instr(), so recover the locale byte from the wide cell
+ when the character maps to exactly one byte, keeping the attribute and color
+ bits in RTN. A character with no single-byte form is left to winch(). */
+static chtype
+curses_cell_locale_byte(chtype rtn, const cchar_t *cell)
+{
+ wchar_t wstr[CCHARW_MAX + 1];
+ attr_t attrs;
+ short pair;
+ /* getcchar() is not guaranteed to write the text of an empty cell. */
+ wstr[0] = L'\0';
+ if (getcchar(cell, wstr, &attrs, &pair, NULL) == ERR
+ || wstr[0] == L'\0' || wstr[1] != L'\0')
+ {
+ return rtn;
+ }
+ /* wctob() mirrors ncurses' own _nc_to_char(): the single-byte form, or EOF
+ when the character has none in this locale. */
+ int byte = wctob(wstr[0]);
+ if (byte != EOF) {
+ rtn = (rtn & ~(chtype)A_CHARTEXT) | (unsigned char)byte;
+ }
+ return rtn;
+}
+#endif
+
/*[clinic input]
_curses.window.inch -> unsigned_long
@@ -1911,7 +1939,14 @@ _curses_window_inch_impl(PyCursesWindowObject *self, int
group_right_1,
else {
rtn = mvwinch(self->win, y, x);
}
-
+#ifdef HAVE_NCURSESW
+ cchar_t cell = {0};
+ if ((group_right_1 ? mvwin_wch(self->win, y, x, &cell)
+ : win_wch(self->win, &cell)) != ERR)
+ {
+ rtn = curses_cell_locale_byte(rtn, &cell);
+ }
+#endif
return rtn;
}
_______________________________________________
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]