Skip to content
Open
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
92 changes: 92 additions & 0 deletions Doc/library/curses.rst
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,16 @@ The module :mod:`!curses` defines the following functions:
the curses library itself.


.. function:: erasewchar()

Return the user's current erase character as a one-character string.
This is the wide-character variant of :func:`erasechar`. Availability
depends on building Python against a wide-character-aware version of the
underlying curses library.

.. versionadded:: next


.. function:: filter()

The :func:`.filter` routine, if used, must be called before :func:`initscr` is
Expand Down Expand Up @@ -379,6 +389,16 @@ The module :mod:`!curses` defines the following functions:
by the curses library itself.


.. function:: killwchar()

Return the user's current line kill character as a one-character string.
This is the wide-character variant of :func:`killchar`. Availability
depends on building Python against a wide-character-aware version of the
underlying curses library.

.. versionadded:: next


.. function:: longname()

Return a bytes object containing the terminfo long name field describing the current
Expand Down Expand Up @@ -690,6 +710,18 @@ The module :mod:`!curses` defines the following functions:
example as ``b'^C'``. Printing characters are left as they are.


.. function:: wunctrl(ch)

Return a string which is a printable representation of the wide character *ch*.
Control characters are represented as a caret followed by the character, for
example as ``'^C'``. Printing characters are left as they are. This is the
wide-character variant of :func:`unctrl`, returning a :class:`str` rather than
:class:`bytes`. Availability depends on building Python against a
wide-character-aware version of the underlying curses library.

.. versionadded:: next


.. function:: ungetch(ch)

Push *ch* so the next :meth:`~window.getch` will return it.
Expand Down Expand Up @@ -770,12 +802,19 @@ Window objects
character previously painted at that location. By default, the character
position and attributes are the current settings for the window object.

*ch* may be a single character, optionally followed by combining
characters, that together occupy one character cell.

.. note::

Writing outside the window, subwindow, or pad raises a :exc:`curses.error`.
Attempting to write to the lower-right corner of a window, subwindow,
or pad will cause an exception to be raised after the character is printed.

.. versionchanged:: next
A character may now be given as a string of a base character followed
by combining characters, instead of only a single character.


.. method:: window.addnstr(str, n[, attr])
window.addnstr(y, x, str, n[, attr])
Expand Down Expand Up @@ -834,6 +873,9 @@ Window objects
* Wherever the former background character appears, it is changed to the new
background character.

.. versionchanged:: next
Wide and combining characters are now accepted.


.. method:: window.bkgdset(ch[, attr])

Expand All @@ -844,6 +886,9 @@ Window objects
characters. The background becomes a property of the character and moves with
the character through any scrolling and insert/delete line/character operations.

.. versionchanged:: next
Wide and combining characters are now accepted.


.. method:: window.border([ls[, rs[, ts[, bs[, tl[, tr[, bl[, br]]]]]]]])

Expand Down Expand Up @@ -877,12 +922,20 @@ Window objects
| *br* | Bottom-right corner | :const:`ACS_LRCORNER` |
+-----------+---------------------+-----------------------+

.. versionchanged:: next
Wide and combining characters are now accepted. A single call cannot mix
them with integer or byte characters.


.. method:: window.box([vertch, horch])

Similar to :meth:`border`, but both *ls* and *rs* are *vertch* and both *ts* and
*bs* are *horch*. The default corner characters are always used by this function.

.. versionchanged:: next
Wide and combining characters are now accepted. A single call cannot mix
them with integer or byte characters.


.. method:: window.chgat(attr)
window.chgat(num, attr)
Expand Down Expand Up @@ -951,6 +1004,9 @@ Window objects
Add character *ch* with attribute *attr*, and immediately call :meth:`refresh`
on the window.

.. versionchanged:: next
Wide and combining characters are now accepted.


.. method:: window.enclose(y, x)

Expand Down Expand Up @@ -1038,6 +1094,20 @@ Window objects
The maximum value for *n* was increased from 1023 to 2047.


.. method:: window.get_wstr()
window.get_wstr(n)
window.get_wstr(y, x)
window.get_wstr(y, x, n)

Read a string from the user, with primitive line editing capacity.
This is the wide-character variant of :meth:`getstr`: it returns a
:class:`str` rather than a :class:`bytes` object, so it can return
characters that are not representable in the window's encoding.
At most *n* characters are read; *n* defaults to and cannot exceed 2047.

.. versionadded:: next


.. method:: window.getyx()

Return a tuple ``(y, x)`` of current cursor position relative to the window's
Expand All @@ -1051,6 +1121,9 @@ Window objects
the character *ch* with attributes *attr*. The line stops at the right edge
of the window if fewer than *n* cells are available.

.. versionchanged:: next
Wide and combining characters are now accepted.


.. method:: window.idcok(flag)

Expand Down Expand Up @@ -1088,6 +1161,9 @@ Window objects
cursor are shifted one position right, with the rightmost character on the
line being lost. The cursor position does not change.

.. versionchanged:: next
Wide and combining characters are now accepted.


.. method:: window.insdelln(nlines)

