From 7e3dd1ead9df8e1bf483ac18303897df2d4a9c25 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 27 Jun 2026 01:11:41 +0300 Subject: [PATCH] gh-87904: Report the public module name in curses types and exceptions The curses C types and exceptions now set their tp_name to the public module, so __module__, repr() and help() report curses.window, curses.complexchar, curses.complexstr, curses.screen, curses.error, curses.panel.panel and curses.panel.error instead of the underscore extension modules. This matches the convention used by decimal.Decimal, collections.deque and sqlite3.Error. The types are unchanged objects, still reachable through the _curses and _curses_panel modules. Co-Authored-By: Claude Opus 4.8 --- Lib/test/test_curses.py | 21 ++++++++++++++++++- ...6-06-26-22-06-07.gh-issue-87904.F71C70.rst | 4 ++++ Modules/_curses_panel.c | 6 +++--- Modules/_cursesmodule.c | 10 ++++----- 4 files changed, 32 insertions(+), 9 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-06-26-22-06-07.gh-issue-87904.F71C70.rst diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 7157896d8cbccda..c193c6c447960ae 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -442,7 +442,8 @@ def test_complexchar(self): curses.complexchar('A', curses.A_BOLD)) self.assertNotEqual(curses.complexchar('A'), curses.complexchar('B')) # repr() shows only a non-default attr/pair, and is a constructor call. - ns = {'_curses': sys.modules[type(cc).__module__]} + modname = type(cc).__module__ + ns = {modname: sys.modules[modname]} self.assertNotIn('attr=', repr(curses.complexchar('z'))) self.assertNotIn('pair=', repr(curses.complexchar('z'))) r = repr(curses.complexchar('A', curses.A_BOLD)) @@ -2187,6 +2188,24 @@ def test_has_extended_color_support(self): r = curses.has_extended_color_support() self.assertIsInstance(r, bool) + def test_type_names(self): + # The curses types report their public module rather than the + # underscore extension that implements them. + for name in 'window', 'complexchar', 'complexstr', 'screen', 'error': + tp = getattr(curses, name) + self.assertEqual(tp.__module__, 'curses') + self.assertEqual(tp.__qualname__, name) + self.assertEqual(tp.__name__, name) + + @requires_curses_func('panel') + def test_panel_type_names(self): + import curses.panel + for name in 'panel', 'error': + tp = getattr(curses.panel, name) + self.assertEqual(tp.__module__, 'curses.panel') + self.assertEqual(tp.__qualname__, name) + self.assertEqual(tp.__name__, name) + class TestAscii(unittest.TestCase): diff --git a/Misc/NEWS.d/next/Library/2026-06-26-22-06-07.gh-issue-87904.F71C70.rst b/Misc/NEWS.d/next/Library/2026-06-26-22-06-07.gh-issue-87904.F71C70.rst new file mode 100644 index 000000000000000..09bd5a06836df7d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-26-22-06-07.gh-issue-87904.F71C70.rst @@ -0,0 +1,4 @@ +The :mod:`curses` types and exceptions now report their public module in +:attr:`~type.__module__`, :func:`repr` and :func:`help` -- for example +``curses.window`` instead of ``_curses.window`` and ``curses.panel.error`` +instead of ``_curses_panel.error``. diff --git a/Modules/_curses_panel.c b/Modules/_curses_panel.c index c192ce5f05452fe..742a3310bc3528a 100644 --- a/Modules/_curses_panel.c +++ b/Modules/_curses_panel.c @@ -687,7 +687,7 @@ static PyType_Slot PyCursesPanel_Type_slots[] = { }; static PyType_Spec PyCursesPanel_Type_spec = { - .name = "_curses_panel.panel", + .name = "curses.panel.panel", .basicsize = sizeof(PyCursesPanelObject), .flags = ( Py_TPFLAGS_DEFAULT @@ -826,9 +826,9 @@ _curses_panel_exec(PyObject *mod) return -1; } - /* For exception _curses_panel.error */ + /* For exception curses.panel.error */ state->error = PyErr_NewExceptionWithDoc( - "_curses_panel.error", + "curses.panel.error", "Exception raised when a curses panel library function returns an error.", NULL, NULL); diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 3d6748340930ee8..cfda2d0cd919ae5 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -4390,7 +4390,7 @@ static PyType_Slot PyCursesComplexChar_Type_slots[] = { }; static PyType_Spec PyCursesComplexChar_Type_spec = { - .name = "_curses.complexchar", + .name = "curses.complexchar", .basicsize = sizeof(PyCursesComplexCharObject), .flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_IMMUTABLETYPE @@ -4415,7 +4415,7 @@ static PyType_Slot PyCursesComplexStr_Type_slots[] = { }; static PyType_Spec PyCursesComplexStr_Type_spec = { - .name = "_curses.complexstr", + .name = "curses.complexstr", .basicsize = offsetof(PyCursesComplexStrObject, cells), .itemsize = sizeof(cchar_t), .flags = Py_TPFLAGS_DEFAULT @@ -4778,7 +4778,7 @@ static PyType_Slot PyCursesWindow_Type_slots[] = { }; static PyType_Spec PyCursesWindow_Type_spec = { - .name = "_curses.window", + .name = "curses.window", .basicsize = sizeof(PyCursesWindowObject), .flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_DISALLOW_INSTANTIATION @@ -4954,7 +4954,7 @@ static PyType_Slot PyCursesScreen_Type_slots[] = { }; static PyType_Spec PyCursesScreen_Type_spec = { - .name = "_curses.screen", + .name = "curses.screen", .basicsize = sizeof(PyCursesScreenObject), .flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_DISALLOW_INSTANTIATION @@ -8042,7 +8042,7 @@ cursesmodule_exec(PyObject *module) /* For exception curses.error */ state->error = PyErr_NewExceptionWithDoc( - "_curses.error", + "curses.error", "Exception raised when a curses library function returns an error.", NULL, NULL); if (state->error == NULL) {