Skip to content

Commit 2ec5e42

Browse files
gh-152415: Verify character output round-trips in test_output_character
Read each written character back with in_wch() or instr() rather than inch(), which on a wide build returns the low byte of the code point instead of the locale-encoded byte and so mangles a non-ASCII character of an 8-bit locale. This lets the int-argument cases cover '€'/'є', and adds matching coverage for the str argument. insch() with an int byte > 127 is checked only for Latin-1: on a wide build ncurses winsch stores a printable byte directly as a code point instead of decoding it through the locale. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent e4dc81b commit 2ec5e42

1 file changed

Lines changed: 49 additions & 10 deletions

File tree

Lib/test/test_curses.py

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,17 @@ def _encodable(self, s):
322322
return False
323323
return True
324324

325+
def _read_char(self, y, x):
326+
# The character written to a cell, read back for output checks. inch()
327+
# is unusable here: on a wide build it returns the low 8 bits of the
328+
# character's code point rather than its locale-encoded byte, mangling
329+
# anything outside Latin-1. in_wch() reads the wide cell directly;
330+
# without it, instr() re-encodes the cell to the window encoding.
331+
stdscr = self.stdscr
332+
if hasattr(stdscr, 'in_wch'):
333+
return str(stdscr.in_wch(y, x))
334+
return stdscr.instr(y, x, 1).decode(stdscr.encoding)
335+
325336
@requires_curses_window_meth('get_wch')
326337
def test_addch_combining(self):
327338
stdscr = self.stdscr
@@ -690,28 +701,56 @@ def test_output_character(self):
690701
stdscr.addch(2, 3, 'A', curses.A_BOLD)
691702
self.assertIs(stdscr.is_wintouched(), True)
692703

693-
# The same characters given as int chtype values (a byte > 127). A wide
694-
# build stores an int through the locale, so only a byte that is itself
695-
# the codepoint (Latin-1) round-trips; '€'/'є' are covered as text above.
696-
chartext = curses.A_CHARTEXT
704+
# The same characters supplied as an int chtype (a byte > 127). The
705+
# cell is read back with _read_char(), not inch(): on a wide build the
706+
# int is stored through the locale as a wide character that inch()
707+
# cannot represent for a character outside Latin-1.
697708
for c in ('é', '¤', '€', 'є'):
698709
try:
699710
b = c.encode(encoding)
700711
except UnicodeEncodeError:
701712
continue
702-
if len(b) != 1 or ord(c) >= 0x100:
713+
if len(b) != 1:
703714
continue
704715
v = b[0]
705716
with self.subTest(c=c):
706717
stdscr.addch(0, 0, v)
707-
self.assertEqual(stdscr.inch(0, 0) & chartext, v)
718+
self.assertEqual(self._read_char(0, 0), c)
708719
stdscr.addch(0, 1, v, curses.A_BOLD)
709-
self.assertEqual(stdscr.inch(0, 1), v | curses.A_BOLD)
710-
stdscr.insch(1, 0, v)
711-
self.assertEqual(stdscr.inch(1, 0) & chartext, v)
720+
self.assertEqual(self._read_char(0, 1), c)
721+
self.assertTrue(stdscr.inch(0, 1) & curses.A_BOLD)
712722
stdscr.move(2, 0)
713723
stdscr.echochar(v)
714-
self.assertEqual(stdscr.inch(2, 0) & chartext, v)
724+
self.assertEqual(self._read_char(2, 0), c)
725+
# insch() round-trips a byte only where its code point equals
726+
# the byte value (Latin-1): on a wide build ncurses winsch
727+
# stores a printable byte directly as a code point instead of
728+
# decoding it through the locale.
729+
if ord(c) < 0x100:
730+
stdscr.insch(1, 0, v)
731+
self.assertEqual(self._read_char(1, 0), c)
732+
733+
# The same characters supplied as a str. Unlike the int path above, a
734+
# str is stored as a wide-character cell on a wide build, so every
735+
# encodable character round-trips, insch() included. A multibyte
736+
# character does not fit a cell on a narrow build and is skipped.
737+
wide = hasattr(stdscr, 'in_wch')
738+
for c in ('é', '¤', '€', 'є'):
739+
if not self._encodable(c):
740+
continue
741+
if not wide and len(c.encode(encoding)) != 1:
742+
continue
743+
with self.subTest(c=c):
744+
stdscr.addch(0, 0, c)
745+
self.assertEqual(self._read_char(0, 0), c)
746+
stdscr.addch(0, 1, c, curses.A_BOLD)
747+
self.assertEqual(self._read_char(0, 1), c)
748+
self.assertTrue(stdscr.inch(0, 1) & curses.A_BOLD)
749+
stdscr.insch(1, 0, c)
750+
self.assertEqual(self._read_char(1, 0), c)
751+
stdscr.move(2, 0)
752+
stdscr.echochar(c)
753+
self.assertEqual(self._read_char(2, 0), c)
715754

716755
# echochar()
717756
stdscr.refresh()

0 commit comments

Comments
 (0)