From 1eb09e8493f8745b53ec72fec35696495b7f5dfb Mon Sep 17 00:00:00 2001 From: Alessandro Gatti Date: Mon, 7 Sep 2026 14:15:20 +0200 Subject: [PATCH 1/3] curses.ascii: Fix ranges for isblank, iscntrl, and ispunct. This commit updates `curses.ascii.isblank`, `curses.ascii.iscntrl`, and `curses.ascii.ispunct` to use the same ranges as CPython. For one reason or another, the three functions in question would use either incorrect values or use partial ranges for validation. The new test file that's part of this commit compares the output of `curses.ascii.is*` functions with the output of the same function from CPython, covering the full 0..255 values range. The ranges fix increases the compiled size of this module by 73 bytes. Signed-off-by: Alessandro Gatti --- python-stdlib/curses.ascii/curses/ascii.py | 7 +- .../curses.ascii/test_curses_ascii.py | 122 ++++++++++++++++++ tools/ci.sh | 1 + 3 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 python-stdlib/curses.ascii/test_curses_ascii.py diff --git a/python-stdlib/curses.ascii/curses/ascii.py b/python-stdlib/curses.ascii/curses/ascii.py index 34cb79853..024247266 100644 --- a/python-stdlib/curses.ascii/curses/ascii.py +++ b/python-stdlib/curses.ascii/curses/ascii.py @@ -94,11 +94,11 @@ def isascii(c): def isblank(c): - return _ctoi(c) in (8, 32) + return _ctoi(c) in (9, 32) def iscntrl(c): - return _ctoi(c) <= 31 + return _ctoi(c) <= 31 or _ctoi(c) == 127 def isdigit(c): @@ -118,7 +118,8 @@ def isprint(c): def ispunct(c): - return _ctoi(c) != 32 and not isalnum(c) + char = _ctoi(c) + return (47 >= char >= 33) or (64 >= char >= 58) or (96 >= char >= 91) or (126 >= char >= 123) def isspace(c): diff --git a/python-stdlib/curses.ascii/test_curses_ascii.py b/python-stdlib/curses.ascii/test_curses_ascii.py new file mode 100644 index 000000000..92b91d6ed --- /dev/null +++ b/python-stdlib/curses.ascii/test_curses_ascii.py @@ -0,0 +1,122 @@ +import curses.ascii + + +def _array_pack(function, char): + out = bytearray(256 // 8) + for octet in range(256 // 8): + byte = 0 + for bit in range(8): + arg = (octet * 8) + bit + if char: + value = function(chr(arg)) + else: + value = function(arg) + byte |= int(value) << bit + out[octet] = byte + return bytes(out) + + +isalnum = ( + b"\x00\x00\x00\x00\x00\x00\xff\x03\xfe\xff\xff\x07\xfe\xff\xff\x07" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +) +assert _array_pack(curses.ascii.isalnum, False) == isalnum +assert _array_pack(curses.ascii.isalnum, True) == isalnum + +isalpha = ( + b"\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xff\xff\x07\xfe\xff\xff\x07" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +) +assert _array_pack(curses.ascii.isalpha, False) == isalpha +assert _array_pack(curses.ascii.isalpha, True) == isalpha + +isascii = ( + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +) +assert _array_pack(curses.ascii.isascii, False) == isascii +assert _array_pack(curses.ascii.isascii, True) == isascii + +isblank = ( + b"\x00\x02\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +) +assert _array_pack(curses.ascii.isblank, False) == isblank +assert _array_pack(curses.ascii.isblank, True) == isblank + +iscntrl = ( + b"\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +) +assert _array_pack(curses.ascii.iscntrl, False) == iscntrl +assert _array_pack(curses.ascii.iscntrl, True) == iscntrl + +isdigit = ( + b"\x00\x00\x00\x00\x00\x00\xff\x03\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +) +assert _array_pack(curses.ascii.isdigit, False) == isdigit +assert _array_pack(curses.ascii.isdigit, True) == isdigit + +isgraph = ( + b"\x00\x00\x00\x00\xfe\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x7f" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +) +assert _array_pack(curses.ascii.isgraph, False) == isgraph +assert _array_pack(curses.ascii.isgraph, True) == isgraph + +islower = ( + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xff\xff\x07" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +) +assert _array_pack(curses.ascii.islower, False) == islower +assert _array_pack(curses.ascii.islower, True) == islower + +isprint = ( + b"\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x7f" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +) +assert _array_pack(curses.ascii.isprint, False) == isprint +assert _array_pack(curses.ascii.isprint, True) == isprint + +ispunct = ( + b"\x00\x00\x00\x00\xfe\xff\x00\xfc\x01\x00\x00\xf8\x01\x00\x00\x78" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +) +assert _array_pack(curses.ascii.ispunct, False) == ispunct +assert _array_pack(curses.ascii.ispunct, True) == ispunct + +isspace = ( + b"\x00\x3e\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +) +assert _array_pack(curses.ascii.isspace, False) == isspace +assert _array_pack(curses.ascii.isspace, True) == isspace + +isupper = ( + b"\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xff\xff\x07\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +) +assert _array_pack(curses.ascii.isupper, False) == isupper +assert _array_pack(curses.ascii.isupper, True) == isupper + +isxdigit = ( + b"\x00\x00\x00\x00\x00\x00\xff\x03\x7e\x00\x00\x00\x7e\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +) +assert _array_pack(curses.ascii.isxdigit, False) == isxdigit +assert _array_pack(curses.ascii.isxdigit, True) == isxdigit + +isctrl = ( + b"\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +) +assert _array_pack(curses.ascii.isctrl, False) == isctrl +assert _array_pack(curses.ascii.isctrl, True) == isctrl + +ismeta = ( + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +) +assert _array_pack(curses.ascii.ismeta, False) == ismeta +assert _array_pack(curses.ascii.ismeta, True) == ismeta diff --git a/tools/ci.sh b/tools/ci.sh index 2fc6fe90c..899a2bdea 100755 --- a/tools/ci.sh +++ b/tools/ci.sh @@ -64,6 +64,7 @@ function ci_package_tests_run { python-stdlib/base64/test_base64.py \ python-stdlib/binascii/test_binascii.py \ python-stdlib/collections-defaultdict/test_defaultdict.py \ + python-stdlib/curses.ascii/test_curses_ascii.py \ python-stdlib/functools/test_partial.py \ python-stdlib/functools/test_reduce.py \ python-stdlib/heapq/test_heapq.py \ From 4c0ca940faf9e3fc4a97875ee2c9aeba8df4aa69 Mon Sep 17 00:00:00 2001 From: Alessandro Gatti Date: Mon, 7 Sep 2026 14:43:41 +0200 Subject: [PATCH 2/3] curses.ascii: Reduce compiled module footprint. This commit reduces the module footprint by changing most functions to generate shorter bytecode sequences once compiled. The same few changes were applied to most functions in the module: type coercions are now done at most once per function body, range membership checks are converted into a series of comparisons (as a series of chained equality checks for disjointed sets or as two numerical comparisons with the range edges otherwise), and using the inline ternary comparison form for returning function values instead of a regular if-else construct. These changes reduce the compiled module size by 89 bytes. Signed-off-by: Alessandro Gatti --- python-stdlib/curses.ascii/curses/ascii.py | 65 +++++++++++----------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/python-stdlib/curses.ascii/curses/ascii.py b/python-stdlib/curses.ascii/curses/ascii.py index 024247266..788d03b8a 100644 --- a/python-stdlib/curses.ascii/curses/ascii.py +++ b/python-stdlib/curses.ascii/curses/ascii.py @@ -75,10 +75,7 @@ def _ctoi(c): - if type(c) == type(""): - return ord(c) - else: - return c + return ord(c) if type(c) is str else c def isalnum(c): @@ -94,46 +91,58 @@ def isascii(c): def isblank(c): - return _ctoi(c) in (9, 32) + ch = _ctoi(c) + return ch == 9 or ch == 32 def iscntrl(c): - return _ctoi(c) <= 31 or _ctoi(c) == 127 + ch = _ctoi(c) + return ch <= 31 or ch == 127 def isdigit(c): - return _ctoi(c) >= 48 and _ctoi(c) <= 57 + ch = _ctoi(c) + return ch >= 48 and ch <= 57 def isgraph(c): - return _ctoi(c) >= 33 and _ctoi(c) <= 126 + ch = _ctoi(c) + return ch >= 33 and ch <= 126 def islower(c): - return _ctoi(c) >= 97 and _ctoi(c) <= 122 + ch = _ctoi(c) + return ch >= 97 and ch <= 122 def isprint(c): - return _ctoi(c) >= 32 and _ctoi(c) <= 126 + ch = _ctoi(c) + return ch >= 32 and ch <= 126 def ispunct(c): - char = _ctoi(c) - return (47 >= char >= 33) or (64 >= char >= 58) or (96 >= char >= 91) or (126 >= char >= 123) + ch = _ctoi(c) + return ( + (ch >= 33 and ch <= 47) + or (ch >= 58 and ch <= 64) + or (ch >= 91 and ch <= 96) + or (ch >= 123 and ch <= 126) + ) def isspace(c): - return _ctoi(c) in (9, 10, 11, 12, 13, 32) + ch = _ctoi(c) + return isblank(c) or (ch >= 10 and ch <= 13) def isupper(c): - return _ctoi(c) >= 65 and _ctoi(c) <= 90 + ch = _ctoi(c) + return ch >= 65 and ch <= 90 def isxdigit(c): - return ( - isdigit(c) or (_ctoi(c) >= 65 and _ctoi(c) <= 70) or (_ctoi(c) >= 97 and _ctoi(c) <= 102) - ) + ch = _ctoi(c) + return isdigit(c) or (ch >= 65 and ch <= 70) or (ch >= 97 and ch <= 102) def isctrl(c): @@ -145,24 +154,18 @@ def ismeta(c): def ascii(c): - if type(c) == type(""): - return chr(_ctoi(c) & 0x7F) - else: - return _ctoi(c) & 0x7F + ch = _ctoi(c) & 0x7F + return chr(ch) if type(c) is str else ch def ctrl(c): - if type(c) == type(""): - return chr(_ctoi(c) & 0x1F) - else: - return _ctoi(c) & 0x1F + ch = _ctoi(c) & 0x1F + return chr(ch) if type(c) is str else ch def alt(c): - if type(c) == type(""): - return chr(_ctoi(c) | 0x80) - else: - return _ctoi(c) | 0x80 + ch = _ctoi(c) | 0x80 + return chr(ch) if type(c) is str else ch def unctrl(c): @@ -173,6 +176,4 @@ def unctrl(c): rep = chr(bits & 0x7F) else: rep = "^" + chr(((bits & 0x7F) | 0x20) + 0x20) - if bits & 0x80: - return "!" + rep - return rep + return "!" + rep if bits & 0x80 else rep From de2050f104ab83fb8c2f842d93e8025222299848 Mon Sep 17 00:00:00 2001 From: Alessandro Gatti Date: Mon, 7 Sep 2026 15:03:03 +0200 Subject: [PATCH 3/3] curses.ascii: Update version number. This commit updates the `curses.ascii` module's manifest version number, bumping its patch version by one. Signed-off-by: Alessandro Gatti --- python-stdlib/curses.ascii/manifest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python-stdlib/curses.ascii/manifest.py b/python-stdlib/curses.ascii/manifest.py index 643e3d49a..4f202ed63 100644 --- a/python-stdlib/curses.ascii/manifest.py +++ b/python-stdlib/curses.ascii/manifest.py @@ -1,3 +1,3 @@ -metadata(version="3.4.3") +metadata(version="3.4.4") package("curses")