Skip to content

Commit 95bfaff

Browse files
gh-152233: Make the curses cell API work without ncursesw (ПР-152466)
Back complexchar, complexstr and the cell read methods (in_wch, in_wchstr, in_wstr, getbkgrnd) with a chtype instead of a cchar_t when ncursesw is absent, so the same code works on both builds. A narrow build is limited to one character per cell, encodable as a single byte in the window's encoding (8-bit locales), with the color pair limited to the color_pair() range. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c253f0c commit 95bfaff

8 files changed

Lines changed: 437 additions & 196 deletions

File tree

Doc/library/curses.rst

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1333,9 +1333,6 @@ Window objects
13331333
and the color pair is not limited to the value that fits in a
13341334
:func:`color_pair`.
13351335

1336-
This method is only available if Python was built against a wide-character
1337-
version of the underlying curses library.
1338-
13391336
.. versionadded:: next
13401337

13411338

@@ -1483,9 +1480,6 @@ Window objects
14831480
followed by combining characters) together with its attributes and color
14841481
pair, none of which :meth:`inch` can represent.
14851482

1486-
This method is only available if Python was built against a wide-character
1487-
version of the underlying curses library.
1488-
14891483
.. versionadded:: next
14901484

14911485

@@ -1585,9 +1579,6 @@ Window objects
15851579
The result can be written back unchanged with :meth:`addstr` (a read and a
15861580
re-write is a round-trip that preserves every cell's rendition).
15871581

1588-
This method is only available if Python was built against a wide-character
1589-
version of the underlying curses library.
1590-
15911582
.. versionadded:: next
15921583

15931584

@@ -2004,7 +1995,7 @@ Complex character objects
20041995
.. class:: complexchar(text, /, attr=0, pair=0)
20051996

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

20102001
*text* is the cell's text, *attr* a combination of the
@@ -2025,8 +2016,10 @@ Complex character objects
20252016
:func:`str` returns the cell's text; two complex characters are equal when
20262017
their text, attributes and color pair all match.
20272018

2028-
This type is only available if Python was built against a wide-character
2029-
version of the underlying curses library.
2019+
The same code works on both wide- and narrow-character builds. On a narrow
2020+
build a cell holds a single character (no combining marks) that must encode to
2021+
one byte in the window's encoding (8-bit locales only), and *pair* is limited
2022+
to the value that fits in a :func:`color_pair`.
20302023

20312024
.. attribute:: attr
20322025

@@ -2042,7 +2035,7 @@ Complex character objects
20422035
.. class:: complexstr(cells[, attr[, pair]])
20432036

20442037
A *complex character string* (or *complexstr*) is an immutable sequence of
2045-
styled wide-character cells -- the string counterpart of
2038+
styled character cells -- the string counterpart of
20462039
:class:`complexchar` (as :class:`str` is to a single character).
20472040

20482041
If *cells* is a string, it is split into character cells (each a spacing
@@ -2070,8 +2063,8 @@ Complex character objects
20702063
:class:`complexchar` (or strings); a :class:`!complexstr` is the immutable
20712064
form returned by a read.
20722065

2073-
This type is only available if Python was built against a wide-character
2074-
version of the underlying curses library.
2066+
Like :class:`complexchar`, this type works on both wide- and narrow-character
2067+
builds, with the same per-cell limitations on a narrow build.
20752068

20762069
.. versionadded:: next
20772070

Doc/whatsnew/3.16.rst

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,12 @@ curses
111111
and the module functions :func:`curses.erasewchar`, :func:`curses.killwchar`
112112
and :func:`curses.wunctrl`, the wide-character counterparts of
113113
:func:`curses.erasechar`, :func:`curses.killchar` and :func:`curses.unctrl`.
114-
These features are only available when built against the wide-character
115-
ncursesw library.
114+
On a narrow (non-ncursesw) build the character cell holds a single character
115+
without combining marks, representable as one byte in the window's encoding,
116+
and :meth:`~curses.window.in_wstr` returns its decoded text;
117+
:meth:`~curses.window.get_wstr` and the :func:`curses.erasewchar`,
118+
:func:`curses.killwchar` and :func:`curses.wunctrl` functions require the
119+
wide-character ncursesw library.
116120
(Contributed by Serhiy Storchaka in :gh:`151757`.)
117121

118122
* Add :func:`curses.nofilter`, which undoes the effect of :func:`curses.filter`.
@@ -139,21 +143,26 @@ curses
139143
(Contributed by Serhiy Storchaka in :gh:`152219`.)
140144

141145
* Add the :class:`curses.complexchar` type, representing a styled
142-
wide-character cell (its text, attributes and color pair), and the window
146+
character cell (its text, attributes and color pair), and the window
143147
methods :meth:`~curses.window.in_wch` and :meth:`~curses.window.getbkgrnd`
144-
that return one --- the wide-character counterparts of
148+
that return one --- the counterparts of
145149
:meth:`~curses.window.inch` and :meth:`~curses.window.getbkgd`. The
146150
character-cell methods, such as :meth:`~curses.window.addch` and
147151
:meth:`~curses.window.border`, now also accept a
148-
:class:`~curses.complexchar`.
152+
:class:`~curses.complexchar`. These work whether or not Python was built
153+
against a wide-character-aware curses library; on a narrow build a cell holds a
154+
single character representable as one byte in the window's encoding (so only
155+
8-bit locales are supported).
149156
(Contributed by Serhiy Storchaka in :gh:`152233`.)
150157

151158
* Add the :class:`curses.complexstr` type, an immutable run of styled cells
152159
(the string counterpart of :class:`~curses.complexchar`), and the window
153160
method :meth:`~curses.window.in_wchstr` that returns one. The string-cell
154161
methods :meth:`~curses.window.addstr`, :meth:`~curses.window.addnstr`,
155162
:meth:`~curses.window.insstr` and :meth:`~curses.window.insnstr` now also
156-
accept a :class:`~curses.complexstr`.
163+
accept a :class:`~curses.complexstr`. Like :class:`~curses.complexchar`, it
164+
works whether or not Python was built against a wide-character-aware curses
165+
library.
157166
(Contributed by Serhiy Storchaka in :gh:`152233`.)
158167

159168
* Add the :mod:`curses` window method :meth:`~curses.window.dupwin`, which

Lib/test/test_curses.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,8 @@ def test_refresh_control(self):
313313
# 'é' common to the Latin encodings
314314
# '¤'/'€'/'є' byte 0xA4 in ISO-8859-1 / ISO-8859-15 / KOI8-U
315315
# Precomposed characters are used so a round-trip does not depend on the form.
316+
# On a narrow (non-wide) build a cell holds one byte, so cases that need a
317+
# combining sequence or a multibyte character are guarded with _storable().
316318

317319
def _encodable(self, s):
318320
# Wide characters are only supported in a locale that can encode them.
@@ -322,6 +324,18 @@ def _encodable(self, s):
322324
return False
323325
return True
324326

327+
def _storable(self, s):
328+
# Text the current build can place in character cells. A wide build
329+
# stores any locale-encodable text (combining sequences and multibyte
330+
# characters included). A narrow build has no wide-character cells, so
331+
# each character must occupy a single cell -- that is, encode to exactly
332+
# one byte.
333+
if not self._encodable(s):
334+
return False
335+
if hasattr(self.stdscr, 'get_wch'): # wide build
336+
return True
337+
return len(s.encode(self.stdscr.encoding)) == len(s)
338+
325339
def _read_char(self, y, x):
326340
# The character written to a cell, read back for output checks. inch()
327341
# is unusable here: on a wide build it returns the low 8 bits of the
@@ -435,7 +449,7 @@ def test_in_wstr(self):
435449
'na\u00efve \u00a4', # ISO-8859-1
436450
'soup\u00e7on \u20ac', # ISO-8859-15
437451
'\u0434\u044f\u043a']: # KOI8-U
438-
if self._encodable(s):
452+
if self._storable(s):
439453
with self.subTest(s=s):
440454
stdscr.addstr(0, 0, s)
441455
self.assertEqual(stdscr.in_wstr(0, 0, len(s)), s)
@@ -450,7 +464,7 @@ def test_complexchar(self):
450464
self.assertTrue(cc.attr & curses.A_BOLD)
451465
self.assertEqual(cc.pair, 0)
452466
# A spacing character optionally followed by combining characters.
453-
if self._encodable('e\u0301'):
467+
if self._storable('e\u0301'):
454468
self.assertEqual(str(curses.complexchar('e\u0301')), 'e\u0301')
455469
# Defaults: no attributes, color pair 0.
456470
cc = curses.complexchar('z')
@@ -496,7 +510,7 @@ def test_in_wch(self):
496510
self.assertTrue(cc.attr & curses.A_UNDERLINE)
497511
# A character round-trips through the cell. See _encodable for the set.
498512
for ch in ('A', '\u00e9', '\u00a4', '\u20ac', '\u0454'):
499-
if self._encodable(ch):
513+
if self._storable(ch):
500514
with self.subTest(ch=ch):
501515
stdscr.addch(3, 0, curses.complexchar(ch))
502516
self.assertEqual(str(stdscr.in_wch(3, 0)), ch)
@@ -530,7 +544,7 @@ def test_getbkgrnd(self):
530544
self.assertTrue(cc.attr & curses.A_BOLD)
531545
# A non-ASCII background round-trips as a complexchar. See _encodable.
532546
for ch in ('é', '¤', '€', 'є'):
533-
if self._encodable(ch):
547+
if self._storable(ch):
534548
with self.subTest(ch=ch):
535549
stdscr.bkgd(curses.complexchar(ch))
536550
self.assertEqual(str(stdscr.getbkgrnd()), ch)
@@ -569,7 +583,7 @@ def test_complexstr(self):
569583
self.assertNotEqual(s, curses.complexstr([cc('A'), 'b', cc('c')]))
570584
self.assertNotEqual(s, curses.complexstr([cc('A', B), 'b']))
571585
# A spacing character optionally followed by combining characters.
572-
if self._encodable('é'):
586+
if self._storable('é'):
573587
self.assertEqual(str(curses.complexstr(['é', 'x'])),
574588
'éx')
575589
# cells is positional-only.
@@ -586,7 +600,9 @@ def test_complexstr(self):
586600
self.assertEqual(str(curses.complexstr('abc')), 'abc')
587601
self.assertEqual(len(curses.complexstr('')), 0)
588602
base = 'é' # 'e' + combining acute: two code points, one cell
589-
if self._encodable(base):
603+
# Combining sequences need wide-character cells (a narrow build stores
604+
# one byte per cell).
605+
if hasattr(curses.window, 'get_wch') and self._encodable(base):
590606
self.assertEqual(len(curses.complexstr(base)), 1)
591607
self.assertEqual(curses.complexstr(base)[0], cc(base))
592608
self.assertEqual(len(curses.complexstr('a' + base + 'b')), 3)
@@ -734,7 +750,7 @@ def test_output_character(self):
734750
# str is stored as a wide-character cell on a wide build, so every
735751
# encodable character round-trips, insch() included. A multibyte
736752
# character does not fit a cell on a narrow build and is skipped.
737-
wide = hasattr(stdscr, 'in_wch')
753+
wide = hasattr(stdscr, 'get_wch')
738754
for c in ('é', '¤', '€', 'є'):
739755
if not self._encodable(c):
740756
continue

