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
98 changes: 89 additions & 9 deletions Doc/library/curses.rst
Original file line number Diff line number Diff line change
Expand Up @@ -941,7 +941,8 @@ Window objects

.. 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.
by combining characters, instead of only a single character, or as a
:class:`complexchar` cell.


.. method:: window.addnstr(str, n[, attr])
Expand Down Expand Up @@ -1051,7 +1052,8 @@ Window objects
background character.

.. versionchanged:: next
Wide and combining characters are now accepted.
Wide and combining characters, and :class:`complexchar` cells, are now
accepted.


.. method:: window.bkgdset(ch[, attr])
Expand All @@ -1064,7 +1066,8 @@ Window objects
the character through any scrolling and insert/delete line/character operations.

.. versionchanged:: next
Wide and combining characters are now accepted.
Wide and combining characters, and :class:`complexchar` cells, are now
accepted.


.. method:: window.border([ls[, rs[, ts[, bs[, tl[, tr[, bl[, br]]]]]]]])
Expand Down Expand Up @@ -1100,7 +1103,8 @@ Window objects
+-----------+---------------------+-----------------------+

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


Expand All @@ -1110,7 +1114,8 @@ Window objects
*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
Wide and combining characters, and :class:`complexchar` cells, are now
accepted. A single call cannot mix
them with integer or byte characters.


Expand Down Expand Up @@ -1182,7 +1187,8 @@ Window objects
on the window.

.. versionchanged:: next
Wide and combining characters are now accepted.
Wide and combining characters, and :class:`complexchar` cells, are now
accepted.


.. method:: window.enclose(y, x)
Expand Down Expand Up @@ -1221,6 +1227,20 @@ Window objects
Return the given window's current background character/attribute pair.


.. method:: window.getbkgrnd()

Return the given window's current background as a :class:`complexchar`.
This is the wide-character variant of :meth:`getbkgd`: the returned object
carries the background character together with its attributes and color pair,
and the color pair is not limited to the value that fits in a
:func:`color_pair`.

This method is only available if Python was built against a wide-character
version of the underlying curses library.

.. versionadded:: next


.. method:: window.getch([y, x])

Get a character. Note that the integer returned does *not* have to be in ASCII
Expand Down Expand Up @@ -1325,7 +1345,8 @@ Window objects
of the window if fewer than *n* cells are available.

.. versionchanged:: next
Wide and combining characters are now accepted.
Wide and combining characters, and :class:`complexchar` cells, are now
accepted.


.. method:: window.idcok(flag)
Expand Down Expand Up @@ -1356,6 +1377,20 @@ Window objects
the character proper, and upper bits are the attributes.


.. method:: window.in_wch([y, x])

Return the complex character at the given position in the window as a
:class:`complexchar`. This is the wide-character variant of :meth:`inch`:
the returned object carries the cell's text (a spacing character optionally
followed by combining characters) together with its attributes and color
pair, none of which :meth:`inch` can represent.

This method is only available if Python was built against a wide-character
version of the underlying curses library.

.. versionadded:: next


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

Expand All @@ -1365,7 +1400,8 @@ Window objects
line being lost. The cursor position does not change.

.. versionchanged:: next
Wide and combining characters are now accepted.
Wide and combining characters, and :class:`complexchar` cells, are now
accepted.


.. method:: window.insdelln(nlines)
Expand Down Expand Up @@ -1776,7 +1812,8 @@ Window objects
character *ch* with attributes *attr*.

.. versionchanged:: next
Wide and combining characters are now accepted.
Wide and combining characters, and :class:`complexchar` cells, are now
accepted.


.. _curses-screen-objects:
Expand Down Expand Up @@ -1833,6 +1870,49 @@ Screen objects
.. versionadded:: next


.. _curses-complexchar-objects:

Complex character objects
-------------------------

.. class:: complexchar(text, /, attr=0, pair=0)

A *complex character* (or *complexchar*) is an immutable styled
wide-character cell: a spacing character optionally followed by combining
characters, together with a set of attributes and a color pair.

*text* is the cell's text, *attr* a combination of the
:ref:`WA_* attributes <curses-wa-constants>` (equivalent to the matching
``A_*`` constants), and *pair* a color pair number. Unlike the packed
:class:`chtype <int>` used by :meth:`~window.inch` and the ``A_*`` methods,
the color pair is stored separately and is not limited to the value that
fits in a :func:`color_pair`.

