https://github.com/python/cpython/commit/a85e73b322c80d46537b6ca03bdf6c9cce1d7cea
commit: a85e73b322c80d46537b6ca03bdf6c9cce1d7cea
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-06-26T16:36:31Z
summary:

gh-152260: Add curses.scr_dump(), scr_restore(), scr_init() and scr_set() 
(GH-152261)

These module-level functions write the whole virtual screen to a file and
load it back -- the screen-wide counterpart of window.putwin()/getwin().
The filename argument accepts a string or a path-like object.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-06-26-15-10-00.gh-issue-152260.Rk4nP9.rst
M Doc/library/curses.rst
M Doc/whatsnew/3.16.rst
M Lib/test/test_curses.py
M Modules/_cursesmodule.c
M Modules/clinic/_cursesmodule.c.h

diff --git a/Doc/library/curses.rst b/Doc/library/curses.rst
index 3ac45eafa9c21e..ba4fe55e4a22c6 100644
--- a/Doc/library/curses.rst
+++ b/Doc/library/curses.rst
@@ -696,6 +696,40 @@ The module :mod:`!curses` defines the following functions:
    Save the current state of the terminal modes in a buffer, usable by
    :func:`resetty`.
 
+.. function:: scr_dump(filename)
+
+   Write the current contents of the virtual screen to *filename*, which may be
+   a string or a :term:`path-like object`.  The file can later be read by
+   :func:`scr_restore`, :func:`scr_init` or :func:`scr_set`.  This is the
+   whole-screen counterpart of :meth:`window.putwin`.
+
+   .. versionadded:: next
+
+.. function:: scr_restore(filename)
+
+   Set the virtual screen to the contents of *filename*, which must have been
+   written by :func:`scr_dump`.  The next call to :func:`doupdate` or
+   :meth:`window.refresh` restores the screen to those contents.
+
+   .. versionadded:: next
+
+.. function:: scr_init(filename)
+
+   Initialize the assumed contents of the terminal from *filename*, which must
+   have been written by :func:`scr_dump`.  Use it when the terminal already
+   displays those contents, for example after another program has drawn the
+   screen, so that curses does not redraw what is already there.
+
+   .. versionadded:: next
+
+.. function:: scr_set(filename)
+
+   Use *filename*, which must have been written by :func:`scr_dump`, as both
+   the virtual screen and the assumed terminal contents.  This combines the
+   effects of :func:`scr_restore` and :func:`scr_init`.
+
+   .. versionadded:: next
+
 .. function:: get_escdelay()
 
    Retrieves the value set by :func:`set_escdelay`.
diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst
index ca80b0a1227588..8dfd3bf8169558 100644
--- a/Doc/whatsnew/3.16.rst
+++ b/Doc/whatsnew/3.16.rst
@@ -160,6 +160,11 @@ curses
   returns a new window that is an independent duplicate of an existing one.
   (Contributed by Serhiy Storchaka in :gh:`152258`.)
 
+* Add the :mod:`curses` functions :func:`~curses.scr_dump`,
+  :func:`~curses.scr_restore`, :func:`~curses.scr_init` and
+  :func:`~curses.scr_set`, which dump the whole screen to a file and restore 
it.
+  (Contributed by Serhiy Storchaka in :gh:`152260`.)
+
 gzip
 ----
 
diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py
index ba259ae0d1ce36..7157896d8cbccd 100644
--- a/Lib/test/test_curses.py
+++ b/Lib/test/test_curses.py
@@ -1116,6 +1116,43 @@ def test_putwin(self):
             self.assertEqual(win.getmaxyx(), (5, 12))
             self.assertEqual(win.instr(2, 0), b' Lorem ipsum')
 
