Skip to content

Commit 25feef4

Browse files
gh-153395: Accept curses.complexchar in curses.ascii predicates and conversions
The curses.ascii predicates and the ctrl() and unctrl() functions now accept a curses.complexchar, classifying it by its single character. ctrl() now returns a non-ASCII argument unchanged instead of masking it to a control character. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent f932a74 commit 25feef4

4 files changed

Lines changed: 75 additions & 19 deletions

File tree

Doc/library/curses.ascii.rst

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -176,15 +176,18 @@ C library:
176176

177177
Checks for a non-ASCII character (ordinal values 0x80 and above).
178178

179-
These functions accept either integers or single-character strings; when the argument is a
180-
string, it is first converted using the built-in function :func:`ord`.
179+
These functions accept an integer, a single-character string, or a
180+
:class:`curses.complexchar`.
181+
A string is converted using the built-in function :func:`ord`, and a
182+
complexchar by the code of its single character; a complexchar that holds
183+
combining characters is not a single character and matches no class.
181184

182185
Note that all these functions check ordinal bit values derived from the
183186
character of the string you pass in; they do not actually know anything about
184187
the host machine's character encoding.
185188

186-
The following two functions take either a single-character string or integer
187-
byte value; they return a value of the same type.
189+
The following three functions take either a single-character string or an
190+
integer byte value; they return a value of the same type.
188191

189192

190193
.. function:: ascii(c)
@@ -194,16 +197,22 @@ byte value; they return a value of the same type.
194197

195198
.. function:: ctrl(c)
196199

197-
Return the control character corresponding to the given character (the character
198-
bit value is bitwise-anded with 0x1f).
200+
Return the control character corresponding to the given ASCII character (the
201+
character bit value is bitwise-anded with 0x1f). A non-ASCII character has no
202+
control character and is returned unchanged.
203+
204+
.. versionchanged:: next
205+
A non-ASCII argument is now returned unchanged instead of masked to a
206+
control character.
199207

200208

201209
.. function:: alt(c)
202210

203211
Return the 8-bit character corresponding to the given ASCII character (the
204212
character bit value is bitwise-ored with 0x80).
205213

206-
The following function takes either a single-character string or integer value;
214+
The following function takes a single-character string, an integer value, or a
215+
:class:`curses.complexchar`;
207216
it returns a string.
208217

209218

Lib/curses/ascii.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
"""Constants and membership tests for ASCII characters"""
22

3+
# A character-cell type, present on wide and narrow builds.
4+
from _curses import complexchar as _complexchar
5+
36
NUL = 0x00 # ^@
47
SOH = 0x01 # ^A
58
STX = 0x02 # ^B
@@ -48,8 +51,12 @@
4851
def _ctoi(c):
4952
if isinstance(c, str):
5053
return ord(c)
51-
else:
52-
return c
54+
if isinstance(c, _complexchar):
55+
# A character cell: its single character, or -1 (matches no class)
56+
# for a cell with combining characters.
57+
s = str(c)
58+
return ord(s) if len(s) == 1 else -1
59+
return c
5360

5461
def isalnum(c): return isalpha(c) or isdigit(c)
5562
def isalpha(c): return isupper(c) or islower(c)
@@ -69,22 +76,23 @@ def isctrl(c): return 0 <= _ctoi(c) < 32
6976
def ismeta(c): return _ctoi(c) > 127
7077

7178
def ascii(c):
72-
if isinstance(c, str):
73-
return chr(_ctoi(c) & 0x7f)
74-
else:
79+
if isinstance(c, int):
7580
return _ctoi(c) & 0x7f
81+
else:
82+
return chr(_ctoi(c) & 0x7f)
7683

7784
def ctrl(c):
78-
if isinstance(c, str):
79-
return chr(_ctoi(c) & 0x1f)
80-
else:
81-
return _ctoi(c) & 0x1f
85+
code = _ctoi(c)
86+
if not 0 <= code < 128:
87+
# No control character outside ASCII: return c unchanged.
88+
return c if isinstance(c, int) else str(c)
89+
return code & 0x1f if isinstance(c, int) else chr(code & 0x1f)
8290

8391
def alt(c):
84-
if isinstance(c, str):
85-
return chr(_ctoi(c) | 0x80)
86-
else:
92+
if isinstance(c, int):
8793
return _ctoi(c) | 0x80
94+
else:
95+
return chr(_ctoi(c) | 0x80)
8896

8997
def unctrl(c):
9098
bits = _ctoi(c)

Lib/test/test_curses.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2740,6 +2740,9 @@ def test_ctrl(self):
27402740
self.assertEqual(ctrl('\n'), '\n')
27412741
self.assertEqual(ctrl('@'), '\0')
27422742
self.assertEqual(ctrl(ord('J')), ord('\n'))
2743+
# A non-ASCII argument is returned unchanged (no control character).
2744+
self.assertEqual(ctrl('\xe9'), '\xe9')
2745+
self.assertEqual(ctrl(0xe9), 0xe9)
27432746

27442747
def test_alt(self):
27452748
alt = curses.ascii.alt
@@ -2764,6 +2767,38 @@ def test_unctrl(self):
27642767
self.assertEqual(unctrl(ord('\x8a')), '!^J')
27652768
self.assertEqual(unctrl(ord('\xc1')), '!A')
27662769

2770+
@unittest.skipUnless(hasattr(curses, 'complexchar'),
2771+
'requires the curses.complexchar type')
2772+
def test_complexchar(self):
2773+
# The predicates, ctrl() and unctrl() accept a complexchar too, using
2774+
# its single character. A narrow build just forms fewer cells.
2775+
cc = curses.complexchar
2776+
def storable(s):
2777+
try:
2778+
cc(s)
2779+
except ValueError:
2780+
return False
2781+
return True
2782+
2783+
self.assertTrue(curses.ascii.isupper(cc('A')))
2784+
self.assertTrue(curses.ascii.isalpha(cc('A', curses.A_BOLD)))
2785+
self.assertFalse(curses.ascii.isdigit(cc('A')))
2786+
self.assertTrue(curses.ascii.isdigit(cc('7')))
2787+
self.assertTrue(curses.ascii.iscntrl(cc('\n')))
2788+
self.assertEqual(curses.ascii.ctrl(cc('J')), '\n')
2789+
self.assertEqual(curses.ascii.unctrl(cc('\n')), '^J')
2790+
self.assertEqual(curses.ascii.unctrl(cc('A')), 'A')
2791+
# A non-ASCII character: classified by code point, no control character.
2792+
if storable('\xe9'):
2793+
self.assertFalse(curses.ascii.isascii(cc('\xe9')))
2794+
self.assertTrue(curses.ascii.ismeta(cc('\xe9')))
2795+
self.assertEqual(curses.ascii.ctrl(cc('\xe9')), '\xe9')
2796+
# A cell with combining marks is not a single character, so no
2797+
# predicate matches it (needs a wide build to store).
2798+
if storable('e\u0301'):
2799+
self.assertFalse(curses.ascii.isalpha(cc('e\u0301')))
2800+
self.assertFalse(curses.ascii.isascii(cc('e\u0301')))
2801+
27672802

27682803
def lorem_ipsum(win):
27692804
text = [
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:mod:`curses.ascii` predicates and the :func:`~curses.ascii.ctrl` and
2+
:func:`~curses.ascii.unctrl` functions now accept a :class:`curses.complexchar`.
3+
:func:`~curses.ascii.ctrl` now returns a non-ASCII argument unchanged instead
4+
of masking it to a control character.

0 commit comments

Comments
 (0)