From c9a6ab56cfae90c96c1361314c3c99b32e114446 Mon Sep 17 00:00:00 2001 From: Martin Panter Date: Sat, 10 Oct 2015 01:25:38 +0000 Subject: [PATCH] Issue #24402: Fix input() when stdout.fileno() fails; diagnosed by Eryksun Also factored out some test cases into a new PtyTests class. --- Lib/test/test_builtin.py | 186 +++++++++++++++++++++++---------------- Misc/NEWS | 3 + Python/bltinmodule.c | 4 +- 3 files changed, 116 insertions(+), 77 deletions(-) diff --git a/Lib/test/test_builtin.py b/Lib/test/test_builtin.py index 14366c652d3..da4244fab7a 100644 --- a/Lib/test/test_builtin.py +++ b/Lib/test/test_builtin.py @@ -1134,82 +1134,6 @@ class BuiltinTest(unittest.TestCase): sys.stdout = savestdout fp.close() - @unittest.skipUnless(pty, "the pty and signal modules must be available") - def check_input_tty(self, prompt, terminal_input, stdio_encoding=None): - if not sys.stdin.isatty() or not sys.stdout.isatty(): - self.skipTest("stdin and stdout must be ttys") - r, w = os.pipe() - try: - pid, fd = pty.fork() - except (OSError, AttributeError) as e: - os.close(r) - os.close(w) - self.skipTest("pty.fork() raised {}".format(e)) - if pid == 0: - # Child - try: - # Make sure we don't get stuck if there's a problem - signal.alarm(2) - os.close(r) - # Check the error handlers are accounted for - if stdio_encoding: - sys.stdin = io.TextIOWrapper(sys.stdin.detach(), - encoding=stdio_encoding, - errors='surrogateescape') - sys.stdout = io.TextIOWrapper(sys.stdout.detach(), - encoding=stdio_encoding, - errors='replace') - with open(w, "w") as wpipe: - print("tty =", sys.stdin.isatty() and sys.stdout.isatty(), file=wpipe) - print(ascii(input(prompt)), file=wpipe) - except: - traceback.print_exc() - finally: - # We don't want to return to unittest... - os._exit(0) - # Parent - os.close(w) - os.write(fd, terminal_input + b"\r\n") - # Get results from the pipe - with open(r, "r") as rpipe: - lines = [] - while True: - line = rpipe.readline().strip() - if line == "": - # The other end was closed => the child exited - break - lines.append(line) - # Check the result was got and corresponds to the user's terminal input - if len(lines) != 2: - # Something went wrong, try to get at stderr - with open(fd, "r", encoding="ascii", errors="ignore") as child_output: - self.fail("got %d lines in pipe but expected 2, child output was:\n%s" - % (len(lines), child_output.read())) - os.close(fd) - # Check we did exercise the GNU readline path - self.assertIn(lines[0], {'tty = True', 'tty = False'}) - if lines[0] != 'tty = True': - self.skipTest("standard IO in should have been a tty") - input_result = eval(lines[1]) # ascii() -> eval() roundtrip - if stdio_encoding: - expected = terminal_input.decode(stdio_encoding, 'surrogateescape') - else: - expected = terminal_input.decode(sys.stdin.encoding) # what else? - self.assertEqual(input_result, expected) - - def test_input_tty(self): - # Test input() functionality when wired to a tty (the code path - # is different and invokes GNU readline if available). - self.check_input_tty("prompt", b"quux") - - def test_input_tty_non_ascii(self): - # Check stdin/stdout encoding is used when invoking GNU readline - self.check_input_tty("prompté", b"quux\xe9", "utf-8") - - def test_input_tty_non_ascii_unicode_errors(self): - # Check stdin/stdout error handler is used when invoking GNU readline - self.check_input_tty("prompté", b"quux\xe9", "ascii") - # test_int(): see test_int.py for tests of built-in function int(). def test_repr(self): @@ -1564,6 +1488,116 @@ class BuiltinTest(unittest.TestCase): self.assertRaises(TypeError, tp, 1, 2) self.assertRaises(TypeError, tp, a=1, b=2) +@unittest.skipUnless(pty, "the pty and signal modules must be available") +class PtyTests(unittest.TestCase): + """Tests that use a pseudo terminal to guarantee stdin and stdout are + terminals in the test environment""" + + def fork(self): + try: + return pty.fork() + except (OSError, AttributeError) as e: + self.skipTest("pty.fork() raised {}".format(e)) + + def check_input_tty(self, prompt, terminal_input, stdio_encoding=None): + if not sys.stdin.isatty() or not sys.stdout.isatty(): + self.skipTest("stdin and stdout must be ttys") + r, w = os.pipe() + try: + pid, fd = self.fork() + except: + os.close(r) + os.close(w) + raise + if pid == 0: + # Child + try: + # Make sure we don't get stuck if there's a problem + signal.alarm(2) + os.close(r) + # Check the error handlers are accounted for + if stdio_encoding: + sys.stdin = io.TextIOWrapper(sys.stdin.detach(), + encoding=stdio_encoding, + errors='surrogateescape') + sys.stdout = io.TextIOWrapper(sys.stdout.detach(), + encoding=stdio_encoding, + errors='replace') + with open(w, "w") as wpipe: + print("tty =", sys.stdin.isatty() and sys.stdout.isatty(), file=wpipe) + print(ascii(input(prompt)), file=wpipe) + except: + traceback.print_exc() + finally: + # We don't want to return to unittest... + os._exit(0) + # Parent + os.close(w) + os.write(fd, terminal_input + b"\r\n") + # Get results from the pipe + with open(r, "r") as rpipe: + lines = [] + while True: + line = rpipe.readline().strip() + if line == "": + # The other end was closed => the child exited + break + lines.append(line) + # Check the result was got and corresponds to the user's terminal input + if len(lines) != 2: + # Something went wrong, try to get at stderr + with open(fd, "r", encoding="ascii", errors="ignore") as child_output: + self.fail("got %d lines in pipe but expected 2, child output was:\n%s" + % (len(lines), child_output.read())) + os.close(fd) + # Check we did exercise the GNU readline path + self.assertIn(lines[0], {'tty = True', 'tty = False'}) + if lines[0] != 'tty = True': + self.skipTest("standard IO in should have been a tty") + input_result = eval(lines[1]) # ascii() -> eval() roundtrip + if stdio_encoding: + expected = terminal_input.decode(stdio_encoding, 'surrogateescape') + else: + expected = terminal_input.decode(sys.stdin.encoding) # what else? + self.assertEqual(input_result, expected) + + def test_input_tty(self): + # Test input() functionality when wired to a tty (the code path + # is different and invokes GNU readline if available). + self.check_input_tty("prompt", b"quux") + + def test_input_tty_non_ascii(self): + # Check stdin/stdout encoding is used when invoking GNU readline + self.check_input_tty("prompté", b"quux\xe9", "utf-8") + + def test_input_tty_non_ascii_unicode_errors(self): + # Check stdin/stdout error handler is used when invoking GNU readline + self.check_input_tty("prompté", b"quux\xe9", "ascii") + + def test_input_no_stdout_fileno(self): + # Issue #24402: If stdin is the original terminal but stdout.fileno() + # fails, do not use the original stdout file descriptor + pid, pty = self.fork() + if pid: # Parent process + # Ideally this should read and write concurrently using select() + # or similar, to avoid the possibility of a deadlock. + os.write(pty, b"quux\r") + _, status = os.waitpid(pid, 0) + output = os.read(pty, 3000).decode("ascii", "backslashreplace") + os.close(pty) + self.assertEqual(status, 0, output) + else: # Child process + try: + self.assertTrue(sys.stdin.isatty(), "stdin not a terminal") + sys.stdout = io.StringIO() # Does not support fileno() + input("prompt") + self.assertEqual(sys.stdout.getvalue(), "prompt") + os._exit(0) # Success! + except: + sys.excepthook(*sys.exc_info()) + finally: + os._exit(1) # Failure + class TestSorted(unittest.TestCase): def test_basic(self): diff --git a/Misc/NEWS b/Misc/NEWS index ba7e54d21f1..ef39bb4583e 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -10,6 +10,9 @@ Release date: tba Core and Builtins ----------------- +- Issue #24402: Fix input() to prompt to the redirected stdout when + sys.stdout.fileno() fails. + - Issue #24806: Prevent builtin types that are not allowed to be subclassed from being subclassed through multiple inheritance. diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index 4b4f979169b..aed93e53523 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -1723,8 +1723,10 @@ builtin_input(PyObject *self, PyObject *args) } if (tty) { tmp = _PyObject_CallMethodId(fout, &PyId_fileno, ""); - if (tmp == NULL) + if (tmp == NULL) { PyErr_Clear(); + tty = 0; + } else { fd = PyLong_AsLong(tmp); Py_DECREF(tmp);