+    def test_scr_dump(self):
+        # Test scr_dump(), scr_restore(), scr_init() and scr_set().
+        # scr_dump() writes the virtual screen to a named file; the other three
+        # functions load it back.  The dumped image is internal curses state,
+        # not a window, so the round-trip is checked by comparing dump files
+        # rather than reading cells.
+        stdscr = self.stdscr
+        stdscr.erase()
+        stdscr.addstr(0, 0, 'screen dump test')
+        stdscr.refresh()
+        with tempfile.TemporaryDirectory() as d:
+            dump = os.path.join(d, 'dump')
+            self.assertIsNone(curses.scr_dump(dump))
+            # Dumping the same screen again is deterministic.
+            dump2 = os.path.join(d, 'dump2')
+            curses.scr_dump(dump2)
+            with open(dump, 'rb') as f1, open(dump2, 'rb') as f2:
+                self.assertEqual(f1.read(), f2.read())
+            # scr_restore() reloads that virtual screen, so dumping it again
+            # reproduces the original file even after the screen has changed.
+            stdscr.erase()
+            stdscr.addstr(0, 0, 'something else')
+            stdscr.refresh()
+            self.assertIsNone(curses.scr_restore(dump))
+            restored = os.path.join(d, 'restored')
+            curses.scr_dump(restored)
+            with open(dump, 'rb') as f1, open(restored, 'rb') as f2:
+                self.assertEqual(f1.read(), f2.read())
+            # scr_init() and scr_set() accept a dump file and return None.
+            self.assertIsNone(curses.scr_init(dump))
+            self.assertIsNone(curses.scr_set(dump))
+            # A bytes (path-like) filename is accepted too.
+            curses.scr_dump(os.fsencode(dump))
+            # Restoring from a missing file is an error.
+            self.assertRaises(curses.error,
+                              curses.scr_restore, os.path.join(d, 'nope'))
+
     def test_borders_and_lines(self):
         win = curses.newwin(5, 10, 5, 2)
         win.border('|', '!', '-', '_',
diff --git 
a/Misc/NEWS.d/next/Library/2026-06-26-15-10-00.gh-issue-152260.Rk4nP9.rst 
b/Misc/NEWS.d/next/Library/2026-06-26-15-10-00.gh-issue-152260.Rk4nP9.rst
new file mode 100644
index 00000000000000..8c396009815399
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-06-26-15-10-00.gh-issue-152260.Rk4nP9.rst
@@ -0,0 +1,3 @@
+Add the :mod:`curses` functions :func:`~curses.scr_dump`,
+:func:`~curses.scr_restore`, :func:`~curses.scr_init` and
+:func:`~curses.scr_set`, which dump the whole screen to a file and restore it.
diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c
index 537a8c4e913f56..3d6748340930ee 100644
--- a/Modules/_cursesmodule.c
+++ b/Modules/_cursesmodule.c
@@ -45,8 +45,8 @@
   mcprint mvaddchnstr mvaddchstr mvcur mvinchnstr
   mvinchstr mvinnstr mmvwaddchnstr mvwaddchstr
   mvwinchnstr mvwinchstr mvwinnstr
-  restartterm ripoffline scr_dump
-  scr_init scr_restore scr_set scrl set_curterm setterm
+  restartterm ripoffline
+  scrl set_curterm setterm
   tgetent tgetflag tgetnum tgetstr tgoto timeout tputs
   vidattr vidputs waddchnstr waddchstr
   wcolor_set winchnstr winchstr winnstr wmouse_trafo wscrl
@@ -5039,6 +5039,22 @@ static PyType_Spec PyCursesScreen_Type_spec = {
   Py_RETURN_NONE;                           \
 }
 
+/*
+ * Function body for a module function that dumps or restores the whole screen
+ * through a file named by a single filesystem-path argument (filename).
+ */
+#define ScreenDumpFunctionBody(X)                       \
+{                                                       \
+    PyCursesStatefulInitialised(module);                \
+    PyObject *bytes;                                    \
+    if (!PyUnicode_FSConverter(filename, &bytes)) {     \
+        return NULL;                                    \
+    }                                                   \
+    int rtn = X(PyBytes_AS_STRING(bytes));              \
+    Py_DECREF(bytes);                                   \
+    return curses_check_err(module, rtn, # X, NULL);    \
+}
+
 /*********************************************************************
  Global Functions
 **********************************************************************/
@@ -5612,6 +5628,77 @@ _curses_getwin(PyObject *module, PyObject *file)
     return res;
 }
 
+/*[clinic input]
+_curses.scr_dump
+
+    filename: object
+        The file to write to.
+    /
+
+Write the current contents of the virtual screen to a file.
+
+The file can later be used to restore the screen with scr_restore(),
+scr_init() or scr_set().
+[clinic start generated code]*/
+
+static PyObject *
+_curses_scr_dump(PyObject *module, PyObject *filename)
+/*[clinic end generated code: output=4425cfa505ac9577 input=358db4b370975345]*/
+ScreenDumpFunctionBody(scr_dump)
+
+/*[clinic input]
+_curses.scr_restore
+
+    filename: object
+        The file to read from.
+    /
+
+Set the virtual screen to the contents of a file made by scr_dump().
+
+The next call to doupdate() or refresh() restores the screen to those
+contents.
+[clinic start generated code]*/
+
+static PyObject *
+_curses_scr_restore(PyObject *module, PyObject *filename)
+/*[clinic end generated code: output=71d669fb560fa57b input=30b1d6b2c328dd55]*/
+ScreenDumpFunctionBody(scr_restore)
+
+/*[clinic input]
+_curses.scr_init
+
+    filename: object
+        The file to read from.
+    /
+
+Initialize the assumed terminal contents from a scr_dump() file.
+
+Use it as what the terminal currently displays, for example after
+another program has drawn the screen.
+[clinic start generated code]*/
+
+static PyObject *
+_curses_scr_init(PyObject *module, PyObject *filename)
+/*[clinic end generated code: output=2e861d381d710419 input=81c45e4702124ef6]*/
+ScreenDumpFunctionBody(scr_init)
+
+/*[clinic input]
+_curses.scr_set
+
+    filename: object
+        The file to read from.
+    /
+
+Use a scr_dump() file as both the virtual screen and the terminal.
+
+This combines the effects of scr_restore() and scr_init().
+[clinic start generated code]*/
+
+static PyObject *
+_curses_scr_set(PyObject *module, PyObject *filename)
+/*[clinic end generated code: output=6056fdec12c5935f input=d248c20543cc289b]*/
+ScreenDumpFunctionBody(scr_set)
+
 /*[clinic input]
 _curses.halfdelay
 
@@ -7715,6 +7802,10 @@ static PyMethodDef cursesmodule_methods[] = {
     _CURSES_RESIZETERM_METHODDEF
     _CURSES_RESIZE_TERM_METHODDEF
     _CURSES_SAVETTY_METHODDEF
+    _CURSES_SCR_DUMP_METHODDEF
+    _CURSES_SCR_INIT_METHODDEF
+    _CURSES_SCR_RESTORE_METHODDEF
+    _CURSES_SCR_SET_METHODDEF
 #if defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS >= 20081102
     _CURSES_GET_ESCDELAY_METHODDEF
     _CURSES_SET_ESCDELAY_METHODDEF
diff --git a/Modules/clinic/_cursesmodule.c.h b/Modules/clinic/_cursesmodule.c.h
index 4f4fda094434be..8fbcf1d99bbbed 100644
--- a/Modules/clinic/_cursesmodule.c.h
+++ b/Modules/clinic/_cursesmodule.c.h
@@ -3017,6 +3017,65 @@ PyDoc_STRVAR(_curses_getwin__doc__,
 #define _CURSES_GETWIN_METHODDEF    \
     {"getwin", (PyCFunction)_curses_getwin, METH_O, _curses_getwin__doc__},
 
+PyDoc_STRVAR(_curses_scr_dump__doc__,
+"scr_dump($module, filename, /)\n"
+"--\n"
+"\n"
+"Write the current contents of the virtual screen to a file.\n"
+"\n"
+"  filename\n"
+"    The file to write to.\n"
+"\n"
+"The file can later be used to restore the screen with scr_restore(),\n"
+"scr_init() or scr_set().");
+
+#define _CURSES_SCR_DUMP_METHODDEF    \
+    {"scr_dump", (PyCFunction)_curses_scr_dump, METH_O, 
_curses_scr_dump__doc__},
+
+PyDoc_STRVAR(_curses_scr_restore__doc__,
+"scr_restore($module, filename, /)\n"
+"--\n"
+"\n"
+"Set the virtual screen to the contents of a file made by scr_dump().\n"
+"\n"
+"  filename\n"
+"    The file to read from.\n"
+"\n"
+"The next call to doupdate() or refresh() restores the screen to those\n"
+"contents.");
+
+#define _CURSES_SCR_RESTORE_METHODDEF    \
+    {"scr_restore", (PyCFunction)_curses_scr_restore, METH_O, 
_curses_scr_restore__doc__},
+
+PyDoc_STRVAR(_curses_scr_init__doc__,
+"scr_init($module, filename, /)\n"
+"--\n"
+"\n"
+"Initialize the assumed terminal contents from a scr_dump() file.\n"
+"\n"
+"  filename\n"
+"    The file to read from.\n"
+"\n"
+"Use it as what the terminal currently displays, for example after\n"
+"another program has drawn the screen.");
+
+#define _CURSES_SCR_INIT_METHODDEF    \
+    {"scr_init", (PyCFunction)_curses_scr_init, METH_O, 
_curses_scr_init__doc__},
+
+PyDoc_STRVAR(_curses_scr_set__doc__,
+"scr_set($module, filename, /)\n"
+"--\n"
+"\n"
+"Use a scr_dump() file as both the virtual screen and the terminal.\n"
+"\n"
+"  filename\n"
+"    The file to read from.\n"
+"\n"
+"This combines the effects of scr_restore() and scr_init().");
+
+#define _CURSES_SCR_SET_METHODDEF    \
+    {"scr_set", (PyCFunction)_curses_scr_set, METH_O, _curses_scr_set__doc__},
+
 PyDoc_STRVAR(_curses_halfdelay__doc__,
 "halfdelay($module, tenths, /)\n"
 "--\n"
@@ -5422,4 +5481,4 @@ _curses_has_extended_color_support(PyObject *module, 
PyObject *Py_UNUSED(ignored
 #ifndef _CURSES_ASSUME_DEFAULT_COLORS_METHODDEF
     #define _CURSES_ASSUME_DEFAULT_COLORS_METHODDEF
 #endif /* !defined(_CURSES_ASSUME_DEFAULT_COLORS_METHODDEF) */
-/*[clinic end generated code: output=9d7ca194927796d8 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=864fa5c0f22fcad3 input=a9049054013a1b77]*/

_______________________________________________
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