Skip to content

Commit 2af5076

Browse files
gh-152233: Add curses complexchar type and wide-character cell reads
Add the immutable curses.complexchar type: a styled wide-character cell (a spacing character optionally followed by combining characters, plus attributes and a color pair stored separately from the packed chtype). Add the window methods in_wch() and getbkgrnd(), the wide-character counterparts of inch() and getbkgd(), which return a complexchar. The character-cell methods (addch, insch, echochar, bkgd, bkgdset, border, box, hline, vline) now also accept a complexchar; combining one with an explicit attr argument raises TypeError. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 285d96d commit 2af5076

6 files changed

Lines changed: 954 additions & 150 deletions

File tree

Doc/library/curses.rst

Lines changed: 89 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -941,7 +941,8 @@ Window objects
941941

942942
.. versionchanged:: next
943943
A character may now be given as a string of a base character followed
944-
by combining characters, instead of only a single character.
944+
by combining characters, instead of only a single character, or as a
945+
:class:`complexchar` cell.
945946

946947

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

10531054
.. versionchanged:: next
1054-
Wide and combining characters are now accepted.
1055+
Wide and combining characters, and :class:`complexchar` cells, are now
1056+
accepted.
10551057

10561058

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

10661068
.. versionchanged:: next
1067-
Wide and combining characters are now accepted.
1069+
Wide and combining characters, and :class:`complexchar` cells, are now
1070+
accepted.
10681071

10691072

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

11021105
.. versionchanged:: next
1103-
Wide and combining characters are now accepted. A single call cannot mix
1106+
Wide and combining characters, and :class:`complexchar` cells, are now
1107+
accepted. A single call cannot mix
11041108
them with integer or byte characters.
11051109

11061110

@@ -1110,7 +1114,8 @@ Window objects
11101114
*bs* are *horch*. The default corner characters are always used by this function.
11111115

11121116
.. versionchanged:: next
1113-
Wide and combining characters are now accepted. A single call cannot mix
1117+
Wide and combining characters, and :class:`complexchar` cells, are now
1118+
accepted. A single call cannot mix
11141119
them with integer or byte characters.
11151120

11161121

@@ -1182,7 +1187,8 @@ Window objects
11821187
on the window.
11831188

11841189
.. versionchanged:: next
1185-
Wide and combining characters are now accepted.
1190+
Wide and combining characters, and :class:`complexchar` cells, are now
1191+
accepted.
11861192

11871193

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

12231229

1230+
.. method:: window.getbkgrnd()
1231+
1232+
Return the given window's current background as a :class:`complexchar`.
1233+
This is the wide-character variant of :meth:`getbkgd`: the returned object
1234+
carries the background character together with its attributes and color pair,
1235+
and the color pair is not limited to the value that fits in a
1236+
:func:`color_pair`.
1237+
1238+
This method is only available if Python was built against a wide-character
1239+
version of the underlying curses library.
1240+
1241+
.. versionadded:: next
1242+
1243+
12241244
.. method:: window.getch([y, x])
12251245

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

13271347
.. versionchanged:: next
1328-
Wide and combining characters are now accepted.
1348+
Wide and combining characters, and :class:`complexchar` cells, are now
1349+
accepted.
13291350

13301351

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

13581379

1380+
.. method:: window.in_wch([y, x])
1381+
1382+
Return the complex character at the given position in the window as a
1383+
:class:`complexchar`. This is the wide-character variant of :meth:`inch`:
1384+
the returned object carries the cell's text (a spacing character optionally
1385+
followed by combining characters) together with its attributes and color
1386+
pair, none of which :meth:`inch` can represent.
1387+
1388+
This method is only available if Python was built against a wide-character
1389+
version of the underlying curses library.
1390+
1391+
.. versionadded:: next
1392+
1393+
13591394
.. method:: window.insch(ch[, attr])
13601395
window.insch(y, x, ch[, attr])
13611396

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

