diff --git a/Lib/subprocess.py b/Lib/subprocess.py index 7ae8df154b481f..75fe912c330e95 100644 --- a/Lib/subprocess.py +++ b/Lib/subprocess.py @@ -1245,11 +1245,39 @@ def _check_timeout(self, endtime, orig_timeout, stdout_seq, stderr_seq, """Convenience for checking if a timeout has expired.""" if endtime is None: return + + def translate_newlines_partial_output(data, encoding, errors): + # Handle decoding the data considering it may be truncated + # mid-codepoint (ignore the trailing partial codepoint). + # See https://github.com/python/cpython/issues/87597 + try: + output = self._translate_newlines(data, encoding, errors) + except UnicodeDecodeError as exc: + if exc.end == len(data): + output = self._translate_newlines(data[:exc.start], + encoding, + errors) + else: + raise + return output + if skip_check_and_raise or _time() > endtime: + if stdout_seq is not None: + stdout = b''.join(stdout_seq) + if self.text_mode: + stdout = translate_newlines_partial_output( + stdout, self.stdout.encoding, self.stdout.errors) + else: + stdout = None + if stderr_seq is not None: + stderr = b''.join(stderr_seq) + if self.text_mode: + stderr = translate_newlines_partial_output( + stderr, self.stderr.encoding, self.stderr.errors) + else: + stderr = None raise TimeoutExpired( - self.args, orig_timeout, - output=b''.join(stdout_seq) if stdout_seq else None, - stderr=b''.join(stderr_seq) if stderr_seq else None) + self.args, orig_timeout, output=stdout, stderr=stderr) def wait(self, timeout=None): diff --git a/Lib/test/test_subprocess.py b/Lib/test/test_subprocess.py index f6854922a5b878..413f63a3c9aa31 100644 --- a/Lib/test/test_subprocess.py +++ b/Lib/test/test_subprocess.py @@ -1129,6 +1129,37 @@ def test_universal_newlines_communicate_encodings(self): stdout, stderr = popen.communicate(input='') self.assertEqual(stdout, '1\n2\n3\n4') + @unittest.skipIf(mswindows, "behavior currently not supported on Windows") + def test_universal_newlines_timeout(self): + with self.assertRaises(subprocess.TimeoutExpired) as c: + p = subprocess.run( + [ + sys.executable, "-c", + "import sys, time;" + r"sys.stderr.buffer.write(b'foo \xc2\xa4 bar');" + "sys.stderr.buffer.flush();" + r"sys.stdout.buffer.write(b'foo \xc2');" + "sys.stdout.buffer.flush();" + "time.sleep(10);" + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + timeout=3) + self.assertEqual(c.exception.stdout, "foo ") + self.assertEqual(c.exception.stderr, "foo ยค bar") + + @unittest.skipIf(mswindows, "behavior currently not supported on Windows") + def test_no_output_timeout(self): + with self.assertRaises(subprocess.TimeoutExpired) as c: + p = subprocess.run( + [sys.executable, "-c", "import time; time.sleep(10)"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=0.1) + self.assertEqual(c.exception.stdout, b"") + self.assertEqual(c.exception.stderr, b"") + def test_communicate_errors(self): for errors, expected in [ ('ignore', ''), diff --git a/Misc/NEWS.d/next/Core and Builtins/2022-08-02-18-12-34.gh-issue-87597.UHFR0H.rst b/Misc/NEWS.d/next/Core and Builtins/2022-08-02-18-12-34.gh-issue-87597.UHFR0H.rst new file mode 100644 index 00000000000000..ff0fdb7e04f3b3 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2022-08-02-18-12-34.gh-issue-87597.UHFR0H.rst @@ -0,0 +1 @@ +Store decoded output from ``subprocess.run()`` on ``TimeoutExpired`` exception when using ``text=True`` mode.