Skip to content

Commit 6fb6465

Browse files
Add screen.close() to break the screen/window reference cycle
A screen and its standard window reference each other, so a screen is only reclaimed by the cyclic garbage collector. screen.close() detaches the standard window -- clearing the cycle and the window so its delwin() is skipped (delscreen() frees it instead) -- letting the screen be released by reference counting. Afterwards screen.stdscr is None and the old window raises curses.error. Use it in the tests instead of an explicit gc.collect(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 03f0d8c commit 6fb6465

3 files changed

Lines changed: 49 additions & 5 deletions

File tree

Doc/library/curses.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1462,6 +1462,20 @@ Screen objects
14621462
.. versionadded:: next
14631463

14641464

1465+
.. method:: screen.close()
1466+
1467+
Detach the screen's standard window,
1468+
breaking the reference cycle between them
1469+
so the screen can be reclaimed promptly instead of waiting for a
1470+
garbage collection.
1471+
Afterwards :attr:`~screen.stdscr` is ``None``
1472+
and the window it returned earlier can no longer be used.
1473+
The screen's resources are released
1474+
once it and all its windows are no longer referenced.
1475+
1476+
.. versionadded:: next
1477+
1478+
14651479
.. attribute:: screen.stdscr
14661480

14671481
The standard :ref:`window <curses-window-objects>` of the screen,

Lib/test/test_curses.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,12 @@ def setUp(self):
124124
infd = stdout_fd
125125
self.screen = curses.newterm(term, stdout_fd, infd)
126126
self.stdscr = self.screen.stdscr
127-
# Drop the screen after the test, and collect the window<->screen
128-
# reference cycle while the screen is still current, so delwin()
129-
# succeeds; collected later, on a non-current screen, it fails
130-
# (unraisable on macOS).
131-
self.addCleanup(gc_collect)
127+
# Close the screen after the test: it breaks the window<->screen
128+
# reference cycle and detaches the standard window so its delwin()
129+
# is skipped. Otherwise the window is collected during a later test
130+
# whose screen is no longer current, and delwin() fails (unraisable
131+
# on macOS).
132+
self.addCleanup(self.screen.close)
132133
self.addCleanup(setattr, self, 'screen', None)
133134
self.addCleanup(setattr, self, 'stdscr', None)
134135
else:
@@ -1869,6 +1870,19 @@ def test_screen_freed(self):
18691870
del a
18701871
gc_collect()
18711872

1873+
def test_close(self):
1874+
s = self.make_pty()
1875+
screen = curses.newterm('xterm', s, s)
1876+
win = screen.stdscr
1877+
self.assertIsInstance(win, curses.window)
1878+
screen.close()
1879+
# After close() the standard window is detached and unusable, and
1880+
# stdscr is None. No reference cycle remains.
1881+
self.assertIsNone(screen.stdscr)
1882+
self.assertRaises(curses.error, win.addstr, 0, 0, 'x')
1883+
# close() is idempotent.
1884+
screen.close()
1885+
18721886
@unittest.skipUnless(hasattr(curses, 'new_prescr'),
18731887
'requires curses.new_prescr()')
18741888
def test_new_prescr(self):

Modules/_cursesmodule.c

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3339,7 +3339,23 @@ PyCursesScreen_use(PyObject *self, PyObject *args, PyObject *kwargs)
33393339
}
33403340
#endif /* HAVE_CURSES_USE_SCREEN */
33413341

3342+
PyDoc_STRVAR(PyCursesScreen_close__doc__,
3343+
"close($self, /)\n--\n\n"
3344+
"Detach the screen's standard window, breaking their reference cycle.\n\n"
3345+
"Afterwards the stdscr attribute is None and the window it returned earlier\n"
3346+
"can no longer be used. The screen is released once it and its windows are\n"
3347+
"no longer referenced.");
3348+
3349+
static PyObject *
3350+
PyCursesScreen_close(PyObject *self, PyObject *Py_UNUSED(ignored))
3351+
{
3352+
(void)PyCursesScreen_clear(self);
3353+
Py_RETURN_NONE;
3354+
}
3355+
33423356
static PyMethodDef PyCursesScreen_methods[] = {
3357+
{"close", PyCursesScreen_close, METH_NOARGS,
3358+
PyCursesScreen_close__doc__},
33433359
#ifdef HAVE_CURSES_USE_SCREEN
33443360
{"use", _PyCFunction_CAST(PyCursesScreen_use),
33453361
METH_VARARGS | METH_KEYWORDS, PyCursesScreen_use__doc__},

0 commit comments

Comments
 (0)