https://github.com/python/cpython/commit/49d484e4b1665fcb546075edbee429b8184a9d73
commit: 49d484e4b1665fcb546075edbee429b8184a9d73
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-06-26T14:28:53Z
summary:

gh-152258: Add curses.window.dupwin() (GH-152259)

dupwin() returns a new window that is an independent duplicate of an existing
one -- same size, position, contents and attributes, but with its own cell
buffer, so changes to one do not affect the other.

Co-authored-by: Claude Opus 4.8 <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-06-26-13-40-00.gh-issue-152258.Qe7Lm3.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 69af3b94d72da1..3ac45eafa9c21e 100644
--- a/Doc/library/curses.rst
+++ b/Doc/library/curses.rst
@@ -1192,6 +1192,17 @@ Window objects
    object for the derived window.
 
 
+.. method:: window.dupwin()
+
+   Return a new window that is an exact duplicate of the window: it has the 
same
+   size, position, contents and attributes.  Unlike a window created by
+   :meth:`subwin` or :meth:`derwin`, the duplicate is independent of the
+   original -- it has its own cell buffer, so later changes to one do not 
affect
+   the other.
+
+   .. versionadded:: next
+
+
 .. method:: window.echochar(ch[, attr])
 
    Add character *ch* with attribute *attr*, and immediately  call 
:meth:`refresh`
diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst
index 0f01c534d9896f..ca80b0a1227588 100644
--- a/Doc/whatsnew/3.16.rst
+++ b/Doc/whatsnew/3.16.rst
@@ -156,6 +156,10 @@ curses
   accept a :class:`~curses.complexstr`.
   (Contributed by Serhiy Storchaka in :gh:`152233`.)
 
+* Add the :mod:`curses` window method :meth:`~curses.window.dupwin`, which
+  returns a new window that is an independent duplicate of an existing one.
+  (Contributed by Serhiy Storchaka in :gh:`152258`.)
+
 gzip
 ----
 
diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py
index 27a64532b21fd8..deadafc93074e3 100644
--- a/Lib/test/test_curses.py
+++ b/Lib/test/test_curses.py
@@ -218,6 +218,35 @@ def test_subwindows_references(self):
         del win2
         gc_collect()
 
+    def test_dupwin(self):
+        win = curses.newwin(5, 10, 2, 3)
+        win.addstr(0, 0, 'ABCDE')
+        win.addstr(1, 0, 'fghij')
+        dup = win.dupwin()
+        # Same geometry and contents as the original.
+        self.assertEqual(dup.getbegyx(), win.getbegyx())
+        self.assertEqual(dup.getmaxyx(), win.getmaxyx())
+        self.assertEqual(dup.instr(0, 0, 5), b'ABCDE')
+        self.assertEqual(dup.instr(1, 0, 5), b'fghij')
+        # The duplicate is independent, not a subwindow.
+        if hasattr(dup, 'is_subwin'):
+            self.assertIs(dup.is_subwin(), False)
+            self.assertIsNone(dup.getparent())
+        # Changes to one do not affect the other.
+        dup.addstr(0, 0, 'xxxxx')
+        win.addstr(1, 0, 'YYYYY')
+        self.assertEqual(win.instr(0, 0, 5), b'ABCDE')
+        self.assertEqual(dup.instr(0, 0, 5), b'xxxxx')
+        self.assertEqual(dup.instr(1, 0, 5), b'fghij')
+        self.assertEqual(win.instr(1, 0, 5), b'YYYYY')
+        # A subwindow can also be duplicated; the duplicate is independent.
+        sub = win.subwin(3, 5, 2, 3)
+        subdup = sub.dupwin()
+        self.assertEqual(subdup.getmaxyx(), sub.getmaxyx())
+        if hasattr(subdup, 'is_subwin'):
+            self.assertIs(subdup.is_subwin(), False)
+            self.assertIsNone(subdup.getparent())
+
     def test_move_cursor(self):
         stdscr = self.stdscr
         win = stdscr.subwin(10, 15, 2, 5)
