From 1f1fc98450a82b87eb7d96179efc84947b80e297 Mon Sep 17 00:00:00 2001 From: Lohitha0-0 Date: Mon, 10 Aug 2026 20:10:26 +0000 Subject: [PATCH 1/2] gh-155499: Fix curses border() and box() rejecting 0 mixed with str/bytes border() and box() raise TypeError when a wide-character (ncursesw) build receives a one-character str/bytes argument alongside an int 0, even though 0 is documented as the sentinel meaning "use the default character" (GH-151758 introduced the stricter type-mixing check but didn't account for this documented case). border() now treats an int argument as acceptable when it is 0. box() is restructured to select each side's cchar_t pointer independently, since it previously required both verch and horch to share the same type. Adds Lib/test/test_curses.py coverage for both functions. --- Lib/test/test_curses.py | 15 ++++++++ ...-08-10-20-05-55.gh-issue-155499._P6SDK.rst | 3 ++ Modules/_cursesmodule.c | 37 ++++++++++++++++--- 3 files changed, 49 insertions(+), 6 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-10-20-05-55.gh-issue-155499._P6SDK.rst diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index f7584a39b182d99..66eed719c018a0b 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -1521,6 +1521,21 @@ def test_borders_and_lines(self): win.box(v, v) self.assertEqual(win.instr(0, 1, 1), b) + def test_border_box_zero_sentinel(self): + # gh-155499: 0 must still mean "use the default character" even + # when mixed with a one-character str/bytes argument. + win = curses.newwin(5, 10, 5, 2) + win.border('|', '|', '-', '-') + win.border('|', '|', '-', '-', 0, 0, 0, 0) + win.border(0, '|', 0, '-', 0, 0, 0, 0) + self.assertRaises(TypeError, win.border, + '|', '|', '-', '-', 1, 0, 0, 0) + win.box('|', 0) + win.box(0, '-') + win.box(0, 0) + self.assertRaises(TypeError, win.box, '|', 1) + self.assertRaises(TypeError, win.box, 1, '-') + def test_unctrl(self): self.assertEqual(curses.unctrl(b'A'), b'A') self.assertEqual(curses.unctrl('A'), b'A') diff --git a/Misc/NEWS.d/next/Library/2026-08-10-20-05-55.gh-issue-155499._P6SDK.rst b/Misc/NEWS.d/next/Library/2026-08-10-20-05-55.gh-issue-155499._P6SDK.rst new file mode 100644 index 000000000000000..55fd8b3227f4fe3 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-10-20-05-55.gh-issue-155499._P6SDK.rst @@ -0,0 +1,3 @@ +:meth:`curses.window.border` and :meth:`curses.window.box` no longer raise +:exc:`TypeError` when ``0`` is mixed with a one-character string or bytes +argument, restoring the documented default-character sentinel behavior. diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 07e924b0fc564bc..0d9cb5bc3377651 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -2711,6 +2711,9 @@ _curses_window_border_impl(PyCursesWindowObject *self, PyObject *ls, else if (types[i] == 2) { wch_p[i] = &wch[i]; } + else if (types[i] == 1 && ch[i] == 0) { + wch_p[i] = NULL; /* int 0 also means "use default" */ + } else { PyErr_SetString(PyExc_TypeError, "border() cannot mix integer or bytes " @@ -2775,15 +2778,37 @@ _curses_window_box_impl(PyCursesWindowObject *self, int group_right_1, } } if (t1 == 2 || t2 == 2) { - if (t1 != 2 || t2 != 2) { - PyErr_SetString(PyExc_TypeError, - "box() cannot mix integer or bytes characters " - "with wide string characters"); - return NULL; + const cchar_t *wch1_p, *wch2_p; + + if (t1 == 2) { + wch1_p = &wch1; + } + else if (t1 == 1 && ch1 == 0) { + wch1_p = NULL; + } + else { + goto mixed_type_error; + } + + if (t2 == 2) { + wch2_p = &wch2; + } + else if (t2 == 1 && ch2 == 0) { + wch2_p = NULL; + } + else { + goto mixed_type_error; } - int rtn = wborder_set(self->win, &wch1, &wch1, &wch2, &wch2, + + int rtn = wborder_set(self->win, wch1_p, wch1_p, wch2_p, wch2_p, NULL, NULL, NULL, NULL); return curses_window_check_err(self, rtn, "wborder_set", "box"); + + mixed_type_error: + PyErr_SetString(PyExc_TypeError, + "box() cannot mix integer or bytes characters " + "with wide string characters"); + return NULL; } #else if (group_right_1) { From 4f8fcd5fde71d2828d19bb360d38df95b416df7c Mon Sep 17 00:00:00 2001 From: Lohitha0-0 Date: Tue, 11 Aug 2026 16:55:46 +0000 Subject: [PATCH 2/2] Simplify border() and box() per review feedback - border(): merge the NULL-default and int-0-default branches into a single condition, as suggested by @picnixz. - box(): replace the goto-based per-side validation with a single upfront check plus ternary pointer assignment, avoiding the extra label. --- Modules/_cursesmodule.c | 41 ++++++++++------------------------------- 1 file changed, 10 insertions(+), 31 deletions(-) diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 0d9cb5bc3377651..3ae0354d48d3b6b 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -2705,15 +2705,12 @@ _curses_window_border_impl(PyCursesWindowObject *self, PyObject *ls, } if (use_wide) { for (i = 0; i < 8; i++) { - if (objs[i] == NULL) { + if (objs[i] == NULL || (types[i] == 1 && ch[i] == 0)) { wch_p[i] = NULL; /* use the default character */ } else if (types[i] == 2) { wch_p[i] = &wch[i]; } - else if (types[i] == 1 && ch[i] == 0) { - wch_p[i] = NULL; /* int 0 also means "use default" */ - } else { PyErr_SetString(PyExc_TypeError, "border() cannot mix integer or bytes " @@ -2778,37 +2775,19 @@ _curses_window_box_impl(PyCursesWindowObject *self, int group_right_1, } } if (t1 == 2 || t2 == 2) { - const cchar_t *wch1_p, *wch2_p; - - if (t1 == 2) { - wch1_p = &wch1; - } - else if (t1 == 1 && ch1 == 0) { - wch1_p = NULL; - } - else { - goto mixed_type_error; - } - - if (t2 == 2) { - wch2_p = &wch2; - } - else if (t2 == 1 && ch2 == 0) { - wch2_p = NULL; - } - else { - goto mixed_type_error; + int t1_ok = (t1 == 2) || (t1 == 1 && ch1 == 0); + int t2_ok = (t2 == 2) || (t2 == 1 && ch2 == 0); + if (!t1_ok || !t2_ok) { + PyErr_SetString(PyExc_TypeError, + "box() cannot mix integer or bytes characters " + "with wide string characters"); + return NULL; } - + const cchar_t *wch1_p = (t1 == 2) ? &wch1 : NULL; + const cchar_t *wch2_p = (t2 == 2) ? &wch2 : NULL; int rtn = wborder_set(self->win, wch1_p, wch1_p, wch2_p, wch2_p, NULL, NULL, NULL, NULL); return curses_window_check_err(self, rtn, "wborder_set", "box"); - - mixed_type_error: - PyErr_SetString(PyExc_TypeError, - "box() cannot mix integer or bytes characters " - "with wide string characters"); - return NULL; } #else if (group_right_1) {