]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-154874: Fix the sign of curses.termattrs() (GH-154875)
authorVyron Vasileiadis <hi@fedonman.com>
Thu, 30 Jul 2026 19:50:03 +0000 (22:50 +0300)
committerGitHub <noreply@github.com>
Thu, 30 Jul 2026 19:50:03 +0000 (19:50 +0000)
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.

Lib/test/test_curses.py
Misc/NEWS.d/next/Library/2026-07-29-11-58-17.gh-issue-154874.NAPdBp.rst [new file with mode: 0644]
Modules/_cursesmodule.c

index 845b9a135a2424381fb1829d10e9de7bf2817d1a..f7584a39b182d99cce07d61854c724d4b8471932 100644 (file)
@@ -3241,5 +3241,33 @@ class SLKTests(NewtermTestBase):
         curses.slk_color(0)
 
 
+@unittest.skipUnless(hasattr(curses, 'newterm'), 'requires curses.newterm()')
+@unittest.skipIf(BROKEN_NEWTERM, 'ncurses < 6.5 mishandles repeated newterm()')
+@unittest.skipIf(not term or term == 'unknown',
+                 f"$TERM={term!r}, newterm() may not work")
+@unittest.skipIf(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 (file)
index 0000000..61ba4f7
--- /dev/null
@@ -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.
index f9ac5f0654f5e9e1c90ef4dba1508d3e3f1b64a1..63e9d1d7a4d95b152f9bfde614bcc01eadef1ded 100644 (file)
@@ -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]