Skip to content

Commit 1933f77

Browse files
[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 a83aaa9) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 6839aa8 commit 1933f77

4 files changed

Lines changed: 45 additions & 7 deletions

File tree

Doc/library/curses.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1082,6 +1082,8 @@ Window objects
10821082
The bottom 8 bits are the character proper and the upper bits are the attributes;
10831083
extract them with the :data:`A_CHARTEXT` and :data:`A_ATTRIBUTES` bit-masks,
10841084
and the color pair with :func:`pair_number`.
1085+
The character byte is the locale-encoded byte of the cell's character,
1086+
consistent with :meth:`instr`.
10851087

10861088

10871089
.. method:: window.insch(ch[, attr])

Lib/test/test_curses.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -506,10 +506,9 @@ def test_read_from_window(self):
506506
self.assertRaises(ValueError, stdscr.instr, -2)
507507
self.assertRaises(ValueError, stdscr.instr, 0, 2, -2)
508508
# A non-ASCII character of an 8-bit locale reads back as its encoded
509-
# byte (see _encodable for the set). instr() returns the locale bytes
510-
# for any single-byte character; inch() packs the text into a chtype, so
511-
# on a wide build it only round-trips a Latin-1 codepoint (byte ==
512-
# codepoint).
509+
# byte (see _encodable for the set). Both instr() and inch() return the
510+
# locale byte for any character that fits the locale's single-byte
511+
# encoding.
513512
encoding = stdscr.encoding
514513
for ch in ('A', 'é', '¤', '€', 'є'):
515514
try:
@@ -521,8 +520,7 @@ def test_read_from_window(self):
521520
with self.subTest(ch=ch):
522521
stdscr.addstr(2, 0, ch)
523522
self.assertEqual(stdscr.instr(2, 0, 1), b)
524-
if ord(ch) < 0x100:
525-
self.assertEqual(stdscr.inch(2, 0) & curses.A_CHARTEXT, b[0])
523+
self.assertEqual(stdscr.inch(2, 0) & curses.A_CHARTEXT, b[0])
526524

527525
def test_coordinate_errors(self):
528526
# Addressing a cell outside the window raises curses.error.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
On a wide :mod:`curses` build, :meth:`curses.window.inch` now returns the
2+
locale-encoded byte of a non-ASCII character, matching
3+
:meth:`~curses.window.instr`, instead of the low byte of its code point.

Modules/_cursesmodule.c

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1881,6 +1881,34 @@ _curses_window_insch_impl(PyCursesWindowObject *self, int group_left_1,
18811881
return PyCursesCheckERR_ForWin(self, rtn, "insch");
18821882
}
18831883

1884+
#ifdef HAVE_NCURSESW
1885+
/* winch() returns the low 8 bits of the character's code point with no locale
1886+
conversion, unlike instr(), so recover the locale byte from the wide cell
1887+
when the character maps to exactly one byte, keeping the attribute and color
1888+
bits in RTN. A character with no single-byte form is left to winch(). */
1889+
static chtype
1890+
curses_cell_locale_byte(chtype rtn, const cchar_t *cell)
1891+
{
1892+
wchar_t wstr[CCHARW_MAX + 1];
1893+
attr_t attrs;
1894+
short pair;
1895+
/* getcchar() is not guaranteed to write the text of an empty cell. */
1896+
wstr[0] = L'\0';
1897+
if (getcchar(cell, wstr, &attrs, &pair, NULL) == ERR
1898+
|| wstr[0] == L'\0' || wstr[1] != L'\0')
1899+
{
1900+
return rtn;
1901+
}
1902+
/* wctob() mirrors ncurses' own _nc_to_char(): the single-byte form, or EOF
1903+
when the character has none in this locale. */
1904+
int byte = wctob(wstr[0]);
1905+
if (byte != EOF) {
1906+
rtn = (rtn & ~(chtype)A_CHARTEXT) | (unsigned char)byte;
1907+
}
1908+
return rtn;
1909+
}
1910+
#endif
1911+
18841912
/*[clinic input]
18851913
_curses.window.inch -> unsigned_long
18861914
@@ -1911,7 +1939,14 @@ _curses_window_inch_impl(PyCursesWindowObject *self, int group_right_1,
19111939
else {
19121940
rtn = mvwinch(self->win, y, x);
19131941
}
1914-
1942+
#ifdef HAVE_NCURSESW
1943+
cchar_t cell = {0};
1944+
if ((group_right_1 ? mvwin_wch(self->win, y, x, &cell)
1945+
: win_wch(self->win, &cell)) != ERR)
1946+
{
1947+
rtn = curses_cell_locale_byte(rtn, &cell);
1948+
}
1949+
#endif
19151950
return rtn;
19161951
}
19171952

0 commit comments

Comments
 (0)