diff --git a/Doc/library/curses.rst b/Doc/library/curses.rst index a8a9680d0703ec..d7c2905ec7347d 100644 --- a/Doc/library/curses.rst +++ b/Doc/library/curses.rst @@ -989,6 +989,55 @@ Window objects ``0`` (no attributes). +.. method:: window.attr_get() + + Return the window's current rendition as a ``(attrs, pair)`` tuple, + where *attrs* is the set of attributes and *pair* is the color pair number. + + Unlike :meth:`attron` and friends, which take packed ``A_*`` attributes, + this method and the other ``attr_*`` methods work with the + :ref:`WA_* attributes ` and keep the color pair as a + separate number, which lets them use color pairs that do not fit alongside + the attributes in a single value. + + .. versionadded:: next + + +.. method:: window.attr_set(attr, pair=0) + + Set the window's rendition to the attributes *attr* and the color pair *pair*. + + .. versionadded:: next + + +.. method:: window.attr_on(attr) + + Turn on the attributes *attr* without affecting any others. + + .. versionadded:: next + + +.. method:: window.attr_off(attr) + + Turn off the attributes *attr* without affecting any others. + + .. versionadded:: next + + +.. method:: window.color_set(pair) + + Set the window's color pair to *pair*, leaving the other attributes unchanged. + + .. versionadded:: next + + +.. method:: window.getattrs() + + Return the window's current attributes. + + .. versionadded:: next + + .. method:: window.bkgd(ch[, attr]) Set the background property of the window to the character *ch*, with @@ -1888,6 +1937,24 @@ The exact constants available are system dependent. .. versionadded:: 3.7 ``A_ITALIC`` was added. +.. _curses-wa-constants: + +The :meth:`~window.attr_get`, :meth:`~window.attr_set`, :meth:`~window.attr_on` +and :meth:`~window.attr_off` methods use a parallel set of ``WA_*`` constants. +These have the same meaning as the corresponding ``A_*`` attributes above +(``WA_BOLD`` like :const:`A_BOLD`, and so on), but belong to the ``attr_t`` type +rather than being packed into a character. In ncurses the two sets share the +same values, but other curses implementations may give them different ones, so +use the ``WA_*`` constants with the ``attr_*`` methods. The available names are +``WA_ATTRIBUTES``, ``WA_NORMAL``, ``WA_STANDOUT``, ``WA_UNDERLINE``, +``WA_REVERSE``, ``WA_BLINK``, ``WA_DIM``, ``WA_BOLD``, ``WA_ALTCHARSET``, +``WA_INVIS``, ``WA_PROTECT``, ``WA_HORIZONTAL``, ``WA_LEFT``, ``WA_LOW``, +``WA_RIGHT``, ``WA_TOP``, ``WA_VERTICAL`` and ``WA_ITALIC`` (each available only +where the platform defines it). + +.. versionadded:: next + The ``WA_*`` constants were added. + Several constants are available to extract corresponding attributes returned by some methods. diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 32962a9520fa69..97435bc0fc377f 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -131,6 +131,13 @@ curses available when built against an ncurses with ``NCURSES_EXT_FUNCS``. (Contributed by Serhiy Storchaka in :gh:`151776`.) +* Add the :mod:`curses` window methods :meth:`~curses.window.attr_get`, + :meth:`~curses.window.attr_set`, :meth:`~curses.window.attr_on`, + :meth:`~curses.window.attr_off` and :meth:`~curses.window.color_set`, which + pass the color pair as a separate argument instead of packing it into the + attribute value, and the corresponding ``WA_*`` attribute constants. + (Contributed by Serhiy Storchaka in :gh:`152219`.) + gzip ---- diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 721cde39861ce9..75e6d2bd62e887 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -651,7 +651,6 @@ def test_scroll(self): win.scrollok(False) def test_attributes(self): - # TODO: attr_get(), attr_set(), ... win = curses.newwin(5, 15, 5, 2) win.attron(curses.A_BOLD) win.attroff(curses.A_BOLD) @@ -660,6 +659,45 @@ def test_attributes(self): win.standout() win.standend() + # The attr_*() family works on attr_t attributes paired with a color + # pair, unlike the chtype-based attron()/attroff()/attrset(). + win.attr_set(curses.A_BOLD | curses.A_UNDERLINE) + attrs, pair = win.attr_get() + self.assertTrue(attrs & curses.A_BOLD) + self.assertTrue(attrs & curses.A_UNDERLINE) + self.assertEqual(pair, 0) + self.assertEqual(win.getattrs(), attrs) + + win.attr_on(curses.A_REVERSE) + self.assertTrue(win.attr_get()[0] & curses.A_REVERSE) + win.attr_off(curses.A_REVERSE) + self.assertFalse(win.attr_get()[0] & curses.A_REVERSE) + + # color_set() with a real pair needs start_color(); see + # test_attr_color_pair. Here only the argument validation is checked, + # which fails before wcolor_set() is reached. + self.assertRaises(TypeError, win.attr_set, 'x') + self.assertRaises(TypeError, win.attr_set, curses.A_BOLD, 'x') + self.assertRaises(TypeError, win.attr_on, 'x') + self.assertRaises(TypeError, win.color_set, 'x') + self.assertRaises(ValueError, win.color_set, -1) + self.assertRaises(ValueError, win.attr_set, curses.A_BOLD, -1) + # attr_t is unsigned: a negative or too-large attribute overflows. + self.assertRaises(OverflowError, win.attr_set, -1) + self.assertRaises(OverflowError, win.attr_on, -1) + self.assertRaises(OverflowError, win.attr_set, 1 << 64) + + @requires_colors + def test_attr_color_pair(self): + win = curses.newwin(5, 15, 5, 2) + curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK) + win.attr_set(curses.A_BOLD, 1) + attrs, pair = win.attr_get() + self.assertTrue(attrs & curses.A_BOLD) + self.assertEqual(pair, 1) + win.color_set(0) + self.assertEqual(win.attr_get()[1], 0) + @requires_curses_window_meth('chgat') def test_chgat(self): win = curses.newwin(5, 15, 5, 2) @@ -691,6 +729,11 @@ def test_chgat(self): self.assertEqual(win.inch(3, 11), b'm'[0] | curses.A_UNDERLINE) self.assertEqual(win.inch(3, 14), b' '[0] | curses.A_UNDERLINE) + # attr_t is unsigned: a negative or too-large attribute overflows. + self.assertRaises(TypeError, win.chgat, 'x') + self.assertRaises(OverflowError, win.chgat, -1) + self.assertRaises(OverflowError, win.chgat, 1 << 64) + def test_background(self): win = curses.newwin(5, 15, 5, 2) win.addstr(0, 0, 'Lorem ipsum') diff --git a/Misc/NEWS.d/next/Library/2026-06-25-19-44-16.gh-issue-152219.ndj4Ib.rst b/Misc/NEWS.d/next/Library/2026-06-25-19-44-16.gh-issue-152219.ndj4Ib.rst new file mode 100644 index 00000000000000..e7ea6011cb7b50 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-25-19-44-16.gh-issue-152219.ndj4Ib.rst @@ -0,0 +1,5 @@ +Add the :mod:`curses` window methods :meth:`~curses.window.attr_get`, +:meth:`~curses.window.attr_set`, :meth:`~curses.window.attr_on`, +:meth:`~curses.window.attr_off` and :meth:`~curses.window.color_set`, which use +a separate color pair argument instead of packing it into the attribute value, +and the corresponding ``WA_*`` attribute constants. diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 3377367d4ce45d..0306c4af3288dc 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -799,6 +799,32 @@ class component_converter(CConverter): [python start generated code]*/ /*[python end generated code: output=da39a3ee5e6b4b0d input=38e9be01d33927fb]*/ +static int +attr_converter(PyObject *arg, void *ptr) +{ + /* attr_t is unsigned and at least as wide as chtype, so an attribute + value must be a non-negative integer that fits in attr_t. */ + unsigned long attr = PyLong_AsUnsignedLong(arg); + if (attr == (unsigned long)-1 && PyErr_Occurred()) { + return 0; + } + if (attr > (unsigned long)(attr_t)-1) { + PyErr_Format(PyExc_OverflowError, + "attribute value is greater than maximum (%lu)", + (unsigned long)(attr_t)-1); + return 0; + } + *(attr_t *)ptr = (attr_t)attr; + return 1; +} + +/*[python input] +class attr_converter(CConverter): + type = 'attr_t' + converter = 'attr_converter' +[python start generated code]*/ +/*[python end generated code: output=da39a3ee5e6b4b0d input=6132d3d99d3ec25a]*/ + /***************************************************************************** The Window Object ******************************************************************************/ @@ -1450,6 +1476,125 @@ _curses_window_attrset_impl(PyCursesWindowObject *self, long attr) return curses_window_check_err(self, rtn, "wattrset", "attrset"); } +/*[clinic input] +_curses.window.attr_get + +Return the window's attributes and color pair as (attrs, pair). +[clinic start generated code]*/ + +static PyObject * +_curses_window_attr_get_impl(PyCursesWindowObject *self) +/*[clinic end generated code: output=74b3f805a2958fb8 input=1efd3c450a1373ef]*/ +{ + attr_t attrs; + int rtn; +#if _NCURSES_EXTENDED_COLOR_FUNCS + int pair; + short legacy_pair; + rtn = wattr_get(self->win, &attrs, &legacy_pair, &pair); +#else + short pair; + rtn = wattr_get(self->win, &attrs, &pair, NULL); +#endif + if (curses_window_check_err(self, rtn, "wattr_get", "attr_get") == NULL) { + return NULL; + } + return Py_BuildValue("(ki)", (unsigned long)attrs, (int)pair); +} + +/*[clinic input] +_curses.window.attr_set + + attr: attr + pair: pair = 0 + / + +Set the window's attributes and color pair. +[clinic start generated code]*/ + +static PyObject * +_curses_window_attr_set_impl(PyCursesWindowObject *self, attr_t attr, + int pair) +/*[clinic end generated code: output=756416e0d6345d4a input=b7936bd6b73eb3f2]*/ +{ + int rtn; +#if _NCURSES_EXTENDED_COLOR_FUNCS + rtn = wattr_set(self->win, attr, 0, &pair); +#else + rtn = wattr_set(self->win, attr, (short)pair, NULL); +#endif + return curses_window_check_err(self, rtn, "wattr_set", "attr_set"); +} + +/*[clinic input] +_curses.window.attr_on + + attr: attr + / + +Turn on the given attributes without affecting any others. +[clinic start generated code]*/ + +static PyObject * +_curses_window_attr_on_impl(PyCursesWindowObject *self, attr_t attr) +/*[clinic end generated code: output=712f13a558c5a6cb input=6a51a3d597ddca4b]*/ +{ + int rtn = wattr_on(self->win, attr, NULL); + return curses_window_check_err(self, rtn, "wattr_on", "attr_on"); +} + +/*[clinic input] +_curses.window.attr_off + + attr: attr + / + +Turn off the given attributes without affecting any others. +[clinic start generated code]*/ + +static PyObject * +_curses_window_attr_off_impl(PyCursesWindowObject *self, attr_t attr) +/*[clinic end generated code: output=ac680aead16f74fa input=c5d778b84030d388]*/ +{ + int rtn = wattr_off(self->win, attr, NULL); + return curses_window_check_err(self, rtn, "wattr_off", "attr_off"); +} + +/*[clinic input] +_curses.window.color_set + + pair: pair + / + +Set the window's color pair attribute. +[clinic start generated code]*/ + +static PyObject * +_curses_window_color_set_impl(PyCursesWindowObject *self, int pair) +/*[clinic end generated code: output=5e9e83f02a29bf1c input=70026f6d411db130]*/ +{ + int rtn; +#if _NCURSES_EXTENDED_COLOR_FUNCS + rtn = wcolor_set(self->win, 0, &pair); +#else + rtn = wcolor_set(self->win, (short)pair, NULL); +#endif + return curses_window_check_err(self, rtn, "wcolor_set", "color_set"); +} + +/*[clinic input] +_curses.window.getattrs + +Return the window's current attributes. +[clinic start generated code]*/ + +static PyObject * +_curses_window_getattrs_impl(PyCursesWindowObject *self) +/*[clinic end generated code: output=835f499205204ec4 input=bf56a0af5b730bd1]*/ +{ + return PyLong_FromUnsignedLong((unsigned long)(attr_t)getattrs(self->win)); +} + /*[clinic input] _curses.window.bkgdset @@ -1697,30 +1842,25 @@ PyCursesWindow_ChgAt(PyObject *op, PyObject *args) int num = -1; short color; attr_t attr = A_NORMAL; - long lattr; int use_xy = FALSE; switch (PyTuple_Size(args)) { case 1: - if (!PyArg_ParseTuple(args,"l;attr", &lattr)) + if (!PyArg_ParseTuple(args,"O&;attr", attr_converter, &attr)) return NULL; - attr = lattr; break; case 2: - if (!PyArg_ParseTuple(args,"il;n,attr", &num, &lattr)) + if (!PyArg_ParseTuple(args,"iO&;n,attr", &num, attr_converter, &attr)) return NULL; - attr = lattr; break; case 3: - if (!PyArg_ParseTuple(args,"iil;y,x,attr", &y, &x, &lattr)) + if (!PyArg_ParseTuple(args,"iiO&;y,x,attr", &y, &x, attr_converter, &attr)) return NULL; - attr = lattr; use_xy = TRUE; break; case 4: - if (!PyArg_ParseTuple(args,"iiil;y,x,n,attr", &y, &x, &num, &lattr)) + if (!PyArg_ParseTuple(args,"iiiO&;y,x,n,attr", &y, &x, &num, attr_converter, &attr)) return NULL; - attr = lattr; use_xy = TRUE; break; default: @@ -3377,6 +3517,12 @@ static PyMethodDef PyCursesWindow_methods[] = { _CURSES_WINDOW_ATTROFF_METHODDEF _CURSES_WINDOW_ATTRON_METHODDEF _CURSES_WINDOW_ATTRSET_METHODDEF + _CURSES_WINDOW_ATTR_GET_METHODDEF + _CURSES_WINDOW_ATTR_SET_METHODDEF + _CURSES_WINDOW_ATTR_ON_METHODDEF + _CURSES_WINDOW_ATTR_OFF_METHODDEF + _CURSES_WINDOW_COLOR_SET_METHODDEF + _CURSES_WINDOW_GETATTRS_METHODDEF _CURSES_WINDOW_BKGD_METHODDEF #ifdef HAVE_CURSES_WCHGAT { @@ -6882,6 +7028,65 @@ cursesmodule_exec(PyObject *module) SetDictInt("A_ITALIC", A_ITALIC); #endif + /* The WA_* attributes are used by the attr_t-based functions + (attr_get, attr_set, ...). ncurses defines them bit-identically to the + matching A_* constants, but X/Open keeps the two sets distinct, so other + implementations (such as NetBSD curses) may give them different values. */ +#ifdef WA_ATTRIBUTES + SetDictInt("WA_ATTRIBUTES", WA_ATTRIBUTES); +#endif +#ifdef WA_NORMAL + SetDictInt("WA_NORMAL", WA_NORMAL); +#endif +#ifdef WA_STANDOUT + SetDictInt("WA_STANDOUT", WA_STANDOUT); +#endif +#ifdef WA_UNDERLINE + SetDictInt("WA_UNDERLINE", WA_UNDERLINE); +#endif +#ifdef WA_REVERSE + SetDictInt("WA_REVERSE", WA_REVERSE); +#endif +#ifdef WA_BLINK + SetDictInt("WA_BLINK", WA_BLINK); +#endif +#ifdef WA_DIM + SetDictInt("WA_DIM", WA_DIM); +#endif +#ifdef WA_BOLD + SetDictInt("WA_BOLD", WA_BOLD); +#endif +#ifdef WA_ALTCHARSET + SetDictInt("WA_ALTCHARSET", WA_ALTCHARSET); +#endif +#ifdef WA_INVIS + SetDictInt("WA_INVIS", WA_INVIS); +#endif +#ifdef WA_PROTECT + SetDictInt("WA_PROTECT", WA_PROTECT); +#endif +#ifdef WA_HORIZONTAL + SetDictInt("WA_HORIZONTAL", WA_HORIZONTAL); +#endif +#ifdef WA_LEFT + SetDictInt("WA_LEFT", WA_LEFT); +#endif +#ifdef WA_LOW + SetDictInt("WA_LOW", WA_LOW); +#endif +#ifdef WA_RIGHT + SetDictInt("WA_RIGHT", WA_RIGHT); +#endif +#ifdef WA_TOP + SetDictInt("WA_TOP", WA_TOP); +#endif +#ifdef WA_VERTICAL + SetDictInt("WA_VERTICAL", WA_VERTICAL); +#endif +#ifdef WA_ITALIC + SetDictInt("WA_ITALIC", WA_ITALIC); +#endif + SetDictInt("COLOR_BLACK", COLOR_BLACK); SetDictInt("COLOR_RED", COLOR_RED); SetDictInt("COLOR_GREEN", COLOR_GREEN); diff --git a/Modules/clinic/_cursesmodule.c.h b/Modules/clinic/_cursesmodule.c.h index 49f30a35656b48..d4d6e4eeef0158 100644 --- a/Modules/clinic/_cursesmodule.c.h +++ b/Modules/clinic/_cursesmodule.c.h @@ -353,6 +353,162 @@ _curses_window_attrset(PyObject *self, PyObject *arg) return return_value; } +PyDoc_STRVAR(_curses_window_attr_get__doc__, +"attr_get($self, /)\n" +"--\n" +"\n" +"Return the window\'s attributes and color pair as (attrs, pair)."); + +#define _CURSES_WINDOW_ATTR_GET_METHODDEF \ + {"attr_get", (PyCFunction)_curses_window_attr_get, METH_NOARGS, _curses_window_attr_get__doc__}, + +static PyObject * +_curses_window_attr_get_impl(PyCursesWindowObject *self); + +static PyObject * +_curses_window_attr_get(PyObject *self, PyObject *Py_UNUSED(ignored)) +{ + return _curses_window_attr_get_impl((PyCursesWindowObject *)self); +} + +PyDoc_STRVAR(_curses_window_attr_set__doc__, +"attr_set($self, attr, pair=0, /)\n" +"--\n" +"\n" +"Set the window\'s attributes and color pair."); + +#define _CURSES_WINDOW_ATTR_SET_METHODDEF \ + {"attr_set", _PyCFunction_CAST(_curses_window_attr_set), METH_FASTCALL, _curses_window_attr_set__doc__}, + +static PyObject * +_curses_window_attr_set_impl(PyCursesWindowObject *self, attr_t attr, + int pair); + +static PyObject * +_curses_window_attr_set(PyObject *self, PyObject *const *args, Py_ssize_t nargs) +{ + PyObject *return_value = NULL; + attr_t attr; + int pair = 0; + + if (!_PyArg_CheckPositional("attr_set", nargs, 1, 2)) { + goto exit; + } + if (!attr_converter(args[0], &attr)) { + goto exit; + } + if (nargs < 2) { + goto skip_optional; + } + if (!pair_converter(args[1], &pair)) { + goto exit; + } +skip_optional: + return_value = _curses_window_attr_set_impl((PyCursesWindowObject *)self, attr, pair); + +exit: + return return_value; +} + +PyDoc_STRVAR(_curses_window_attr_on__doc__, +"attr_on($self, attr, /)\n" +"--\n" +"\n" +"Turn on the given attributes without affecting any others."); + +#define _CURSES_WINDOW_ATTR_ON_METHODDEF \ + {"attr_on", (PyCFunction)_curses_window_attr_on, METH_O, _curses_window_attr_on__doc__}, + +static PyObject * +_curses_window_attr_on_impl(PyCursesWindowObject *self, attr_t attr); + +static PyObject * +_curses_window_attr_on(PyObject *self, PyObject *arg) +{ + PyObject *return_value = NULL; + attr_t attr; + + if (!attr_converter(arg, &attr)) { + goto exit; + } + return_value = _curses_window_attr_on_impl((PyCursesWindowObject *)self, attr); + +exit: + return return_value; +} + +PyDoc_STRVAR(_curses_window_attr_off__doc__, +"attr_off($self, attr, /)\n" +"--\n" +"\n" +"Turn off the given attributes without affecting any others."); + +#define _CURSES_WINDOW_ATTR_OFF_METHODDEF \ + {"attr_off", (PyCFunction)_curses_window_attr_off, METH_O, _curses_window_attr_off__doc__}, + +static PyObject * +_curses_window_attr_off_impl(PyCursesWindowObject *self, attr_t attr); + +static PyObject * +_curses_window_attr_off(PyObject *self, PyObject *arg) +{ + PyObject *return_value = NULL; + attr_t attr; + + if (!attr_converter(arg, &attr)) { + goto exit; + } + return_value = _curses_window_attr_off_impl((PyCursesWindowObject *)self, attr); + +exit: + return return_value; +} + +PyDoc_STRVAR(_curses_window_color_set__doc__, +"color_set($self, pair, /)\n" +"--\n" +"\n" +"Set the window\'s color pair attribute."); + +#define _CURSES_WINDOW_COLOR_SET_METHODDEF \ + {"color_set", (PyCFunction)_curses_window_color_set, METH_O, _curses_window_color_set__doc__}, + +static PyObject * +_curses_window_color_set_impl(PyCursesWindowObject *self, int pair); + +static PyObject * +_curses_window_color_set(PyObject *self, PyObject *arg) +{ + PyObject *return_value = NULL; + int pair; + + if (!pair_converter(arg, &pair)) { + goto exit; + } + return_value = _curses_window_color_set_impl((PyCursesWindowObject *)self, pair); + +exit: + return return_value; +} + +PyDoc_STRVAR(_curses_window_getattrs__doc__, +"getattrs($self, /)\n" +"--\n" +"\n" +"Return the window\'s current attributes."); + +#define _CURSES_WINDOW_GETATTRS_METHODDEF \ + {"getattrs", (PyCFunction)_curses_window_getattrs, METH_NOARGS, _curses_window_getattrs__doc__}, + +static PyObject * +_curses_window_getattrs_impl(PyCursesWindowObject *self); + +static PyObject * +_curses_window_getattrs(PyObject *self, PyObject *Py_UNUSED(ignored)) +{ + return _curses_window_getattrs_impl((PyCursesWindowObject *)self); +} + PyDoc_STRVAR(_curses_window_bkgdset__doc__, "bkgdset($self, ch, attr=_curses.A_NORMAL, /)\n" "--\n" @@ -4966,4 +5122,4 @@ _curses_has_extended_color_support(PyObject *module, PyObject *Py_UNUSED(ignored #ifndef _CURSES_ASSUME_DEFAULT_COLORS_METHODDEF #define _CURSES_ASSUME_DEFAULT_COLORS_METHODDEF #endif /* !defined(_CURSES_ASSUME_DEFAULT_COLORS_METHODDEF) */ -/*[clinic end generated code: output=fd0f4e65dc594a65 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=3d8d59f44ded2226 input=a9049054013a1b77]*/