Complex characters are returned by :meth:`window.in_wch` and
:meth:`window.getbkgrnd`, and are accepted (along with an integer, a byte
or a string) by the character-cell methods such as :meth:`window.addch`,
:meth:`window.insch`, :meth:`window.bkgd`, :meth:`window.border`,
:meth:`window.hline` and :meth:`window.vline`. A complex character already
carries its own rendition, so it cannot be combined with an explicit *attr*
argument.

:func:`str` returns the cell's text; two complex characters are equal when
their text, attributes and color pair all match.

This type is only available if Python was built against a wide-character
version of the underlying curses library.

.. attribute:: attr

The attributes of the character cell (read-only).

.. attribute:: pair

The color pair number of the character cell (read-only).

.. versionadded:: next


Constants
---------

Expand Down
10 changes: 10 additions & 0 deletions Doc/whatsnew/3.16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,16 @@ curses
attribute value, and the corresponding ``WA_*`` attribute constants.
(Contributed by Serhiy Storchaka in :gh:`152219`.)

* Add the :class:`curses.complexchar` type, representing a styled
wide-character cell (its text, attributes and color pair), and the window
methods :meth:`~curses.window.in_wch` and :meth:`~curses.window.getbkgrnd`
that return one --- the wide-character counterparts of
:meth:`~curses.window.inch` and :meth:`~curses.window.getbkgd`. The
character-cell methods, such as :meth:`~curses.window.addch` and
:meth:`~curses.window.border`, now also accept a
:class:`~curses.complexchar`.
(Contributed by Serhiy Storchaka in :gh:`152233`.)

gzip
----

Expand Down
2 changes: 2 additions & 0 deletions Include/internal/pycore_global_objects_fini_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Include/internal/pycore_global_strings.h
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(asend)
STRUCT_FOR_ID(ast)
STRUCT_FOR_ID(athrow)
STRUCT_FOR_ID(attr)
STRUCT_FOR_ID(attribute)
STRUCT_FOR_ID(autocommit)
STRUCT_FOR_ID(backtick)
Expand Down Expand Up @@ -702,6 +703,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(pad)
STRUCT_FOR_ID(padded)
STRUCT_FOR_ID(pages)
STRUCT_FOR_ID(pair)
STRUCT_FOR_ID(parameter)
STRUCT_FOR_ID(parent)
STRUCT_FOR_ID(password)
Expand Down
2 changes: 2 additions & 0 deletions Include/internal/pycore_runtime_init_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions Include/internal/pycore_unicodeobject_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

114 changes: 114 additions & 0 deletions Lib/test/test_curses.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,36 @@ def test_wide_characters(self):
# border() and box() cannot mix integer and wide-string characters.
self.assertRaises(TypeError, stdscr.box, vline, ord('-'))

@requires_curses_func('complexchar')
def test_complexchar_in_cell_methods(self):
# Every single-character-cell method also accepts a complexchar, whose
# attributes and color pair come from the cell itself.
stdscr = self.stdscr
cc = curses.complexchar('A', curses.A_BOLD)
v = curses.complexchar('|')
h = curses.complexchar('-')
stdscr.move(0, 0)
stdscr.addch(0, 0, cc)
self.assertEqual(str(stdscr.in_wch(0, 0)), 'A')
self.assertTrue(stdscr.in_wch(0, 0).attr & curses.A_BOLD)
stdscr.insch(1, 0, cc)
stdscr.echochar(cc)
stdscr.bkgdset(cc)
stdscr.bkgd(cc)
stdscr.hline(2, 0, h, 3)
stdscr.vline(3, 0, v, 3)
stdscr.border(v, v, h, h)
stdscr.box(v, h)
# A complexchar already carries its rendition, so combining it with an
# explicit attr argument is rejected.
self.assertRaises(TypeError, stdscr.addch, cc, curses.A_BOLD)
self.assertRaises(TypeError, stdscr.addch, 0, 0, cc, curses.A_BOLD)
self.assertRaises(TypeError, stdscr.insch, cc, curses.A_BOLD)
self.assertRaises(TypeError, stdscr.echochar, cc, curses.A_BOLD)
self.assertRaises(TypeError, stdscr.bkgd, cc, curses.A_BOLD)
self.assertRaises(TypeError, stdscr.bkgdset, cc, curses.A_BOLD)
self.assertRaises(TypeError, stdscr.hline, h, 3, curses.A_BOLD)
self.assertRaises(TypeError, stdscr.vline, v, 3, curses.A_BOLD)

@requires_curses_window_meth('in_wstr')
def test_in_wstr(self):
Expand All @@ -355,6 +385,90 @@ def test_in_wstr(self):
self.assertEqual(stdscr.in_wstr(0, 0, len(s)), s)
self.assertIsInstance(stdscr.instr(0, 0, len(s)), bytes)

