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]