gh-156138: Keep the color pair when a curses write restores the rendition (GH-156139)

serhiy-storchaka <[email protected]>
Newsgroups gmane.comp.python.cvs
Message-ID <[email protected]>
https://github.com/python/cpython/commit/0f9fb00af8af3f08388d50d09121b9ce460c38a7
commit: 0f9fb00af8af3f08388d50d09121b9ce460c38a7
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-08-21T09:39:12+03:00
summary:

gh-156138: Keep the color pair when a curses write restores the rendition (GH-156139)

addstr(), addnstr(), insstr() and insnstr() saved the window rendition with
getattrs() and put it back with wattrset(), whose A_COLOR field holds only
pairs 0 to 255, so a window using a larger pair lost it.  Save and restore
the pair with wattr_get() and wattr_set() where they exist.

files:
M Lib/test/test_curses.py
M Modules/_cursesmodule.c

diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py
index 6ab951ad2786ea..6518dadebae83f 100644
--- a/Lib/test/test_curses.py
+++ b/Lib/test/test_curses.py
@@ -910,6 +910,30 @@ def test_output_string_attr_restored(self):
                 func(0, 0, *args, curses.A_BOLD)
                 self.assertEqual(win.getattrs(), curses.A_UNDERLINE)
 
+    @requires_colors
+    @requires_curses_window_meth('color_set')
+    @requires_curses_window_meth('attr_get')
+    def test_output_string_pair_restored(self):
+        # The rendition put back after a write includes the color pair, also
+        # when it is larger than the A_COLOR field of a chtype holds.
+        pairs = [7]
+        if curses.has_extended_color_support() and curses.COLOR_PAIRS > 300:
+            pairs.append(300)
+        win = curses.newwin(2, 10, 0, 0)
+        for pair in pairs:
+            curses.init_pair(pair, curses.COLOR_RED, curses.COLOR_BLACK)
+            for func, args in [(win.addstr, ('x',)), (win.addnstr, ('x', 1)),
+                               (win.insstr, ('x',)), (win.insnstr, ('x', 1))]:
+                with self.subTest(func.__qualname__, pair=pair):
+                    win.color_set(pair)
+                    func(0, 0, *args, curses.A_BOLD)
+                    self.assertEqual(win.attr_get()[1], pair)
+                    win.color_set(pair)
+                    # y=100 is outside the window, so the write fails.
+                    self.assertRaises(curses.error, func, 100, 0, *args,
+                                      curses.A_BOLD)
+                    self.assertEqual(win.attr_get()[1], pair)
+
     def test_add_string_behavior(self):
         # addstr() advances the cursor past the written text; addnstr()
         # writes at most n characters.
diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c
index 0bab30184a357a..866ed4bafe6b9c 100644
--- a/Modules/_cursesmodule.c
+++ b/Modules/_cursesmodule.c
@@ -2161,6 +2161,56 @@ curses_wattrset(PyCursesWindowObject *self, attr_t attr, const char *funcname)
     return 0;
 }
 
