From fe71527dc7f7cc0baa2a0034ae8cbe10b90e68b2 Mon Sep 17 00:00:00 2001 From: Vyron Vasileiadis Date: Wed, 29 Jul 2026 14:58:39 +0300 Subject: [PATCH] gh-154874: Fix the sign of curses.termattrs() 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 4ba210c162d0f6..cbc77c0046b73e 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -3242,5 +3242,33 @@ def test_color(self): 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 00000000000000..61ba4f7374b318 --- /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 b8680edc6c0bed..261d0b1c6b4958 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -7899,7 +7899,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]