Skip to content

Commit 86c90ed

Browse files
gh-152258: Add curses.window.dupwin()
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. Unlike subwin()/derwin(), which share the parent's buffer, the duplicate has no parent. copywin() is not added: its explicit-rectangle copy is already available through the six-coordinate forms of window.overlay() and window.overwrite(). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 285d96d commit 86c90ed

6 files changed

Lines changed: 98 additions & 2 deletions

File tree

Doc/library/curses.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1176,6 +1176,17 @@ Window objects
11761176
object for the derived window.
11771177

11781178

1179+
.. method:: window.dupwin()
1180+
1181+
Return a new window that is an exact duplicate of the window: it has the same
1182+
size, position, contents and attributes. Unlike a window created by
1183+
:meth:`subwin` or :meth:`derwin`, the duplicate is independent of the
1184+
original -- it has its own cell buffer, so later changes to one do not affect
1185+
the other.
1186+
1187+
.. versionadded:: next
1188+
1189+
11791190
.. method:: window.echochar(ch[, attr])
11801191

11811192
Add character *ch* with attribute *attr*, and immediately call :meth:`refresh`

Doc/whatsnew/3.16.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,10 @@ curses
138138
attribute value, and the corresponding ``WA_*`` attribute constants.
139139
(Contributed by Serhiy Storchaka in :gh:`152219`.)
140140

141+
* Add the :mod:`curses` window method :meth:`~curses.window.dupwin`, which
142+
returns a new window that is an independent duplicate of an existing one.
143+
(Contributed by Serhiy Storchaka in :gh:`152258`.)
144+
141145
gzip
142146
----
143147

Lib/test/test_curses.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,35 @@ def test_subwindows_references(self):
218218
del win2
219219
gc_collect()
220220

221+
def test_dupwin(self):
222+
win = curses.newwin(5, 10, 2, 3)
223+
win.addstr(0, 0, 'ABCDE')
224+
win.addstr(1, 0, 'fghij')
225+
dup = win.dupwin()
226+
# Same geometry and contents as the original.
227+
self.assertEqual(dup.getbegyx(), win.getbegyx())
228+
self.assertEqual(dup.getmaxyx(), win.getmaxyx())
229+
self.assertEqual(dup.instr(0, 0, 5), b'ABCDE')
230+
self.assertEqual(dup.instr(1, 0, 5), b'fghij')
231+
# The duplicate is independent, not a subwindow.
232+
if hasattr(dup, 'is_subwin'):
233+
self.assertIs(dup.is_subwin(), False)
234+
self.assertIsNone(dup.getparent())
235+
# Changes to one do not affect the other.
236+
dup.addstr(0, 0, 'xxxxx')
237+
win.addstr(1, 0, 'YYYYY')
238+
self.assertEqual(win.instr(0, 0, 5), b'ABCDE')
239+
self.assertEqual(dup.instr(0, 0, 5), b'xxxxx')
240+
self.assertEqual(dup.instr(1, 0, 5), b'fghij')
241+
self.assertEqual(win.instr(1, 0, 5), b'YYYYY')
242+
# A subwindow can also be duplicated; the duplicate is independent.
243+
sub = win.subwin(3, 5, 2, 3)
244+
subdup = sub.dupwin()
245+
self.assertEqual(subdup.getmaxyx(), sub.getmaxyx())
246+
if hasattr(subdup, 'is_subwin'):
247+
self.assertIs(subdup.is_subwin(), False)
248+
self.assertIsNone(subdup.getparent())
249+
221250
def test_move_cursor(self):
222251
stdscr = self.stdscr
223252
win = stdscr.subwin(10, 15, 2, 5)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add the :mod:`curses` window method :meth:`~curses.window.dupwin`, which
2+
returns a new window that is an independent duplicate of an existing one.

Modules/_cursesmodule.c

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
Here's a list of currently unsupported functions:
4242
4343
addchnstr addchstr color_set define_key
44-
del_curterm dupwin inchnstr inchstr innstr keyok
44+
del_curterm inchnstr inchstr innstr keyok
4545
mcprint mvaddchnstr mvaddchstr mvcur mvinchnstr
4646
mvinchstr mvinnstr mmvwaddchnstr mvwaddchstr
4747
mvwinchnstr mvwinchstr mvwinnstr
@@ -1964,6 +1964,33 @@ _curses_window_derwin_impl(PyCursesWindowObject *self, int group_left_1,
19641964
return PyCursesWindow_New(state, win, NULL, self, self->screen);
19651965
}
19661966

1967+
/*[clinic input]
1968+
_curses.window.dupwin
1969+
1970+
Create an exact duplicate of the window.
1971+
1972+
The new window is independent of the original: it has the same size,
1973+
position, contents and attributes, but its own cell buffer, so later
1974+
changes to one do not affect the other.
1975+
[clinic start generated code]*/
1976+
1977+
static PyObject *
1978+
_curses_window_dupwin_impl(PyCursesWindowObject *self)
1979+
/*[clinic end generated code: output=37d91aa8f88f13d1 input=787301b3799b618e]*/
1980+
{
1981+
WINDOW *win = dupwin(self->win);
1982+
if (win == NULL) {
1983+
curses_window_set_null_error(self, "dupwin", NULL);
1984+
return NULL;
1985+
}
1986+
1987+
/* The duplicate owns an independent cell buffer (unlike a subwindow), so
1988+
it has no parent: pass NULL as orig. Inherit the source encoding and
1989+
screen so it matches the original. */
1990+
cursesmodule_state *state = get_cursesmodule_state_by_win(self);
1991+
return PyCursesWindow_New(state, win, self->encoding, NULL, self->screen);
1992+
}
1993+
19671994
/*[clinic input]
19681995
_curses.window.echochar
19691996
@@ -3553,6 +3580,7 @@ static PyMethodDef PyCursesWindow_methods[] = {
35533580
"deleteln($self, /)\n--\n\n"
35543581
"Delete the line under the cursor; move following lines up by one."},
35553582
_CURSES_WINDOW_DERWIN_METHODDEF
3583+
_CURSES_WINDOW_DUPWIN_METHODDEF
35563584
_CURSES_WINDOW_ECHOCHAR_METHODDEF
35573585
_CURSES_WINDOW_ENCLOSE_METHODDEF
35583586
{"erase", PyCursesWindow_werase, METH_NOARGS,

Modules/clinic/_cursesmodule.c.h

Lines changed: 23 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)