+/* Read the rendition a write with an *attr* argument has to put back.  The
+   color pair is read apart from the attributes because the A_COLOR field of a
+   chtype holds only pairs 0 to 255, while a window can use a larger one. */
+static int
+curses_wattr_save(PyCursesWindowObject *self, attr_t *attrs, int *pair,
+                  const char *funcname)
+{
+#if defined(HAVE_CURSES_WATTR_GET) && defined(HAVE_CURSES_WATTR_SET)
+    int rtn;
+#if _NCURSES_EXTENDED_COLOR_FUNCS
+    short legacy_pair;
+    rtn = wattr_get(self->win, attrs, &legacy_pair, pair);
+#else
+    short spair;
+    rtn = wattr_get(self->win, attrs, &spair, NULL);
+    *pair = spair;
+#endif
+    if (rtn == ERR) {
+        curses_window_set_error(self, "wattr_get", funcname);
+        return -1;
+    }
+#else
+    *attrs = getattrs(self->win);
+    *pair = 0;
+#endif
+    return 0;
+}
+
+/* Put the rendition back.  The name of the curses function used is
+   _CURSES_WATTR_RESTORE_FUNC, for the caller to name it in an error. */
+#if defined(HAVE_CURSES_WATTR_GET) && defined(HAVE_CURSES_WATTR_SET)
+#define _CURSES_WATTR_RESTORE_FUNC      "wattr_set"
+#else
+#define _CURSES_WATTR_RESTORE_FUNC      "wattrset"
+#endif
+
+static int
+curses_wattr_restore(PyCursesWindowObject *self, attr_t attrs, int pair)
+{
+#if defined(HAVE_CURSES_WATTR_GET) && defined(HAVE_CURSES_WATTR_SET)
+#if _NCURSES_EXTENDED_COLOR_FUNCS
+    return wattr_set(self->win, attrs, 0, &pair);
+#else
+    return wattr_set(self->win, attrs, (short)pair, NULL);
+#endif
+#else
+    return wattrset(self->win, attrs);
+#endif
+}
+
 /*[clinic input]
 _curses.window.addstr
 
@@ -2201,6 +2251,7 @@ _curses_window_addstr_impl(PyCursesWindowObject *self, int group_left_1,
     wchar_t *wstr = NULL;
 #endif
     attr_t attr_old = A_NORMAL;
+    int pair_old = 0;
     int use_xy = group_left_1, use_attr = group_right_1;
     const char *funcname;
 
@@ -2225,8 +2276,9 @@ _curses_window_addstr_impl(PyCursesWindowObject *self, int group_left_1,
         return NULL;
     }
     if (use_attr) {
-        attr_old = getattrs(self->win);
-        if (curses_wattrset(self, attr, "addstr") < 0) {
+        if (curses_wattr_save(self, &attr_old, &pair_old, "addstr") < 0 ||
+            curses_wattrset(self, attr, "addstr") < 0)
+        {
             curses_release_wstr(strtype, wstr);
             Py_XDECREF(bytesobj);
             return NULL;
@@ -2263,10 +2315,10 @@ _curses_window_addstr_impl(PyCursesWindowObject *self, int group_left_1,
         Py_DECREF(bytesobj);
     }
     if (use_attr) {
-        int attr_rtn = wattrset(self->win, attr_old);
+        int attr_rtn = curses_wattr_restore(self, attr_old, pair_old);
         if (rtn != ERR) {
             rtn = attr_rtn;
-            funcname = "wattrset";
+            funcname = _CURSES_WATTR_RESTORE_FUNC;
         }
     }
     return curses_window_check_err(self, rtn, funcname, "addstr");
@@ -2315,6 +2367,7 @@ _curses_window_addnstr_impl(PyCursesWindowObject *self, int group_left_1,
     wchar_t *wstr = NULL;
 #endif
     attr_t attr_old = A_NORMAL;
+    int pair_old = 0;
     int use_xy = group_left_1, use_attr = group_right_1;
     const char *funcname;
 
@@ -2339,8 +2392,9 @@ _curses_window_addnstr_impl(PyCursesWindowObject *self, int group_left_1,
         return NULL;
 
     if (use_attr) {
-        attr_old = getattrs(self->win);
-        if (curses_wattrset(self, attr, "addnstr") < 0) {
+        if (curses_wattr_save(self, &attr_old, &pair_old, "addnstr") < 0 ||
+            curses_wattrset(self, attr, "addnstr") < 0)
+        {
             curses_release_wstr(strtype, wstr);
             Py_XDECREF(bytesobj);
             return NULL;
@@ -2373,10 +2427,10 @@ _curses_window_addnstr_impl(PyCursesWindowObject *self, int group_left_1,
         Py_DECREF(bytesobj);
     }
     if (use_attr) {
-        int attr_rtn = wattrset(self->win, attr_old);
+        int attr_rtn = curses_wattr_restore(self, attr_old, pair_old);
         if (rtn != ERR) {
             rtn = attr_rtn;
-            funcname = "wattrset";
+            funcname = _CURSES_WATTR_RESTORE_FUNC;
         }
     }
     return curses_window_check_err(self, rtn, funcname, "addnstr");
@@ -4035,6 +4089,7 @@ _curses_window_insstr_impl(PyCursesWindowObject *self, int group_left_1,
     wchar_t *wstr = NULL;
 #endif
     attr_t attr_old = A_NORMAL;
+    int pair_old = 0;
     int use_xy = group_left_1, use_attr = group_right_1;
     const char *funcname;
 
@@ -4059,8 +4114,9 @@ _curses_window_insstr_impl(PyCursesWindowObject *self, int group_left_1,
         return NULL;
 
     if (use_attr) {
-        attr_old = getattrs(self->win);
-        if (curses_wattrset(self, attr, "insstr") < 0) {
+        if (curses_wattr_save(self, &attr_old, &pair_old, "insstr") < 0 ||
+            curses_wattrset(self, attr, "insstr") < 0)
+        {
             curses_release_wstr(strtype, wstr);
             Py_XDECREF(bytesobj);
             return NULL;
@@ -4093,10 +4149,10 @@ _curses_window_insstr_impl(PyCursesWindowObject *self, int group_left_1,
         Py_DECREF(bytesobj);
     }
     if (use_attr) {
-        int attr_rtn = wattrset(self->win, attr_old);
+        int attr_rtn = curses_wattr_restore(self, attr_old, pair_old);
         if (rtn != ERR) {
             rtn = attr_rtn;
-            funcname = "wattrset";
+            funcname = _CURSES_WATTR_RESTORE_FUNC;
         }
     }
     return curses_window_check_err(self, rtn, funcname, "insstr");
@@ -4147,6 +4203,7 @@ _curses_window_insnstr_impl(PyCursesWindowObject *self, int group_left_1,
     wchar_t *wstr = NULL;
 #endif
     attr_t attr_old = A_NORMAL;
+    int pair_old = 0;
     int use_xy = group_left_1, use_attr = group_right_1;
     const char *funcname;
 
@@ -4171,8 +4228,9 @@ _curses_window_insnstr_impl(PyCursesWindowObject *self, int group_left_1,
         return NULL;
 
     if (use_attr) {
-        attr_old = getattrs(self->win);
-        if (curses_wattrset(self, attr, "insnstr") < 0) {
+        if (curses_wattr_save(self, &attr_old, &pair_old, "insnstr") < 0 ||
+            curses_wattrset(self, attr, "insnstr") < 0)
+        {
             curses_release_wstr(strtype, wstr);
             return NULL;
         }
@@ -4204,10 +4262,10 @@ _curses_window_insnstr_impl(PyCursesWindowObject *self, int group_left_1,
         Py_DECREF(bytesobj);
     }
     if (use_attr) {
-        int attr_rtn = wattrset(self->win, attr_old);
+        int attr_rtn = curses_wattr_restore(self, attr_old, pair_old);
         if (rtn != ERR) {
             rtn = attr_rtn;
-            funcname = "wattrset";
+            funcname = _CURSES_WATTR_RESTORE_FUNC;
         }
     }
     return curses_window_check_err(self, rtn, funcname, "insnstr");

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.