From da6982ef5c27270c6f916bd155b448185294179c Mon Sep 17 00:00:00 2001 From: Mandar Wagh Date: Wed, 29 Jul 2026 22:04:43 +0530 Subject: [PATCH 1/2] gh-154874: Fix curses.termattrs() returning a negative attribute mask termattrs() returns a chtype mask, but GH-134327 routed it through the NoArgReturnIntFunctionBody macro, which stores the result in an int to compare it against ERR. On a terminal that advertises A_ITALIC, the topmost bit of a 32-bit mask, that sign-extends and the mask comes back negative, so it can no longer be passed to the attribute functions: >>> curses.termattrs() -2130771968 >>> curses.newwin(1, 1).attrset(curses.termattrs()) OverflowError: can't convert negative value to unsigned int Return the mask unsigned instead, the same way term_attrs(), slk_attr() and window.getattrs() already do. baudrate(), the macro's only other user, really does return a status, so it keeps the ERR check; the macro gains a comment saying it is only for such functions. Co-Authored-By: Claude Opus 5 (1M context) --- Lib/test/test_curses.py | 27 +++++++++++++++++++ ...-07-29-14-22-08.gh-issue-154874.Kq7mZx.rst | 3 +++ Modules/_cursesmodule.c | 14 +++++++++- 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-29-14-22-08.gh-issue-154874.Kq7mZx.rst diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 4ba210c162d0f6..707f255ddf5789 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -3242,5 +3242,32 @@ 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 signed termattrs() only differs from an unsigned one on a terminal + # that advertises the topmost bit of the mask, which is A_ITALIC. Drive + # a terminal type that supports italics over a pseudo-terminal instead of + # trusting $TERM, which on CI is usually 'linux' and advertises none. + + def test_termattrs_is_a_usable_mask(self): + s = self.make_pty() + try: + screen = curses.newterm('xterm-256color', s, s) + except curses.error: + self.skipTest('no xterm-256color terminfo entry') + attrs = curses.termattrs() + # An attribute mask is unsigned, whichever attributes the terminal + # happens to support. + self.assertGreaterEqual(attrs, 0) + # termattrs() exists to be passed back to the attribute functions, + # which reject a negative mask with OverflowError. + screen.stdscr.attrset(attrs) + + if __name__ == '__main__': unittest.main() diff --git a/Misc/NEWS.d/next/Library/2026-07-29-14-22-08.gh-issue-154874.Kq7mZx.rst b/Misc/NEWS.d/next/Library/2026-07-29-14-22-08.gh-issue-154874.Kq7mZx.rst new file mode 100644 index 00000000000000..5904e0fa3e0c9d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-29-14-22-08.gh-issue-154874.Kq7mZx.rst @@ -0,0 +1,3 @@ +Fix :func:`curses.termattrs` returning a negative value on a terminal that +supports :const:`curses.A_ITALIC`. The attribute mask is now returned +unsigned, so it can be passed back to the attribute functions. diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index b8680edc6c0bed..7adf442ad4a5e1 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -5495,6 +5495,10 @@ static PyType_Spec PyCursesScreen_Type_spec = { return curses_check_err(module, rtn, funcname, # X); \ } +/* Only for a function returning an int that may be ERR. A function returning + an attribute mask must not use this: the mask is unsigned and its topmost + bit is a valid attribute, so storing it in an int makes the result both + negative and, for an all-bits-set mask, indistinguishable from ERR. */ #define NoArgReturnIntFunctionBody(X) \ { \ PyCursesStatefulInitialised(module); \ @@ -7899,7 +7903,15 @@ 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); + + /* termattrs() returns a chtype mask, not a status, so there is no ERR to + check for. Go through chtype rather than int: A_ITALIC is the topmost + bit of a 32-bit mask, and sign-extending it would make the result + negative and unusable as an attribute. */ + return PyLong_FromUnsignedLong((unsigned long)(chtype)termattrs()); +} #ifdef HAVE_CURSES_TERM_ATTRS /*[clinic input] From 2453e6772a5ee842f1cb93257e5588568dbd6634 Mon Sep 17 00:00:00 2001 From: Mandar Wagh Date: Wed, 29 Jul 2026 22:07:20 +0530 Subject: [PATCH 2/2] Rename the test class so it does not read as term_attrs() Co-Authored-By: Claude Opus 5 (1M context) --- Lib/test/test_curses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 707f255ddf5789..9a74672d7428c6 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -3248,7 +3248,7 @@ def test_color(self): f"$TERM={term!r}, newterm() may not work") @unittest.skipIf(sys.platform == "cygwin", "cygwin's curses mostly just hangs") -class TermAttrsTests(NewtermTestBase): +class TermattrsTests(NewtermTestBase): # A signed termattrs() only differs from an unsigned one on a terminal # that advertises the topmost bit of the mask, which is A_ITALIC. Drive # a terminal type that supports italics over a pseudo-terminal instead of