From 9438b7d7e36e2d5a688bb711c168fb193524a3c1 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 25 Apr 2023 10:57:30 -0400 Subject: [PATCH 1/4] GH-103929: handle long input lines with PyOS_InputHook If: - stdin is in line buffer mode - the readline module is not loaded - not on windows - a GUI toolkit has installd PyOS_InputHook Then, if the user enters lines longer than 98 characters into `input`: - user calls `input(...)` - user types/pastes a line longer than 98 characters + 1 newline - PyOS_InputHook returns - the first 99 characters are returned - the last character is not a new line so we go back to my_fgets - the PyOS_InputHook blocks because despite there being value in the buffer, stdin does not flag itself as ready to be read again - user hits enter again and to finish reading the input - the extra new line comes out the next time the user calls input This fixes this bug by passing the currently read number of characters to `my_fgets` to skip the input hook when reading out a long buffer. closes #103929 --- Doc/c-api/veryhigh.rst | 2 ++ Parser/myreadline.c | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Doc/c-api/veryhigh.rst b/Doc/c-api/veryhigh.rst index 6256bf7a1454a9a..eefc6386597b395 100644 --- a/Doc/c-api/veryhigh.rst +++ b/Doc/c-api/veryhigh.rst @@ -151,6 +151,8 @@ the same library that the Python runtime is using. event loops, as done in :file:`Modules/_tkinter.c` in the Python source code. + This function should block until stdin is readable. + .. versionchanged:: 3.12 This function is only called from the :ref:`main interpreter `. diff --git a/Parser/myreadline.c b/Parser/myreadline.c index ee77479ba7bdccb..5fed50136485c79 100644 --- a/Parser/myreadline.c +++ b/Parser/myreadline.c @@ -38,7 +38,7 @@ int (*PyOS_InputHook)(void) = NULL; except if _PyOS_InterruptOccurred() returns true. */ static int -my_fgets(PyThreadState* tstate, char *buf, int len, FILE *fp) +my_fgets(PyThreadState* tstate, char *buf, int len, FILE *fp, int n) { #ifdef MS_WINDOWS HANDLE handle; @@ -53,7 +53,7 @@ my_fgets(PyThreadState* tstate, char *buf, int len, FILE *fp) #endif while (1) { - if (PyOS_InputHook != NULL && + if (PyOS_InputHook != NULL && n == 0 && // GH-104668: See PyOS_ReadlineFunctionPointer's comment below... _Py_IsMainInterpreter(tstate->interp)) { @@ -333,7 +333,7 @@ PyOS_StdioReadline(FILE *sys_stdin, FILE *sys_stdout, const char *prompt) return NULL; } p = pr; - int err = my_fgets(tstate, p + n, (int)incr, sys_stdin); + int err = my_fgets(tstate, p + n, (int)incr, sys_stdin, n); if (err == 1) { // Interrupt PyMem_RawFree(p); From ba854ef170276132c9dbf025507c8cad86e5ed40 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 17 Oct 2025 13:14:41 -0400 Subject: [PATCH 2/4] DOC: add additional qualifier to expected behavior of PyOS_InputHook --- Doc/c-api/veryhigh.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Doc/c-api/veryhigh.rst b/Doc/c-api/veryhigh.rst index eefc6386597b395..01e6826f7674878 100644 --- a/Doc/c-api/veryhigh.rst +++ b/Doc/c-api/veryhigh.rst @@ -151,7 +151,8 @@ the same library that the Python runtime is using. event loops, as done in :file:`Modules/_tkinter.c` in the Python source code. - This function should block until stdin is readable. + This function should block until stdin is readable but may return + early. .. versionchanged:: 3.12 This function is only called from the From 5e443f4591d487d86ab17e105e144c6745ada2fa Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 3 Dec 2025 21:55:28 -0500 Subject: [PATCH 3/4] MNT: move the PyOS_InputHook call up a loop level If we have returned from the input hook then there is something to read in the stdin so read it out to the new line before calling the input hook again. --- Doc/c-api/veryhigh.rst | 3 +-- Parser/myreadline.c | 17 +++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Doc/c-api/veryhigh.rst b/Doc/c-api/veryhigh.rst index 01e6826f7674878..eefc6386597b395 100644 --- a/Doc/c-api/veryhigh.rst +++ b/Doc/c-api/veryhigh.rst @@ -151,8 +151,7 @@ the same library that the Python runtime is using. event loops, as done in :file:`Modules/_tkinter.c` in the Python source code. - This function should block until stdin is readable but may return - early. + This function should block until stdin is readable. .. versionchanged:: 3.12 This function is only called from the diff --git a/Parser/myreadline.c b/Parser/myreadline.c index 5fed50136485c79..7648690991ae362 100644 --- a/Parser/myreadline.c +++ b/Parser/myreadline.c @@ -38,7 +38,7 @@ int (*PyOS_InputHook)(void) = NULL; except if _PyOS_InterruptOccurred() returns true. */ static int -my_fgets(PyThreadState* tstate, char *buf, int len, FILE *fp, int n) +my_fgets(PyThreadState* tstate, char *buf, int len, FILE *fp) { #ifdef MS_WINDOWS HANDLE handle; @@ -53,12 +53,6 @@ my_fgets(PyThreadState* tstate, char *buf, int len, FILE *fp, int n) #endif while (1) { - if (PyOS_InputHook != NULL && n == 0 && - // GH-104668: See PyOS_ReadlineFunctionPointer's comment below... - _Py_IsMainInterpreter(tstate->interp)) - { - (void)(PyOS_InputHook)(); - } errno = 0; clearerr(fp); @@ -313,6 +307,13 @@ PyOS_StdioReadline(FILE *sys_stdin, FILE *sys_stdout, const char *prompt) } fflush(stderr); + if (PyOS_InputHook != NULL && + // GH-104668: See PyOS_ReadlineFunctionPointer's comment below... + _Py_IsMainInterpreter(tstate->interp)) + { + (void)(PyOS_InputHook)(); + } + n = 0; p = NULL; do { @@ -333,7 +334,7 @@ PyOS_StdioReadline(FILE *sys_stdin, FILE *sys_stdout, const char *prompt) return NULL; } p = pr; - int err = my_fgets(tstate, p + n, (int)incr, sys_stdin, n); + int err = my_fgets(tstate, p + n, (int)incr, sys_stdin); if (err == 1) { // Interrupt PyMem_RawFree(p); From d046a67203ae073cac7c1b52aba6c639b9a4c6ae Mon Sep 17 00:00:00 2001 From: Pablo Galindo Salgado Date: Mon, 29 Jun 2026 13:31:37 +0100 Subject: [PATCH 4/4] Fix review comments for GH-103931 --- .../2026-06-29-00-00-00.gh-issue-103929.InputHook.rst | 1 + Parser/myreadline.c | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-06-29-00-00-00.gh-issue-103929.InputHook.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-29-00-00-00.gh-issue-103929.InputHook.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-29-00-00-00.gh-issue-103929.InputHook.rst new file mode 100644 index 000000000000000..ebae4b74a2a6c2d --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-29-00-00-00.gh-issue-103929.InputHook.rst @@ -0,0 +1 @@ +Fix repeated calls to :c:var:`PyOS_InputHook` for long input lines. diff --git a/Parser/myreadline.c b/Parser/myreadline.c index 7648690991ae362..1504577ab11de2c 100644 --- a/Parser/myreadline.c +++ b/Parser/myreadline.c @@ -306,12 +306,12 @@ PyOS_StdioReadline(FILE *sys_stdin, FILE *sys_stdout, const char *prompt) fprintf(stderr, "%s", prompt); } fflush(stderr); - + // Keep this outside my_fgets(): long lines call my_fgets() repeatedly. if (PyOS_InputHook != NULL && // GH-104668: See PyOS_ReadlineFunctionPointer's comment below... _Py_IsMainInterpreter(tstate->interp)) { - (void)(PyOS_InputHook)(); + (void)(PyOS_InputHook)(); } n = 0;