diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 4ba210c162d0f6..9a74672d7428c6 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]