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]

Reply via email to