https://github.com/python/cpython/commit/68a0f7e073e6d9d8dd2d2a66a7f85309085eefce
commit: 68a0f7e073e6d9d8dd2d2a66a7f85309085eefce
branch: 3.15
author: Miss Islington (bot) <[email protected]>
committer: hugovk <[email protected]>
date: 2026-08-29T17:50:26+03:00
summary:

[3.15] gh-155974: Restore the window attributes when a curses write fails 
(GH-155975) (#156136)

* gh-155974: Restore the window attributes when a curses write fails (GH-155975)

addstr(), addnstr(), insstr() and insnstr() set the window rendition to
the caller's attr, write, then restore the previous rendition.  Since
30dde1eeb3fa the restore sits below an early return taken when the write
fails, so a failed write leaves the caller's attr on the window and
drops whatever the application had set with attrset().

Restore the rendition first and report the write error afterwards.  A
wattrset() failure is still reported when the write itself succeeded.
(cherry picked from commit 83531fd39f24f873671c38b981061afe1730613f)

Co-authored-by: Vyron Vasileiadis <[email protected]>
Co-authored-by: Serhiy Storchaka <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-08-17-22-51-22.gh-issue-155974.Qz3Vb7.rst
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 8b05d2df10b3436..ae3d7cdc5ea31e5 100644
--- a/Lib/test/test_curses.py
+++ b/Lib/test/test_curses.py
@@ -431,6 +431,26 @@ def test_output_string_embedded_null_chars(self):
                 self.assertRaises(ValueError, stdscr.insstr, arg)
                 self.assertRaises(ValueError, stdscr.insnstr, arg, 1)
 
+    def test_output_string_attr_restored(self):
+        # A write with an attr restores the window rendition afterwards,
+        # whether it succeeded or failed.
+        win = curses.newwin(2, 10, 0, 0)
+        def current_attrs():
+            # Write a cell with the window's current rendition and read it
+            # back, so that the rendition itself is checked.
+            win.addstr(1, 0, ' ')
+            return win.inch(1, 0) & curses.A_ATTRIBUTES
+        for func, args in [(win.addstr, ('x',)), (win.addnstr, ('x', 1)),
+                           (win.insstr, ('x',)), (win.insnstr, ('x', 1))]:
+            with self.subTest(func.__qualname__):
+                win.attrset(curses.A_UNDERLINE)
+                # y=100 is outside the window, so the write fails.
+                self.assertRaises(curses.error, func, 100, 0, *args,
+                                  curses.A_BOLD)
+                self.assertEqual(current_attrs(), curses.A_UNDERLINE)
+                func(0, 0, *args, curses.A_BOLD)
+                self.assertEqual(current_attrs(), curses.A_UNDERLINE)
+
     def test_add_string_behavior(self):
         # addstr() advances the cursor past the written text; addnstr()
         # writes at most n characters.
diff --git 
a/Misc/NEWS.d/next/Library/2026-08-17-22-51-22.gh-issue-155974.Qz3Vb7.rst 
b/Misc/NEWS.d/next/Library/2026-08-17-22-51-22.gh-issue-155974.Qz3Vb7.rst
new file mode 100644
index 000000000000000..8621bc49a03ccab
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-08-17-22-51-22.gh-issue-155974.Qz3Vb7.rst
@@ -0,0 +1,4 @@
+Fix a regression in Python 3.15: :meth:`~curses.window.addstr`,
+:meth:`~curses.window.addnstr`, :meth:`~curses.window.insstr` and
+:meth:`~curses.window.insnstr` again restore the window attributes when the
+write fails, instead of leaving the temporary *attr* applied.
diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c
index f7ec3d24b03abd5..88511bc3573e52e 100644
--- a/Modules/_cursesmodule.c
+++ b/Modules/_cursesmodule.c
@@ -1150,15 +1150,14 @@ _curses_window_addstr_impl(PyCursesWindowObject *self, 
int group_left_1,
         }
         Py_DECREF(bytesobj);
     }
-    if (rtn == ERR) {
-        curses_window_set_error(self, funcname, "addstr");
-        return NULL;
-    }
     if (use_attr) {
-        rtn = wattrset(self->win, attr_old);
-        return curses_window_check_err(self, rtn, "wattrset", "addstr");
+        int attr_rtn = wattrset(self->win, attr_old);
+        if (rtn != ERR) {
+            rtn = attr_rtn;
+            funcname = "wattrset";
+        }
     }
-    Py_RETURN_NONE;
+    return curses_window_check_err(self, rtn, funcname, "addstr");
 }
 
 /*[clinic input]
@@ -1249,15 +1248,14 @@ _curses_window_addnstr_impl(PyCursesWindowObject *self, 
int group_left_1,
         }
         Py_DECREF(bytesobj);
     }
-    if (rtn == ERR) {
-        curses_window_set_error(self, funcname, "addnstr");
-        return NULL;
-    }
     if (use_attr) {
-        rtn = wattrset(self->win, attr_old);
-        return curses_window_check_err(self, rtn, "wattrset", "addnstr");
+        int attr_rtn = wattrset(self->win, attr_old);
+        if (rtn != ERR) {
+            rtn = attr_rtn;
+            funcname = "wattrset";
+        }
     }
-    Py_RETURN_NONE;
+    return curses_window_check_err(self, rtn, funcname, "addnstr");
 }
 
 /*[clinic input]
@@ -2326,15 +2324,14 @@ _curses_window_insstr_impl(PyCursesWindowObject *self, 
int group_left_1,
         }
         Py_DECREF(bytesobj);
     }
-    if (rtn == ERR) {
-        curses_window_set_error(self, funcname, "insstr");
-        return NULL;
-    }
     if (use_attr) {
-        rtn = wattrset(self->win, attr_old);
-        return curses_window_check_err(self, rtn, "wattrset", "insstr");
+        int attr_rtn = wattrset(self->win, attr_old);
+        if (rtn != ERR) {
+            rtn = attr_rtn;
+            funcname = "wattrset";
+        }
     }
-    Py_RETURN_NONE;
+    return curses_window_check_err(self, rtn, funcname, "insstr");
 }
 
 /*[clinic input]
@@ -2426,15 +2423,14 @@ _curses_window_insnstr_impl(PyCursesWindowObject *self, 
int group_left_1,
         }
         Py_DECREF(bytesobj);
     }
-    if (rtn == ERR) {
-        curses_window_set_error(self, funcname, "insnstr");
-        return NULL;
-    }
     if (use_attr) {
-        rtn = wattrset(self->win, attr_old);
-        return curses_window_check_err(self, rtn, "wattrset", "insnstr");
+        int attr_rtn = wattrset(self->win, attr_old);
+        if (rtn != ERR) {
+            rtn = attr_rtn;
+            funcname = "wattrset";
+        }
     }
-    Py_RETURN_NONE;
+    return curses_window_check_err(self, rtn, funcname, "insnstr");
 }
 
 /*[clinic input]

_______________________________________________
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