Expand Down Expand Up @@ -1137,6 +1213,19 @@ Window objects
The maximum value for *n* was increased from 1023 to 2047.


.. method:: window.in_wstr([n])
window.in_wstr(y, x[, n])

Return a string of characters, extracted from the window starting at the
current cursor position, or at *y*, *x* if specified. This is the
wide-character variant of :meth:`instr`: it returns a :class:`str` rather
than a :class:`bytes` object, so it can return characters that are not
representable in the window's encoding. Attributes and color information
are stripped from the characters. The maximum value for *n* is 2047.

.. versionadded:: next


.. method:: window.is_linetouched(line)

Return ``True`` if the specified line was modified since the last call to
Expand Down Expand Up @@ -1386,6 +1475,9 @@ Window objects
Display a vertical line starting at ``(y, x)`` with length *n* consisting of the
character *ch* with attributes *attr*.

.. versionchanged:: next
Wide and combining characters are now accepted.


Constants
---------
Expand Down
19 changes: 19 additions & 0 deletions Doc/whatsnew/3.16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,25 @@ Improved modules
curses
------

* The :mod:`curses` character-cell window methods now accept a full character
cell --- a spacing character optionally followed by combining characters ---
in addition to a single integer or byte character. This affects
:meth:`~curses.window.addch`, :meth:`~curses.window.bkgd`,
:meth:`~curses.window.bkgdset`, :meth:`~curses.window.border`,
:meth:`~curses.window.box`, :meth:`~curses.window.echochar`,
:meth:`~curses.window.hline`, :meth:`~curses.window.insch` and
:meth:`~curses.window.vline`.
Also add the wide-character read methods :meth:`~curses.window.get_wstr` and
:meth:`~curses.window.in_wstr`, the counterparts of
:meth:`~curses.window.getstr` and :meth:`~curses.window.instr` that return a
:class:`str` rather than :class:`bytes`,
and the module functions :func:`curses.erasewchar`, :func:`curses.killwchar`
and :func:`curses.wunctrl`, the wide-character counterparts of
:func:`curses.erasechar`, :func:`curses.killchar` and :func:`curses.unctrl`.
These features are only available when built against the wide-character
ncursesw library.
(Contributed by Serhiy Storchaka in :gh:`151757`.)

* Add :func:`curses.nofilter`, which undoes the effect of :func:`curses.filter`.
(Contributed by Serhiy Storchaka in :gh:`151744`.)

Expand Down
116 changes: 107 additions & 9 deletions Lib/test/test_curses.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,69 @@ def test_refresh_control(self):
self.assertIs(win.is_wintouched(), syncok)
self.assertIs(stdscr.is_wintouched(), syncok)

@requires_curses_window_meth('get_wch')
def test_addch_combining(self):
# A character cell may hold a spacing char plus combining marks.
stdscr = self.stdscr
stdscr.move(0, 0)
stdscr.addch('e\u0301') # 'e' + COMBINING ACUTE ACCENT
stdscr.addch(1, 0, 'a\u0323\u0300') # base plus two combining marks
# Too many code points to fit in a single character cell.
self.assertRaises(TypeError, stdscr.addch, 'e' + '\u0301' * 10)
# Only the first code point may be a spacing character.
self.assertRaises(ValueError, stdscr.addch, 'ab')
self.assertRaises(ValueError, stdscr.addch, 'a\u0301b')
# A lone control character is allowed (like addch(ord('\n'))), but it
# cannot be combined with other characters, as base or otherwise.
stdscr.addch('\n')
self.assertRaises(ValueError, stdscr.addch, 'a\n')
self.assertRaises(ValueError, stdscr.addch, '\n\u0301')
self.assertRaises(ValueError, stdscr.addch, '\ne\u0301')

@requires_curses_window_meth('get_wch')
def test_addch_emoji(self):
# curses has no grapheme-cluster support: a cell holds one spacing
# character plus zero-width combining characters. A lone emoji fits,
# as does an emoji with a zero-width variation selector.
stdscr = self.stdscr
stdscr.addch(0, 0, '\U0001f600') # single emoji
stdscr.addch(1, 0, '\u263a\ufe0f') # WHITE SMILING FACE + VS-16
# An emoji ZWJ sequence or an emoji with a modifier is more than one
# spacing character and cannot share a single cell.
self.assertRaises(ValueError, stdscr.addch,
'\U0001f44d\U0001f3fd') # thumbs up + skin tone
self.assertRaises(ValueError, stdscr.addch,
'\U0001f468\u200d\U0001f469') # man ZWJ woman

@requires_curses_window_meth('get_wch')
def test_wide_characters(self):
# Wide and combining characters in the character-cell methods.
stdscr = self.stdscr
combining = 'e\u0301' # 'e' + COMBINING ACUTE ACCENT
vline, hline = '\u2502', '\u2500' # box-drawing vertical/horizontal
stdscr.move(0, 0)
stdscr.echochar(combining)
stdscr.insch(1, 0, combining)
stdscr.hline(2, 0, hline, 5)
stdscr.vline(3, 0, vline, 3)
stdscr.bkgdset(combining)
stdscr.bkgd(combining)
stdscr.border(vline, vline, hline, hline)
stdscr.box(vline, hline)
# border() and box() cannot mix integer and wide-string characters.
self.assertRaises(TypeError, stdscr.box, vline, ord('-'))