13671402
.. versionchanged:: next
1368-
Wide and combining characters are now accepted.
1403+
Wide and combining characters, and :class:`complexchar` cells, are now
1404+
accepted.
13691405

13701406

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

17781814
.. versionchanged:: next
1779-
Wide and combining characters are now accepted.
1815+
Wide and combining characters, and :class:`complexchar` cells, are now
1816+
accepted.
17801817

17811818

17821819
.. _curses-screen-objects:
@@ -1833,6 +1870,49 @@ Screen objects
18331870
.. versionadded:: next
18341871

18351872

1873+
.. _curses-complexchar-objects:
1874+
1875+
Complex character objects
1876+
-------------------------
1877+
1878+
.. class:: complexchar(text, /, attr=0, pair=0)
1879+
1880+
A *complex character* (or *complexchar*) is an immutable styled
1881+
wide-character cell: a spacing character optionally followed by combining
1882+
characters, together with a set of attributes and a color pair.
1883+
1884+
*text* is the cell's text, *attr* a combination of the
1885+
:ref:`WA_* attributes <curses-wa-constants>` (equivalent to the matching
1886+
``A_*`` constants), and *pair* a color pair number. Unlike the packed
1887+
:class:`chtype <int>` used by :meth:`~window.inch` and the ``A_*`` methods,
1888+
the color pair is stored separately and is not limited to the value that
1889+
fits in a :func:`color_pair`.
1890+
1891+
Complex characters are returned by :meth:`window.in_wch` and
1892+
:meth:`window.getbkgrnd`, and are accepted (along with an integer, a byte
1893+
or a string) by the character-cell methods such as :meth:`window.addch`,
1894+
:meth:`window.insch`, :meth:`window.bkgd`, :meth:`window.border`,
1895+
:meth:`window.hline` and :meth:`window.vline`. A complex character already
1896+
carries its own rendition, so it cannot be combined with an explicit *attr*
1897+
argument.
1898+
1899+
:func:`str` returns the cell's text; two complex characters are equal when
1900+
their text, attributes and color pair all match.
1901+
1902+
This type is only available if Python was built against a wide-character
1903+
version of the underlying curses library.
1904+
1905+
.. attribute:: attr
1906+
1907+
The attributes of the character cell (read-only).
1908+
1909+
.. attribute:: pair
1910+
1911+
The color pair number of the character cell (read-only).
1912+
1913+
.. versionadded:: next
1914+
1915+
18361916
Constants
18371917
---------
18381918

Doc/whatsnew/3.16.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,16 @@ curses
138138
attribute value, and the corresponding ``WA_*`` attribute constants.
139139
(Contributed by Serhiy Storchaka in :gh:`152219`.)
140140

141+
* Add the :class:`curses.complexchar` type, representing a styled
142+
wide-character cell (its text, attributes and color pair), and the window
143+
methods :meth:`~curses.window.in_wch` and :meth:`~curses.window.getbkgrnd`
144+
that return one --- the wide-character counterparts of
145+
:meth:`~curses.window.inch` and :meth:`~curses.window.getbkgd`. The
146+
character-cell methods, such as :meth:`~curses.window.addch` and
147+
:meth:`~curses.window.border`, now also accept a
148+
:class:`~curses.complexchar`.
149+
(Contributed by Serhiy Storchaka in :gh:`152233`.)
150+
141151
gzip
142152
----
143153

Lib/test/test_curses.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,36 @@ def test_wide_characters(self):
345345
# border() and box() cannot mix integer and wide-string characters.
346346
self.assertRaises(TypeError, stdscr.box, vline, ord('-'))
347347

