From 7d979cf3ae942ce637a4acb1b1963daf164f0287 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 20 May 2025 13:13:59 +0200 Subject: [PATCH 1/6] add helper for handling input errors --- Modules/_cursesmodule.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 2e6ec822e2d5b6..1ac2ee037c4cf1 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -1659,6 +1659,19 @@ _curses_window_getbkgd_impl(PyCursesWindowObject *self) return (long) getbkgd(self->win); } +static PyObject * +curses_check_signals_on_input_error(PyCursesWindowObject *self, + const char *curses_funcname, + const char *python_funcname) +{ + if (!PyErr_CheckSignals() && !PyErr_Occurred()) { + cursesmodule_state *state = get_cursesmodule_state_by_win(self); + PyErr_Format(state->error, "%s() (called by %s()): no input", + curses_funcname, python_funcname); + } + return NULL; +} + /*[clinic input] _curses.window.getch -> int From 81056a3ce8dc05937738e60ed84b48fe44b0bb18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 20 May 2025 13:14:31 +0200 Subject: [PATCH 2/6] handle input errors in `getch`, `get_wch` and `getkey` --- Modules/_cursesmodule.c | 33 ++++++++++++++------------------ Modules/clinic/_cursesmodule.c.h | 11 +++-------- 2 files changed, 17 insertions(+), 27 deletions(-) diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 1ac2ee037c4cf1..49c31585b69e3f 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -1673,7 +1673,7 @@ curses_check_signals_on_input_error(PyCursesWindowObject *self, } /*[clinic input] -_curses.window.getch -> int +_curses.window.getch [ y: int @@ -1690,10 +1690,10 @@ keypad keys and so on return numbers higher than 256. In no-delay mode, -1 is returned if there is no input, else getch() waits until a key is pressed. [clinic start generated code]*/ -static int +static PyObject * _curses_window_getch_impl(PyCursesWindowObject *self, int group_right_1, int y, int x) -/*[clinic end generated code: output=980aa6af0c0ca387 input=bb24ebfb379f991f]*/ +/*[clinic end generated code: output=e1639e87d545e676 input=73f350336b1ee8c8]*/ { int rtn; @@ -1706,7 +1706,12 @@ _curses_window_getch_impl(PyCursesWindowObject *self, int group_right_1, } Py_END_ALLOW_THREADS - return rtn; + if (rtn == ERR) { + /* wgetch() returns ERR in nodelay mode */ + const char *funcname = group_right_1 ? "mvwgetch" : "wgetch"; + return curses_check_signals_on_input_error(self, funcname, "getch"); + } + return PyLong_FromLong(rtn); } /*[clinic input] @@ -1744,14 +1749,9 @@ _curses_window_getkey_impl(PyCursesWindowObject *self, int group_right_1, Py_END_ALLOW_THREADS if (rtn == ERR) { - /* getch() returns ERR in nodelay mode */ - PyErr_CheckSignals(); - if (!PyErr_Occurred()) { - cursesmodule_state *state = get_cursesmodule_state_by_win(self); - const char *funcname = group_right_1 ? "mvwgetch" : "wgetch"; - PyErr_Format(state->error, "getkey(): %s(): no input", funcname); - } - return NULL; + /* wgetch() returns ERR in nodelay mode */ + const char *funcname = group_right_1 ? "mvwgetch" : "wgetch"; + return curses_check_signals_on_input_error(self, funcname, "getkey"); } else if (rtn <= 255) { #ifdef NCURSES_VERSION_MAJOR #if NCURSES_VERSION_MAJOR*100+NCURSES_VERSION_MINOR <= 507 @@ -1804,14 +1804,9 @@ _curses_window_get_wch_impl(PyCursesWindowObject *self, int group_right_1, Py_END_ALLOW_THREADS if (ct == ERR) { - if (PyErr_CheckSignals()) - return NULL; - - /* get_wch() returns ERR in nodelay mode */ - cursesmodule_state *state = get_cursesmodule_state_by_win(self); + /* wget_wch() returns ERR in nodelay mode */ const char *funcname = group_right_1 ? "mvwget_wch" : "wget_wch"; - PyErr_Format(state->error, "get_wch(): %s(): no input", funcname); - return NULL; + return curses_check_signals_on_input_error(self, funcname, "get_wch"); } if (ct == KEY_CODE_YES) return PyLong_FromLong(rtn); diff --git a/Modules/clinic/_cursesmodule.c.h b/Modules/clinic/_cursesmodule.c.h index 552360eb80a545..a898a7e17cf8d1 100644 --- a/Modules/clinic/_cursesmodule.c.h +++ b/Modules/clinic/_cursesmodule.c.h @@ -768,7 +768,7 @@ PyDoc_STRVAR(_curses_window_getch__doc__, #define _CURSES_WINDOW_GETCH_METHODDEF \ {"getch", (PyCFunction)_curses_window_getch, METH_VARARGS, _curses_window_getch__doc__}, -static int +static PyObject * _curses_window_getch_impl(PyCursesWindowObject *self, int group_right_1, int y, int x); @@ -779,7 +779,6 @@ _curses_window_getch(PyObject *self, PyObject *args) int group_right_1 = 0; int y = 0; int x = 0; - int _return_value; switch (PyTuple_GET_SIZE(args)) { case 0: @@ -794,11 +793,7 @@ _curses_window_getch(PyObject *self, PyObject *args) PyErr_SetString(PyExc_TypeError, "_curses.window.getch requires 0 to 2 arguments"); goto exit; } - _return_value = _curses_window_getch_impl((PyCursesWindowObject *)self, group_right_1, y, x); - if ((_return_value == -1) && PyErr_Occurred()) { - goto exit; - } - return_value = PyLong_FromLong((long)_return_value); + return_value = _curses_window_getch_impl((PyCursesWindowObject *)self, group_right_1, y, x); exit: return return_value; @@ -4440,4 +4435,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=42b2923d88c8d0f6 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=7753612d7613903c input=a9049054013a1b77]*/ From b926e640a1b43ea286cb8d2a040b89d38360cccd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 20 May 2025 14:13:47 +0200 Subject: [PATCH 3/6] do not break stuff! --- Modules/_cursesmodule.c | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 49c31585b69e3f..4c31c368eb1f92 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -1659,19 +1659,6 @@ _curses_window_getbkgd_impl(PyCursesWindowObject *self) return (long) getbkgd(self->win); } -static PyObject * -curses_check_signals_on_input_error(PyCursesWindowObject *self, - const char *curses_funcname, - const char *python_funcname) -{ - if (!PyErr_CheckSignals() && !PyErr_Occurred()) { - cursesmodule_state *state = get_cursesmodule_state_by_win(self); - PyErr_Format(state->error, "%s() (called by %s()): no input", - curses_funcname, python_funcname); - } - return NULL; -} - /*[clinic input] _curses.window.getch @@ -1707,13 +1694,31 @@ _curses_window_getch_impl(PyCursesWindowObject *self, int group_right_1, Py_END_ALLOW_THREADS if (rtn == ERR) { - /* wgetch() returns ERR in nodelay mode */ - const char *funcname = group_right_1 ? "mvwgetch" : "wgetch"; - return curses_check_signals_on_input_error(self, funcname, "getch"); + // We suppress ERR returned by wgetch() in nodelay mode + // after we handled possible interruption signals. + if (PyErr_CheckSignals()) { + return NULL; + } + // ERR is an implementation detail, so to be on the safe side, + // we forcibly set the return value to -1 as documented above. + rtn = -1; } return PyLong_FromLong(rtn); } +static PyObject * +curses_check_signals_on_input_error(PyCursesWindowObject *self, + const char *curses_funcname, + const char *python_funcname) +{ + if (!PyErr_CheckSignals() && !PyErr_Occurred()) { + cursesmodule_state *state = get_cursesmodule_state_by_win(self); + PyErr_Format(state->error, "%s() (called by %s()): no input", + curses_funcname, python_funcname); + } + return NULL; +} + /*[clinic input] _curses.window.getkey From 3a1b29cee8478e71acb45ccbd98ba9742a0fab63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 24 May 2025 13:10:38 +0200 Subject: [PATCH 4/6] blurb --- .../next/Library/2025-05-24-13-10-35.gh-issue-134210.0IuMY2.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-05-24-13-10-35.gh-issue-134210.0IuMY2.rst diff --git a/Misc/NEWS.d/next/Library/2025-05-24-13-10-35.gh-issue-134210.0IuMY2.rst b/Misc/NEWS.d/next/Library/2025-05-24-13-10-35.gh-issue-134210.0IuMY2.rst new file mode 100644 index 00000000000000..57a3cbc5f87740 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-05-24-13-10-35.gh-issue-134210.0IuMY2.rst @@ -0,0 +1,2 @@ +:func:`curses.window.getch` now correctly handle signals. Patch by Bénédikt +Tran. From fd468029364b858a14f61c9974ddd0e12c084767 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 25 May 2025 10:03:02 +0200 Subject: [PATCH 5/6] revert side modifications --- Modules/_cursesmodule.c | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 4c31c368eb1f92..4cffd46430377e 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -1706,19 +1706,6 @@ _curses_window_getch_impl(PyCursesWindowObject *self, int group_right_1, return PyLong_FromLong(rtn); } -static PyObject * -curses_check_signals_on_input_error(PyCursesWindowObject *self, - const char *curses_funcname, - const char *python_funcname) -{ - if (!PyErr_CheckSignals() && !PyErr_Occurred()) { - cursesmodule_state *state = get_cursesmodule_state_by_win(self); - PyErr_Format(state->error, "%s() (called by %s()): no input", - curses_funcname, python_funcname); - } - return NULL; -} - /*[clinic input] _curses.window.getkey @@ -1754,9 +1741,14 @@ _curses_window_getkey_impl(PyCursesWindowObject *self, int group_right_1, Py_END_ALLOW_THREADS if (rtn == ERR) { - /* wgetch() returns ERR in nodelay mode */ - const char *funcname = group_right_1 ? "mvwgetch" : "wgetch"; - return curses_check_signals_on_input_error(self, funcname, "getkey"); + /* getch() returns ERR in nodelay mode */ + PyErr_CheckSignals(); + if (!PyErr_Occurred()) { + cursesmodule_state *state = get_cursesmodule_state_by_win(self); + const char *funcname = group_right_1 ? "mvwgetch" : "wgetch"; + PyErr_Format(state->error, "getkey(): %s(): no input", funcname); + } + return NULL; } else if (rtn <= 255) { #ifdef NCURSES_VERSION_MAJOR #if NCURSES_VERSION_MAJOR*100+NCURSES_VERSION_MINOR <= 507 @@ -1809,9 +1801,14 @@ _curses_window_get_wch_impl(PyCursesWindowObject *self, int group_right_1, Py_END_ALLOW_THREADS if (ct == ERR) { - /* wget_wch() returns ERR in nodelay mode */ + if (PyErr_CheckSignals()) + return NULL; + + /* get_wch() returns ERR in nodelay mode */ + cursesmodule_state *state = get_cursesmodule_state_by_win(self); const char *funcname = group_right_1 ? "mvwget_wch" : "wget_wch"; - return curses_check_signals_on_input_error(self, funcname, "get_wch"); + PyErr_Format(state->error, "get_wch(): %s(): no input", funcname); + return NULL; } if (ct == KEY_CODE_YES) return PyLong_FromLong(rtn); From 091ca8f485af76dc8992cb158d69fd80609c5a1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 26 May 2025 17:32:34 +0200 Subject: [PATCH 6/6] Update Misc/NEWS.d/next/Library/2025-05-24-13-10-35.gh-issue-134210.0IuMY2.rst --- .../next/Library/2025-05-24-13-10-35.gh-issue-134210.0IuMY2.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2025-05-24-13-10-35.gh-issue-134210.0IuMY2.rst b/Misc/NEWS.d/next/Library/2025-05-24-13-10-35.gh-issue-134210.0IuMY2.rst index 57a3cbc5f87740..b440e8308db6a2 100644 --- a/Misc/NEWS.d/next/Library/2025-05-24-13-10-35.gh-issue-134210.0IuMY2.rst +++ b/Misc/NEWS.d/next/Library/2025-05-24-13-10-35.gh-issue-134210.0IuMY2.rst @@ -1,2 +1,2 @@ -:func:`curses.window.getch` now correctly handle signals. Patch by Bénédikt +:func:`curses.window.getch` now correctly handles signals. Patch by Bénédikt Tran.