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]
