gh-154874: Fix the sign of curses.termattrs() (GH-154875)

serhiy-storchaka <[email protected]> Thu, 30 Jul 2026 15:50:20 -0400 (EDT)
Newsgroups gmane.comp.python.cvs
Message-ID <[email protected]>
https://github.com/python/cpython/commit/0b083c43a455fbdf5378495751e142ea83e294b0
commit: 0b083c43a455fbdf5378495751e142ea83e294b0
branch: main
author: Vyron Vasileiadis <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-07-30T19:50:03Z
summary:

gh-154874: Fix the sign of curses.termattrs() (GH-154875)

termattrs() returns a chtype mask, but it was routed through an int, so a
terminal that advertises A_ITALIC came back negative and the result could
no longer be passed to the attribute functions.

files:
A Misc/NEWS.d/next/Library/2026-07-29-11-58-17.gh-issue-154874.NAPdBp.rst
M Lib/test/test_curses.py
M Modules/_cursesmodule.c

diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py
index 845b9a135a24243..f7584a39b182d99 100644
--- a/Lib/test/test_curses.py
+++ b/Lib/test/test_curses.py
@@ -3241,5 +3241,33 @@ def test_color(self):
         curses.slk_color(0)
 
 
[email protected](hasattr(curses, 'newterm'), 'requires curses.newterm()')
[email protected](BROKEN_NEWTERM, 'ncurses < 6.5 mishandles repeated newterm()')
[email protected](not term or term == 'unknown',
+                 f"$TERM={term!r}, newterm() may not work")
[email protected](sys.platform == "cygwin",
+                 "cygwin's curses mostly just hangs")
+class TermAttrsTests(NewtermTestBase):
+    # A_ITALIC is the topmost bit of a 32-bit attribute mask, so termattrs()
+    # only tells a signed result from an unsigned one on a terminal that
+    # advertises it.  Drive a known terminal type over a pseudo-terminal
+    # instead of relying on whatever $TERM happens to be.
+
+    def test_termattrs_is_not_negative(self):
+        s = self.make_pty()
+        try:
+            curses.newterm('xterm-256color', s, s)
+        except curses.error:
+            self.skipTest('no xterm-256color terminfo entry')
+        attrs = curses.termattrs()
+        italic = getattr(curses, 'A_ITALIC', 0)
+        if not italic or not attrs & italic:
+            self.skipTest('the terminal advertises no attribute in the top bit')
+        self.assertGreaterEqual(attrs, 0)
+        # termattrs() exists to be passed back to the attribute functions,
+        # which reject a negative mask.
+        curses.newwin(1, 1).attrset(attrs)
+
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/Misc/NEWS.d/next/Library/2026-07-29-11-58-17.gh-issue-154874.NAPdBp.rst b/Misc/NEWS.d/next/Library/2026-07-29-11-58-17.gh-issue-154874.NAPdBp.rst
new file mode 100644
index 000000000000000..61ba4f7374b3184
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-07-29-11-58-17.gh-issue-154874.NAPdBp.rst
@@ -0,0 +1,3 @@
+Fix :func:`curses.termattrs` returning a negative value on a terminal that
+supports :const:`curses.A_ITALIC`, which left its result unusable as an
+attribute mask.
diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c
index f9ac5f0654f5e9e..63e9d1d7a4d95b1 100644
--- a/Modules/_cursesmodule.c
+++ b/Modules/_cursesmodule.c
@@ -7898,7 +7898,11 @@ Return a logical OR of all video attributes supported by the terminal.
 static PyObject *
 _curses_termattrs_impl(PyObject *module)
 /*[clinic end generated code: output=b06f437fce1b6fc4 input=0559882a04f84d1d]*/
-NoArgReturnIntFunctionBody(termattrs)
+{
+    PyCursesStatefulInitialised(module);
+
+    return PyLong_FromUnsignedLong((unsigned long)(chtype)termattrs());
+}
 
 #ifdef HAVE_CURSES_TERM_ATTRS
 /*[clinic input]

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]