From 0b083c43a455fbdf5378495751e142ea83e294b0 Mon Sep 17 00:00:00 2001 From: Vyron Vasileiadis Date: Thu, 30 Jul 2026 22:50:03 +0300 Subject: [PATCH] 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. --- Lib/test/test_curses.py | 28 +++++++++++++++++++ ...-07-29-11-58-17.gh-issue-154874.NAPdBp.rst | 3 ++ Modules/_cursesmodule.c | 6 +++- 3 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-29-11-58-17.gh-issue-154874.NAPdBp.rst diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 845b9a135a24..f7584a39b182 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -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 index 000000000000..61ba4f7374b3 --- /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 f9ac5f0654f5..63e9d1d7a4d9 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] -- 2.47.3