@requires_curses_window_meth('in_wstr')
def test_in_wstr(self):
# The wide-character window read returns a str (instr returns bytes).
stdscr = self.stdscr
s = 'a\u00e9\u2502z' # 'a', 'e'+acute (precomposed), box vline, 'z'
stdscr.addstr(0, 0, s)
self.assertEqual(stdscr.in_wstr(0, 0, len(s)), s)
self.assertIsInstance(stdscr.instr(0, 0, len(s)), bytes)


def test_output_character(self):
stdscr = self.stdscr
encoding = stdscr.encoding
Expand Down Expand Up @@ -281,13 +344,16 @@ def test_output_character(self):
stdscr.echochar('A')
stdscr.echochar(b'A')
stdscr.echochar(65)
with self.assertRaises((UnicodeEncodeError, OverflowError)):
# Unicode is not fully supported yet, but at least it does
# not crash.
# It is supposed to fail because either the character is
# not encodable with the current encoding, or it is encoded to
# a multibyte sequence.
stdscr.echochar('\u0114')
c = '\u0114'
try:
stdscr.echochar(c)
except UnicodeEncodeError:
# The character is not encodable with the current encoding.
self.assertRaises(UnicodeEncodeError, c.encode, encoding)
except OverflowError:
# The character is encoded to a multibyte sequence.
encoded = c.encode(encoding)
self.assertNotEqual(len(encoded), 1, repr(encoded))
stdscr.echochar('A', curses.A_BOLD)
self.assertIs(stdscr.is_wintouched(), False)

Expand Down Expand Up @@ -742,7 +808,6 @@ def test_borders_and_lines(self):
self.assertEqual(win.inch(3, 1), b'a'[0])

def test_unctrl(self):
# TODO: wunctrl()
self.assertEqual(curses.unctrl(b'A'), b'A')
self.assertEqual(curses.unctrl('A'), b'A')
self.assertEqual(curses.unctrl(65), b'A')
Expand All @@ -753,6 +818,21 @@ def test_unctrl(self):
self.assertRaises(TypeError, curses.unctrl, b'AB')
self.assertRaises(TypeError, curses.unctrl, '')
self.assertRaises(TypeError, curses.unctrl, 'AB')

@requires_curses_func('wunctrl')
def test_wunctrl(self):
# The wide-character variant of unctrl() returns a str.
self.assertEqual(curses.wunctrl(b'A'), 'A')
self.assertEqual(curses.wunctrl('A'), 'A')
self.assertEqual(curses.wunctrl(65), 'A')
self.assertEqual(curses.wunctrl('\n'), '^J')
self.assertEqual(curses.wunctrl(10), '^J')
self.assertEqual(curses.wunctrl('é'), 'é') # printable
self.assertRaises(TypeError, curses.wunctrl, b'')
self.assertRaises(TypeError, curses.wunctrl, b'AB')
self.assertRaises(TypeError, curses.wunctrl, '')
# More than one spacing character is not a single cell.
self.assertRaises(ValueError, curses.wunctrl, 'AB')
self.assertRaises(OverflowError, curses.unctrl, 2**64)

def test_endwin(self):
Expand Down Expand Up @@ -800,7 +880,7 @@ def test_misc_module_funcs(self):
curses.newpad(50, 50)

def test_env_queries(self):
# TODO: term_attrs(), erasewchar(), killwchar()
# TODO: term_attrs()
self.assertIsInstance(curses.termname(), bytes)
self.assertIsInstance(curses.longname(), bytes)
self.assertIsInstance(curses.baudrate(), int)
Expand All @@ -815,6 +895,24 @@ def test_env_queries(self):
self.assertIsInstance(c, bytes)
self.assertEqual(len(c), 1)

# The erase and kill characters are a property of the controlling
# terminal: the wide variants report ERR (raising curses.error) without
# one, while the narrow variants above return an unspecified byte.
try:
tty_fd = os.open(os.ctermid(), os.O_RDONLY)
except OSError:
tty_fd = None
if tty_fd is not None:
os.close(tty_fd)
if hasattr(curses, 'erasewchar'):
c = curses.erasewchar()
self.assertIsInstance(c, str)
self.assertEqual(len(c), 1)
if hasattr(curses, 'killwchar'):
c = curses.killwchar()
self.assertIsInstance(c, str)
self.assertEqual(len(c), 1)

def test_output_options(self):
stdscr = self.stdscr

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
The :mod:`curses` character-cell window methods now accept a full character
cell -- a spacing character optionally followed by combining characters -- in
addition to a single integer or byte character. Add the wide-character read
methods :meth:`curses.window.get_wstr` and :meth:`curses.window.in_wstr`, and
the functions :func:`curses.erasewchar`, :func:`curses.killwchar` and
:func:`curses.wunctrl`. These features are only available when built against
the wide-character ncursesw library.
Loading
Loading