diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index f582336fae17344..6ab951ad2786ea8 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -895,6 +895,21 @@ 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) + 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(win.getattrs(), curses.A_UNDERLINE) + func(0, 0, *args, curses.A_BOLD) + self.assertEqual(win.getattrs(), 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 006e27d55d8925d..0bab30184a357ad 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -2262,15 +2262,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] @@ -2373,15 +2372,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] @@ -4094,15 +4092,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] @@ -4206,15 +4203,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] diff --git a/Platforms/WASI/_build.py b/Platforms/WASI/_build.py index 66e2ae64e181d70..fff7e3f9f8cdada 100644 --- a/Platforms/WASI/_build.py +++ b/Platforms/WASI/_build.py @@ -129,7 +129,19 @@ def call(command, *, context=None, quiet=False, **kwargs): stderr = subprocess.STDOUT _shared.log("📝", f"Logging output to {stdout.name} (--quiet)...") - subprocess.check_call(command, **kwargs, stdout=stdout, stderr=stderr) + try: + subprocess.check_call(command, **kwargs, stdout=stdout, stderr=stderr) + except subprocess.CalledProcessError as error: + if quiet: + _shared.log("❌", f"Exit code {error.returncode}") + separator() + with open(stdout.name, encoding="utf-8") as file: + lines = file.readlines() + # Inefficient, but the log shouldn't be dramatically large. + print("".join(lines[-10:]), end="") + if not lines[-1].endswith("\n"): + print() + sys.exit(error.returncode) @subdir("build_python_path", clean_ok=True) @@ -163,8 +175,7 @@ def make_build_python(context, _working_dir): cmd = [ binary, "-c", - "import sys; " - "print(f'{sys.version_info.major}.{sys.version_info.minor}')", + "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')", ] version = subprocess.check_output(cmd, encoding="utf-8").strip() diff --git a/Platforms/WASI/_package.py b/Platforms/WASI/_package.py index 37119799c233a23..d1d43e5da2843c1 100644 --- a/Platforms/WASI/_package.py +++ b/Platforms/WASI/_package.py @@ -4,6 +4,7 @@ "pathlib", "shutil", "subprocess", + "sys", "_shared", ] @@ -12,6 +13,7 @@ import pathlib import shutil import subprocess +import sys import _shared @@ -376,45 +378,53 @@ def archive(context): int(source_date_epoch), datetime.UTC ).strftime(mtime_format) else: - mtime = subprocess.run( + try: + mtime = subprocess.run( + [ + "git", + "log", + "-1", + "--format=tformat:%cd", + f"--date=format:{mtime_format}", + os.fsdecode(context.checkout), + ], + env={"TZ": "UTC0"}, + capture_output=True, + text=True, + check=True, + ).stdout.strip() + except subprocess.CalledProcessError as error: + print(error.output) + sys.exit(error.returncode) + + try: + subprocess.run( [ - "git", - "log", - "-1", - "--format=tformat:%cd", - f"--date=format:{mtime_format}", - os.fsdecode(context.checkout), + "tar", + "-c", + "-f", + os.fsdecode(file_path), + "--sort=name", + "--mtime", + mtime, + "--clamp-mtime", + "--owner=0", + "--group=0", + "--numeric-owner", + "--pax-option=exthdr.name=%d/PaxHeaders/%f,delete=atime,delete=ctime", + "--mode=go+u,go-w", + # Explicitly using `-T` because if you don't compress with threads you can't + # uncompress with them and the size difference is negligible when using + # single-threaded compression. + "--use-compress-program", + "xz -T 0", + to_compress.name, ], - env={"TZ": "UTC0"}, + cwd=to_compress.parent, capture_output=True, text=True, check=True, - ).stdout.strip() - - subprocess.run( - [ - "tar", - "-c", - "-f", - os.fsdecode(file_path), - "--sort=name", - "--mtime", - mtime, - "--clamp-mtime", - "--owner=0", - "--group=0", - "--numeric-owner", - "--pax-option=exthdr.name=%d/PaxHeaders/%f,delete=atime,delete=ctime", - "--mode=go+u,go-w", - # Explicitly using `-T` because if you don't compress with threads you can't - # uncompress with them and the size difference is negligible when using - # single-threaded compression. - "--use-compress-program", - "xz -T 0", - to_compress.name, - ], - cwd=to_compress.parent, - capture_output=True, - text=True, - check=True, - ) + ) + except subprocess.CalledProcessError as error: + print(error.output) + sys.exit(error.returncode)