diff --git 
a/Misc/NEWS.d/next/Library/2026-06-26-13-40-00.gh-issue-152258.Qe7Lm3.rst 
b/Misc/NEWS.d/next/Library/2026-06-26-13-40-00.gh-issue-152258.Qe7Lm3.rst
new file mode 100644
index 00000000000000..b2a0e776ce9c41
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-06-26-13-40-00.gh-issue-152258.Qe7Lm3.rst
@@ -0,0 +1,2 @@
+Add the :mod:`curses` window method :meth:`~curses.window.dupwin`, which
+returns a new window that is an independent duplicate of an existing one.
diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c
index 1274f291e15f98..537a8c4e913f56 100644
--- a/Modules/_cursesmodule.c
+++ b/Modules/_cursesmodule.c
@@ -41,7 +41,7 @@
   Here's a list of currently unsupported functions:
 
   addchnstr addchstr color_set define_key
-  del_curterm dupwin inchnstr inchstr innstr keyok
+  del_curterm inchnstr inchstr innstr keyok
   mcprint mvaddchnstr mvaddchstr mvcur mvinchnstr
   mvinchstr mvinnstr mmvwaddchnstr mvwaddchstr
   mvwinchnstr mvwinchstr mvwinnstr
@@ -2722,6 +2722,33 @@ _curses_window_derwin_impl(PyCursesWindowObject *self, 
int group_left_1,
     return PyCursesWindow_New(state, win, NULL, self, self->screen);
 }
 
+/*[clinic input]
+_curses.window.dupwin
+
+Create an exact duplicate of the window.
+
+The new window is independent of the original: it has the same size,
+position, contents and attributes, but its own cell buffer, so later
+changes to one do not affect the other.
+[clinic start generated code]*/
+
+static PyObject *
+_curses_window_dupwin_impl(PyCursesWindowObject *self)
+/*[clinic end generated code: output=37d91aa8f88f13d1 input=787301b3799b618e]*/
+{
+    WINDOW *win = dupwin(self->win);
+    if (win == NULL) {
+        curses_window_set_null_error(self, "dupwin", NULL);
+        return NULL;
+    }
+
+    /* The duplicate owns an independent cell buffer (unlike a subwindow), so
+       it has no parent: pass NULL as orig.  Inherit the source encoding and
+       screen so it matches the original. */
+    cursesmodule_state *state = get_cursesmodule_state_by_win(self);
+    return PyCursesWindow_New(state, win, self->encoding, NULL, self->screen);
+}
+
 /*[clinic input]
 _curses.window.echochar
 
@@ -4520,6 +4547,7 @@ static PyMethodDef PyCursesWindow_methods[] = {
      "deleteln($self, /)\n--\n\n"
      "Delete the line under the cursor; move following lines up by one."},
     _CURSES_WINDOW_DERWIN_METHODDEF
+    _CURSES_WINDOW_DUPWIN_METHODDEF
     _CURSES_WINDOW_ECHOCHAR_METHODDEF
     _CURSES_WINDOW_ENCLOSE_METHODDEF
     {"erase", PyCursesWindow_werase, METH_NOARGS,
diff --git a/Modules/clinic/_cursesmodule.c.h b/Modules/clinic/_cursesmodule.c.h
index fc48af636346c2..4f4fda094434be 100644
--- a/Modules/clinic/_cursesmodule.c.h
+++ b/Modules/clinic/_cursesmodule.c.h
@@ -991,6 +991,28 @@ _curses_window_derwin(PyObject *self, PyObject *args)
     return return_value;
 }
 
+PyDoc_STRVAR(_curses_window_dupwin__doc__,
+"dupwin($self, /)\n"
+"--\n"
+"\n"
+"Create an exact duplicate of the window.\n"
+"\n"
+"The new window is independent of the original: it has the same size,\n"
+"position, contents and attributes, but its own cell buffer, so later\n"
+"changes to one do not affect the other.");
+
+#define _CURSES_WINDOW_DUPWIN_METHODDEF    \
+    {"dupwin", (PyCFunction)_curses_window_dupwin, METH_NOARGS, 
_curses_window_dupwin__doc__},
+
+static PyObject *
+_curses_window_dupwin_impl(PyCursesWindowObject *self);
+
+static PyObject *
+_curses_window_dupwin(PyObject *self, PyObject *Py_UNUSED(ignored))
+{
+    return _curses_window_dupwin_impl((PyCursesWindowObject *)self);
+}
+
 PyDoc_STRVAR(_curses_window_echochar__doc__,
 "echochar(ch, [attr])\n"
 "Add character ch with attribute attr, and refresh.\n"
@@ -5400,4 +5422,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=7940d7d4775b58fd input=a9049054013a1b77]*/
+/*[clinic end generated code: output=9d7ca194927796d8 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