Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 16 additions & 9 deletions Lib/curses/textpad.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,17 @@ def _update_max_yx(self):
self.maxx = maxx - 1

def _decode(self, ch):
# Decode an integer keystroke or byte to text with the window's encoding.
# A_CHARTEXT drops any attribute bits.
return bytes([ch & curses.A_CHARTEXT]).decode(self.win.encoding, 'replace')
# An integer keystroke is a byte: get_wch() passes a character as a
# string, and getch() reports one as its bytes in the encoding.
return bytes([ch & 0xff]).decode(self.win.encoding, 'replace')

def _printable_key(self, ch):
# Whether the integer keystroke is a printable character, not a key code:
# 0..255 are character bytes, larger values are function keys.
return ch <= 0xff and self._decode(ch).isprintable()
# Whether the integer keystroke is a printable character rather than a
# key code. Key codes occupy [curses.KEY_MIN, curses.KEY_MAX], which
# may start above a byte.
return (ch <= 0xff
and not curses.KEY_MIN <= ch <= curses.KEY_MAX
and self._decode(ch).isprintable())

def _end_of_line(self, y):
"""Go to the location of the first blank on the given line,
Expand Down Expand Up @@ -93,14 +96,18 @@ def _insert_printable_char(self, ch):
if y >= self.maxy and x >= self.maxx:
# Use insch() in the lower-right cell; addch() there would push
# the cursor out of the window (an error, and it scrolls a
# scrollable window). insch() does not decode an int byte
# through the locale on a wide build, so pass it as text.
# scrollable window).
if isinstance(ch, int):
self.win.insch(self._decode(ch), ch & curses.A_ATTRIBUTES)
else:
self.win.insch(ch)
break
self.win.addch(ch)
# Pass the character as text: an integer is a byte, which addch()
# takes as a code point on a wide build.
if isinstance(ch, int):
self.win.addch(self._decode(ch), ch & curses.A_ATTRIBUTES)
else:
self.win.addch(ch)
# In insert mode keep shifting cells right until a blank one.
if not self.insert_mode or not str(oldch).isprintable():
break
Expand Down
119 changes: 59 additions & 60 deletions Lib/test/test_curses.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,22 +46,7 @@ def wrapped(self, *args, **kwargs):
return wrapped
return deco

def _wide_build():
# True on a build that stores wide-character cells (built against ncursesw).
# A wide build accepts a spacing character plus a combining mark in a single
# cell; a narrow build accepts only one character per cell. This stays a
# reliable wide/narrow signal even as the wide-character functions (get_wch()
# and friends) become available on narrow builds too, because the
# multi-codepoint cell capacity itself is build-specific.
if not hasattr(curses, 'complexchar'):
return hasattr(curses.window, 'get_wch')
try:
curses.complexchar('e\u0301') # 'e' + combining acute: two code points
except ValueError:
return False
return True

WIDE_BUILD = _wide_build()
WIDE_BUILD = import_module('_curses')._wide_character_support

def requires_wide_build(test):
@functools.wraps(test)
Expand Down Expand Up @@ -404,6 +389,15 @@ def _storable(self, s):
return True
return len(s.encode(self.stdscr.encoding)) == len(s)

def _char_code(self, ch):
# The integer the int-input API (addch(int), do_command()) uses for a
# character, or None if it has none: a cell holds a single locale byte.
try:
b = ch.encode(self.stdscr.encoding)
except UnicodeEncodeError:
return None
return b[0] if len(b) == 1 else None

def _read_char(self, y, x):
# The character written to a cell, read back for output checks. inch()
# is unusable here: on a wide build it returns the low 8 bits of the
Expand Down Expand Up @@ -783,18 +777,15 @@ def test_output_character(self):
stdscr.addch(2, 3, 'A', curses.A_BOLD)
self.assertIs(stdscr.is_wintouched(), True)

# The same characters supplied as an int chtype (a byte > 127). The
# cell is read back with _read_char(), not inch(): on a wide build the
# int is stored through the locale as a wide character that inch()
# cannot represent for a character outside Latin-1.
# The same characters supplied as an int chtype. The cell is read back
# with _read_char(), not inch(): on a wide build the int is stored as a
# wide character that inch() cannot represent for a character outside
# Latin-1. The int is decoded as a locale byte, so only a single-byte
# character round-trips.
for c in ('é', '¤', '€', 'є'):
try:
b = c.encode(encoding)
except UnicodeEncodeError:
continue
if len(b) != 1:
v = self._char_code(c)
if v is None:
continue
v = b[0]
with self.subTest(c=c):
stdscr.addch(0, 0, v)
self.assertEqual(self._read_char(0, 0), c)
Expand Down Expand Up @@ -973,22 +964,21 @@ def test_read_from_window(self):
self.assertEqual(stdscr.instr(0, 2, 4), b'BCD ')
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). Both instr() and inch() return the
# locale byte for any character that fits the locale's single-byte
# encoding.
encoding = stdscr.encoding
# instr(y, x, 1) reads a single cell byte, so only a character that the
# window encoding maps to one byte is checked. inch() returns the cell
# value, which is the locale byte.
for ch in ('A', 'é', '¤', '€', 'є'):
try:
b = ch.encode(encoding)
b = ch.encode(stdscr.encoding)
except UnicodeEncodeError:
continue
if len(b) != 1:
continue
v = self._char_code(ch)
with self.subTest(ch=ch):
stdscr.addstr(2, 0, ch)
self.assertEqual(stdscr.instr(2, 0, 1), b)
self.assertEqual(stdscr.inch(2, 0), b[0])
self.assertEqual(stdscr.inch(2, 0), v)

def test_coordinate_errors(self):
# Addressing a cell outside the window raises curses.error.
Expand Down Expand Up @@ -1030,9 +1020,23 @@ def test_getch(self):
self.assertEqual(win.getch(), b'm'[0])
self.assertEqual(win.getch(), b'\n'[0])

# A key value > 127 is delivered unchanged (it is not locale text).
curses.ungetch(0xE9)
self.assertEqual(win.getch(), 0xE9)
# A non-ASCII character encodable as a single byte in the locale
# round-trips as that byte.
encoding = self.stdscr.encoding
for ch in ('é', '¤', '€', 'є'):
try:
b = ch.encode(encoding)
except UnicodeEncodeError:
continue
if len(b) != 1:
continue
with self.subTest(ch=ch):
curses.ungetch(self._char_code(ch))
self.assertEqual(win.getch(), b[0])

# A key code is delivered unchanged.
curses.ungetch(curses.KEY_LEFT)
self.assertEqual(win.getch(), curses.KEY_LEFT)

def test_getstr(self):
win = curses.newwin(5, 12, 5, 2)
Expand Down Expand Up @@ -1277,28 +1281,24 @@ def test_background(self):
self.assertEqual(win.inch(0, 0), b'L'[0] | curses.A_REVERSE)
self.assertEqual(win.inch(0, 5), b'#'[0] | curses.A_REVERSE)

# A non-ASCII background character of an 8-bit locale reads back as its
# encoded byte. See _encodable for the character set.
# A non-ASCII background character reads back as its cell value, the
# locale byte.
win.bkgd(' ')
encoding = win.encoding
for ch in ('é', '¤', '€', 'є'):
try:
b = ch.encode(encoding)
except UnicodeEncodeError:
continue
if len(b) != 1:
v = self._char_code(ch)
if v is None:
continue
with self.subTest(ch=ch):
win.bkgd(ch)
self.assertEqual(win.getbkgd(), b[0])
self.assertEqual(win.getbkgd(), v)
if ord(ch) < 0x100:
# The same byte given as an int. A wide build stores it
# through the locale, so only a Latin-1 byte round-trips.
win.bkgd(' ')
win.bkgdset(b[0])
self.assertEqual(win.getbkgd(), b[0])
win.bkgd(b[0])
self.assertEqual(win.getbkgd(), b[0])
win.bkgdset(v)
self.assertEqual(win.getbkgd(), v)
win.bkgd(v)
self.assertEqual(win.getbkgd(), v)

def test_overlay(self):
srcwin = curses.newwin(5, 18, 3, 4)
Expand Down Expand Up @@ -1488,20 +1488,21 @@ def test_borders_and_lines(self):
self.assertEqual(win.inch(2, 1), b';'[0] | curses.A_STANDOUT)
self.assertEqual(win.inch(3, 1), b'a'[0])

# A border or line character of an 8-bit locale round-trips as its
# encoded byte. See _encodable for the character set.
encoding = win.encoding
# A border or line character that fits a single cell byte reads back
# via instr() as that byte and via inch() as the cell value.
for ch in ('é', '¤', '€', 'є'):
try:
b = ch.encode(encoding)
b = ch.encode(win.encoding)
except UnicodeEncodeError:
continue
if len(b) != 1:
continue
v = self._char_code(ch)
with self.subTest(ch=ch):
win.erase()
win.hline(2, 0, ch, 5)
self.assertEqual(win.instr(2, 0, 5), b * 5)
self.assertEqual(win.inch(2, 0) & curses.A_CHARTEXT, v)
win.vline(0, 0, ch, 3)
self.assertEqual(win.instr(0, 0, 1), b)
self.assertEqual(win.instr(1, 0, 1), b)
Expand All @@ -1510,7 +1511,6 @@ def test_borders_and_lines(self):
if ord(ch) < 0x100:
# The same byte given as an int. A wide build stores it
# through the locale, so only a Latin-1 byte round-trips.
v = b[0]
win.erase()
win.hline(2, 0, v, 5)
self.assertEqual(win.instr(2, 0, 5), b * 5)
Expand Down Expand Up @@ -2894,12 +2894,11 @@ def test_init(self):
def test_insert(self):
"""Test inserting a printable character."""
self.mock_win.reset_mock()
self.textbox.do_command(ord('a'))
self.mock_win.addch.assert_called_with(ord('a'))
self.textbox.do_command(ord('b'))
self.mock_win.addch.assert_called_with(ord('b'))
self.textbox.do_command(ord('c'))
self.mock_win.addch.assert_called_with(ord('c'))
# An integer keystroke is decoded to text: addch() would take it as a
# code point on a wide build.
for ch in 'abc':
self.textbox.do_command(ord(ch))
self.mock_win.addch.assert_called_with(ch, 0)
self.mock_win.reset_mock()

def test_delete(self):
Expand Down
Loading
Loading