@requires_curses_func('complexchar')
def test_complexchar(self):
# A complexchar is a styled wide-character cell: str() is its text,
# and the attr and pair attributes are its rendition.
cc = curses.complexchar('A', curses.A_BOLD)
self.assertEqual(str(cc), 'A')
self.assertTrue(cc.attr & curses.A_BOLD)
self.assertEqual(cc.pair, 0)
# A spacing character optionally followed by combining characters.
if self._encodable('e\u0301'):
self.assertEqual(str(curses.complexchar('e\u0301')), 'e\u0301')
# Defaults: no attributes, color pair 0.
cc = curses.complexchar('z')
self.assertEqual(str(cc), 'z')
self.assertEqual(cc.attr, 0)
self.assertEqual(cc.pair, 0)
# Immutable rendition.
self.assertRaises(AttributeError, setattr, cc, 'attr', 1)
self.assertRaises(AttributeError, setattr, cc, 'pair', 1)
# Equality and hashing compare text, attributes and color pair.
self.assertEqual(curses.complexchar('A', curses.A_BOLD),
curses.complexchar('A', curses.A_BOLD))
self.assertEqual(hash(curses.complexchar('A', curses.A_BOLD)),
hash(curses.complexchar('A', curses.A_BOLD)))
self.assertNotEqual(curses.complexchar('A'),
curses.complexchar('A', curses.A_BOLD))
self.assertNotEqual(curses.complexchar('A'), curses.complexchar('B'))
# repr() shows only a non-default attr/pair, and is a constructor call.
ns = {'_curses': sys.modules[type(cc).__module__]}
self.assertNotIn('attr=', repr(curses.complexchar('z')))
self.assertNotIn('pair=', repr(curses.complexchar('z')))
r = repr(curses.complexchar('A', curses.A_BOLD))
self.assertIn('attr=', r)
self.assertNotIn('pair=', r)
self.assertEqual(eval(r, ns), curses.complexchar('A', curses.A_BOLD))
# Invalid arguments.
self.assertRaises(TypeError, curses.complexchar, 65)
self.assertRaises(TypeError, curses.complexchar, 'A', 'bold')
self.assertRaises(OverflowError, curses.complexchar, 'A', -1)
self.assertRaises(OverflowError, curses.complexchar, 'A', 1 << 64)
self.assertRaises(ValueError, curses.complexchar, 'A', 0, -1)
self.assertRaises(ValueError, curses.complexchar, 'ab')

@requires_curses_window_meth('in_wch')
def test_in_wch(self):
# in_wch() returns the styled wide cell as a complexchar -- something
# inch() (a packed chtype) cannot represent.
stdscr = self.stdscr
stdscr.addch(0, 0, curses.complexchar('A', curses.A_UNDERLINE))
cc = stdscr.in_wch(0, 0)
self.assertEqual(str(cc), 'A')
self.assertTrue(cc.attr & curses.A_UNDERLINE)
if self._encodable('\u00e9'): # precomposed, for a portable round-trip
stdscr.addch(3, 0, curses.complexchar('\u00e9'))
self.assertEqual(str(stdscr.in_wch(3, 0)), '\u00e9')
# in_wch() without coordinates reads at the cursor position.
stdscr.move(0, 0)
self.assertEqual(str(stdscr.in_wch()), 'A')

@requires_curses_window_meth('in_wch')
@requires_colors
def test_in_wch_color(self):
# Unlike the chtype methods (which pack the pair into the value via
# COLOR_PAIR), a complex character carries its color pair separately.
stdscr = self.stdscr
curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK)
stdscr.addch(0, 0, curses.complexchar('A', curses.A_BOLD, 1))
cc = stdscr.in_wch(0, 0)
self.assertEqual(str(cc), 'A')
self.assertTrue(cc.attr & curses.A_BOLD)
self.assertEqual(cc.pair, 1)
self.assertEqual(curses.complexchar('A', 0, 1).pair, 1)

@requires_curses_window_meth('getbkgrnd')
def test_getbkgrnd(self):
# getbkgrnd() returns the background as a complexchar (getbkgd() can
# only return a packed chtype).
stdscr = self.stdscr
stdscr.bkgdset(curses.complexchar(' ', curses.A_DIM))
stdscr.bkgd(curses.complexchar(' ', curses.A_BOLD))
cc = stdscr.getbkgrnd()
self.assertEqual(str(cc), ' ')
self.assertTrue(cc.attr & curses.A_BOLD)


def test_output_character(self):
stdscr = self.stdscr
Expand Down
Loading
Loading