Misc/NEWS.d/next/Library/2026-06-20-02-14-55.gh-issue-151757.TP9A2x.rst

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,9 @@ cell -- a spacing character optionally followed by combining characters -- in
33
addition to a single integer or byte character. Add the wide-character read
44
methods :meth:`curses.window.get_wstr` and :meth:`curses.window.in_wstr`, and
55
the functions :func:`curses.erasewchar`, :func:`curses.killwchar` and
6-
:func:`curses.wunctrl`. These features are only available when built against
7-
the wide-character ncursesw library.
6+
:func:`curses.wunctrl`. On a narrow (non-ncursesw) build the character cell
7+
holds a single character without combining marks, representable as one byte in
8+
the window's encoding, and :meth:`curses.window.in_wstr` returns its decoded
9+
text; :meth:`curses.window.get_wstr` and the :func:`curses.erasewchar`,
10+
:func:`curses.killwchar` and :func:`curses.wunctrl` functions require the
11+
wide-character ncursesw library.
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
Add the :class:`curses.complexchar` type, representing a styled wide-character
1+
Add the :class:`curses.complexchar` type, representing a styled character
22
cell (text, attributes and color pair), and the :mod:`curses` window methods
33
:meth:`~curses.window.in_wch` and :meth:`~curses.window.getbkgrnd` that return
44
one. The character-cell methods (:meth:`~curses.window.addch`,
55
:meth:`~curses.window.bkgd`, :meth:`~curses.window.border`,
66
:meth:`~curses.window.hline` and others) now also accept a
7-
:class:`~curses.complexchar`.
7+
:class:`~curses.complexchar`. This works whether or not Python was built
8+
against a wide-character-aware curses library; on a narrow build a cell holds a
9+
single character representable as one byte in the window's encoding.
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
Add the :class:`curses.complexstr` type, an immutable string of styled
2-
wide-character cells (the counterpart of :class:`curses.complexchar`), and the
2+
character cells (the counterpart of :class:`curses.complexchar`), and the
33
:mod:`curses` window method :meth:`~curses.window.in_wchstr` that returns one.
44
The string-cell methods :meth:`~curses.window.addstr`,
55
:meth:`~curses.window.addnstr`, :meth:`~curses.window.insstr` and
66
:meth:`~curses.window.insnstr` now also accept a :class:`~curses.complexstr`.
7+
Like :class:`curses.complexchar`, it works whether or not Python was built
8+
against a wide-character-aware curses library.

0 commit comments

Comments
 (0)