348+
@requires_curses_func('complexchar')
349+
def test_complexchar_in_cell_methods(self):
350+
# Every single-character-cell method also accepts a complexchar, whose
351+
# attributes and color pair come from the cell itself.
352+
stdscr = self.stdscr
353+
cc = curses.complexchar('A', curses.A_BOLD)
354+
v = curses.complexchar('|')
355+
h = curses.complexchar('-')
356+
stdscr.move(0, 0)
357+
stdscr.addch(0, 0, cc)
358+
self.assertEqual(str(stdscr.in_wch(0, 0)), 'A')
359+
self.assertTrue(stdscr.in_wch(0, 0).attr & curses.A_BOLD)
360+
stdscr.insch(1, 0, cc)
361+
stdscr.echochar(cc)
362+
stdscr.bkgdset(cc)
363+
stdscr.bkgd(cc)
364+
stdscr.hline(2, 0, h, 3)
365+
stdscr.vline(3, 0, v, 3)
366+
stdscr.border(v, v, h, h)
367+
stdscr.box(v, h)
368+
# A complexchar already carries its rendition, so combining it with an
369+
# explicit attr argument is rejected.
370+
self.assertRaises(TypeError, stdscr.addch, cc, curses.A_BOLD)
371+
self.assertRaises(TypeError, stdscr.addch, 0, 0, cc, curses.A_BOLD)
372+
self.assertRaises(TypeError, stdscr.insch, cc, curses.A_BOLD)
373+
self.assertRaises(TypeError, stdscr.echochar, cc, curses.A_BOLD)
374+
self.assertRaises(TypeError, stdscr.bkgd, cc, curses.A_BOLD)
375+
self.assertRaises(TypeError, stdscr.bkgdset, cc, curses.A_BOLD)
376+
self.assertRaises(TypeError, stdscr.hline, h, 3, curses.A_BOLD)
377+
self.assertRaises(TypeError, stdscr.vline, v, 3, curses.A_BOLD)
348378

349379
@requires_curses_window_meth('in_wstr')
350380
def test_in_wstr(self):
@@ -355,6 +385,90 @@ def test_in_wstr(self):
355385
self.assertEqual(stdscr.in_wstr(0, 0, len(s)), s)
356386
self.assertIsInstance(stdscr.instr(0, 0, len(s)), bytes)
357387

