]> granicus.if.org Git - python/commitdiff
Issue #21332: Ensure that ``bufsize=1`` in subprocess.Popen() selects line buffering...
authorAntoine Pitrou <solipsis@pitrou.net>
Sun, 21 Sep 2014 19:10:56 +0000 (21:10 +0200)
committerAntoine Pitrou <solipsis@pitrou.net>
Sun, 21 Sep 2014 19:10:56 +0000 (21:10 +0200)
Doc/library/subprocess.rst
Lib/subprocess.py
Lib/test/test_subprocess.py
Misc/NEWS

index 854993cba0fa0c5a62ded63acda8a447bc858f18..b2238f0a005a427090994cf88094a2ebfa88039d 100644 (file)
@@ -406,12 +406,18 @@ functions.
 
       Read the `Security Considerations`_ section before using ``shell=True``.
 
-   *bufsize* will be supplied as the corresponding argument to the :func:`open`
-   function when creating the stdin/stdout/stderr pipe file objects: :const:`0`
-   means unbuffered (read and write are one system call and can return short),
-   :const:`1` means line buffered, any other positive value means use a buffer
-   of approximately that size.  A negative bufsize (the default) means the
-   system default of io.DEFAULT_BUFFER_SIZE will be used.
+   *bufsize* will be supplied as the corresponding argument to the
+   :func:`open` function when creating the stdin/stdout/stderr pipe
+   file objects:
+
+   - :const:`0` means unbuffered (read and write are one
+     system call and can return short)
+   - :const:`1` means line buffered
+     (only usable if ``universal_newlines=True`` i.e., in a text mode)
+   - any other positive value means use a buffer of approximately that
+     size
+   - negative bufsize (the default) means the system default of
+     io.DEFAULT_BUFFER_SIZE will be used.
 
    .. versionchanged:: 3.3.1
       *bufsize* now defaults to -1 to enable buffering by default to match the
index ddc033a2a71802f9f008b2bfec8669bf0d0d0365..bc0ef0b4bea147cdd1ac2d4dfb771dea77031488 100644 (file)
@@ -837,7 +837,8 @@ class Popen(object):
         if p2cwrite != -1:
             self.stdin = io.open(p2cwrite, 'wb', bufsize)
             if universal_newlines:
-                self.stdin = io.TextIOWrapper(self.stdin, write_through=True)
+                self.stdin = io.TextIOWrapper(self.stdin, write_through=True,
+                                              line_buffering=(bufsize == 1))
         if c2pread != -1:
             self.stdout = io.open(c2pread, 'rb', bufsize)
             if universal_newlines:
index d0ab718646b9b936faf7e397ecf200f013f58c7e..9616da03f338dde5b5a68de91cf2e962752f15d5 100644 (file)
@@ -1008,6 +1008,39 @@ class ProcessTestCase(BaseTestCase):
         p = subprocess.Popen([sys.executable, "-c", "pass"], bufsize=None)
         self.assertEqual(p.wait(), 0)
 
+    def _test_bufsize_equal_one(self, line, expected, universal_newlines):
+        # subprocess may deadlock with bufsize=1, see issue #21332
+        with subprocess.Popen([sys.executable, "-c", "import sys;"
+                               "sys.stdout.write(sys.stdin.readline());"
+                               "sys.stdout.flush()"],
+                              stdin=subprocess.PIPE,
+                              stdout=subprocess.PIPE,
+                              stderr=subprocess.DEVNULL,
+                              bufsize=1,
+                              universal_newlines=universal_newlines) as p:
+            p.stdin.write(line) # expect that it flushes the line in text mode
+            os.close(p.stdin.fileno()) # close it without flushing the buffer
+            read_line = p.stdout.readline()
+            try:
+                p.stdin.close()
+            except OSError:
+                pass
+            p.stdin = None
+        self.assertEqual(p.returncode, 0)
+        self.assertEqual(read_line, expected)
+
+    def test_bufsize_equal_one_text_mode(self):
+        # line is flushed in text mode with bufsize=1.
+        # we should get the full line in return
+        line = "line\n"
+        self._test_bufsize_equal_one(line, line, universal_newlines=True)
+
+    def test_bufsize_equal_one_binary_mode(self):
+        # line is not flushed in binary mode with bufsize=1.
+        # we should get empty response
+        line = b'line' + os.linesep.encode() # assume ascii-based locale
+        self._test_bufsize_equal_one(line, b'', universal_newlines=False)
+
     def test_leaking_fds_on_error(self):
         # see bug #5179: Popen leaks file descriptors to PIPEs if
         # the child fails to execute; this will eventually exhaust
index 356021291a3598070549e27e989d9b81542f56b2..9e4c55ed43b9563df735bbc6088306817230bd66 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -32,6 +32,9 @@ Core and Builtins
 Library
 -------
 
+- Issue #21332: Ensure that ``bufsize=1`` in subprocess.Popen() selects
+  line buffering, rather than block buffering.  Patch by Akira Li.
+
 - Issue #21091: Fix API bug: email.message.EmailMessage.is_attachment is now
   a method.  Since EmailMessage is provisional, we can change the API in a
   maintenance release, but we use a trick to remain backward compatible with