388+
@requires_curses_func('complexchar')
389+
def test_complexchar(self):
390+
# A complexchar is a styled wide-character cell: str() is its text,
391+
# and the attr and pair attributes are its rendition.
392+
cc = curses.complexchar('A', curses.A_BOLD)
393+
self.assertEqual(str(cc), 'A')
394+
self.assertTrue(cc.attr & curses.A_BOLD)
395+
self.assertEqual(cc.pair, 0)
396+
# A spacing character optionally followed by combining characters.
397+
if self._encodable('e\u0301'):
398+
self.assertEqual(str(curses.complexchar('e\u0301')), 'e\u0301')
399+
# Defaults: no attributes, color pair 0.
400+
cc = curses.complexchar('z')
401+
self.assertEqual(str(cc), 'z')
402+
self.assertEqual(cc.attr, 0)
403+
self.assertEqual(cc.pair, 0)
404+
# Immutable rendition.
405+
self.assertRaises(AttributeError, setattr, cc, 'attr', 1)
406+
self.assertRaises(AttributeError, setattr, cc, 'pair', 1)
407+
# Equality and hashing compare text, attributes and color pair.
408+
self.assertEqual(curses.complexchar('A', curses.A_BOLD),
409+
curses.complexchar('A', curses.A_BOLD))
410+
self.assertEqual(hash(curses.complexchar('A', curses.A_BOLD)),
411+
hash(curses.complexchar('A', curses.A_BOLD)))
412+
self.assertNotEqual(curses.complexchar('A'),
413+
curses.complexchar('A', curses.A_BOLD))
414+
self.assertNotEqual(curses.complexchar('A'), curses.complexchar('B'))
415+
# repr() shows only a non-default attr/pair, and is a constructor call.
416+
ns = {'_curses': sys.modules[type(cc).__module__]}
417+
self.assertNotIn('attr=', repr(curses.complexchar('z')))
418+
self.assertNotIn('pair=', repr(curses.complexchar('z')))
419+
r = repr(curses.complexchar('A', curses.A_BOLD))
420+
self.assertIn('attr=', r)
421+
self.assertNotIn('pair=', r)
422+
self.assertEqual(eval(r, ns), curses.complexchar('A', curses.A_BOLD))
423+
# Invalid arguments.
424+
self.assertRaises(TypeError, curses.complexchar, 65)
425+
self.assertRaises(TypeError, curses.complexchar, 'A', 'bold')
426+
self.assertRaises(OverflowError, curses.complexchar, 'A', -1)
427+
self.assertRaises(OverflowError, curses.complexchar, 'A', 1 << 64)
428+
self.assertRaises(ValueError, curses.complexchar, 'A', 0, -1)
429+
self.assertRaises(ValueError, curses.complexchar, 'ab')
430+
431+
@requires_curses_window_meth('in_wch')
432+
def test_in_wch(self):
433+
# in_wch() returns the styled wide cell as a complexchar -- something
434+
# inch() (a packed chtype) cannot represent.
435+
stdscr = self.stdscr
436+
stdscr.addch(0, 0, curses.complexchar('A', curses.A_UNDERLINE))
437+
cc = stdscr.in_wch(0, 0)
438+
self.assertEqual(str(cc), 'A')
439+
self.assertTrue(cc.attr & curses.A_UNDERLINE)
440+
if self._encodable('\u00e9'): # precomposed, for a portable round-trip
441+
stdscr.addch(3, 0, curses.complexchar('\u00e9'))
442+
self.assertEqual(str(stdscr.in_wch(3, 0)), '\u00e9')
443+
# in_wch() without coordinates reads at the cursor position.
444+
stdscr.move(0, 0)
445+
self.assertEqual(str(stdscr.in_wch()), 'A')
446+
447+
@requires_curses_window_meth('in_wch')
448+
@requires_colors
449+
def test_in_wch_color(self):
450+
# Unlike the chtype methods (which pack the pair into the value via
451+
# COLOR_PAIR), a complex character carries its color pair separately.
452+
stdscr = self.stdscr
453+
curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK)
454+
stdscr.addch(0, 0, curses.complexchar('A', curses.A_BOLD, 1))
455+
cc = stdscr.in_wch(0, 0)
456+
self.assertEqual(str(cc), 'A')
457+
self.assertTrue(cc.attr & curses.A_BOLD)
458+
self.assertEqual(cc.pair, 1)
459+
self.assertEqual(curses.complexchar('A', 0, 1).pair, 1)
460+
461+
@requires_curses_window_meth('getbkgrnd')
462+
def test_getbkgrnd(self):
463+
# getbkgrnd() returns the background as a complexchar (getbkgd() can
464+
# only return a packed chtype).
465+
stdscr = self.stdscr
466+
stdscr.bkgdset(curses.complexchar(' ', curses.A_DIM))
467+
stdscr.bkgd(curses.complexchar(' ', curses.A_BOLD))
468+
cc = stdscr.getbkgrnd()
469+
self.assertEqual(str(cc), ' ')
470+
self.assertTrue(cc.attr & curses.A_BOLD)
471+
358472

359473
def test_output_character(self):
360474
stdscr = self.stdscr
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Add the :class:`curses.complexchar` type, representing a styled wide-character
2+
cell (text, attributes and color pair), and the :mod:`curses` window methods
3+
:meth:`~curses.window.in_wch` and :meth:`~curses.window.getbkgrnd` that return
4+
one. The character-cell methods (:meth:`~curses.window.addch`,
5+
:meth:`~curses.window.bkgd`, :meth:`~curses.window.border`,
6+
:meth:`~curses.window.hline` and others) now also accept a
7+
:class:`~curses.complexchar`.

0 commit comments

Comments
 (0)