Commit 511b75f3 authored by Jason Madden's avatar Jason Madden

Return the correct value from Popen.communicate() on Py3/text mode

With tests, verified against the stdlib.

Fixes a todo, and fixes #939.
parent 5164f435
...@@ -46,6 +46,11 @@ ...@@ -46,6 +46,11 @@
- Linux CI now tests on PyPy3 3.5-5.8.0, updated from PyPy3 3.5-5.7.1. - Linux CI now tests on PyPy3 3.5-5.8.0, updated from PyPy3 3.5-5.7.1.
See :issue:`1001`. See :issue:`1001`.
- :meth:`gevent.subprocess.Popen.communicate` returns the correct type
of str (not bytes) in universal newline mode under Python 3, or when
an encoding has been specified. Initial patch in :pr:`939` by
William Grzybowski.
1.2.2 (2017-06-05) 1.2.2 (2017-06-05)
================== ==================
......
...@@ -387,6 +387,10 @@ class Popen(object): ...@@ -387,6 +387,10 @@ class Popen(object):
restricted to Python 3. restricted to Python 3.
""" """
# The value returned from communicate() when there was nothing to read.
# Changes if we're in text mode or universal newlines mode.
_communicate_empty_value = b''
def __init__(self, args, bufsize=None, executable=None, def __init__(self, args, bufsize=None, executable=None,
stdin=None, stdout=None, stderr=None, stdin=None, stdout=None, stderr=None,
preexec_fn=None, close_fds=_PLATFORM_DEFAULT_CLOSE_FDS, shell=False, preexec_fn=None, close_fds=_PLATFORM_DEFAULT_CLOSE_FDS, shell=False,
...@@ -504,6 +508,11 @@ class Popen(object): ...@@ -504,6 +508,11 @@ class Popen(object):
errread = msvcrt.open_osfhandle(errread.Detach(), 0) errread = msvcrt.open_osfhandle(errread.Detach(), 0)
text_mode = PY3 and (self.encoding or self.errors or universal_newlines) text_mode = PY3 and (self.encoding or self.errors or universal_newlines)
if text_mode or universal_newlines:
# Always a native str in universal_newlines mode, even when that
# str type is bytes. Additionally, text_mode is only true under
# Python 3, so it's actually a unicode str
self._communicate_empty_value = ''
if p2cwrite is not None: if p2cwrite is not None:
if PY3 and text_mode: if PY3 and text_mode:
...@@ -643,31 +652,33 @@ class Popen(object): ...@@ -643,31 +652,33 @@ class Popen(object):
# that no output should be lost in the event of a timeout.) Instead, we're # that no output should be lost in the event of a timeout.) Instead, we're
# watching for the exception and ignoring it. It's not elegant, # watching for the exception and ignoring it. It's not elegant,
# but it works # but it works
if self.stdout: def _make_pipe_reader(pipe_name):
def _read_out(): pipe = getattr(self, pipe_name)
buf_name = '_' + pipe_name + '_buffer'
def _read():
try: try:
data = self.stdout.read() data = pipe.read()
except RuntimeError: except RuntimeError:
return return
if self._stdout_buffer is not None: if not data:
self._stdout_buffer += data return
the_buffer = getattr(self, buf_name)
if the_buffer:
the_buffer.append(data)
else: else:
self._stdout_buffer = data setattr(self, buf_name, [data])
return _read
if self.stdout:
_read_out = _make_pipe_reader('stdout')
stdout = spawn(_read_out) stdout = spawn(_read_out)
greenlets.append(stdout) greenlets.append(stdout)
else: else:
stdout = None stdout = None
if self.stderr: if self.stderr:
def _read_err(): _read_err = _make_pipe_reader('stderr')
try:
data = self.stderr.read()
except RuntimeError:
return
if self._stderr_buffer is not None:
self._stderr_buffer += data
else:
self._stderr_buffer = data
stderr = spawn(_read_err) stderr = spawn(_read_err)
greenlets.append(stderr) greenlets.append(stderr)
else: else:
...@@ -686,25 +697,30 @@ class Popen(object): ...@@ -686,25 +697,30 @@ class Popen(object):
if timeout is not None and len(done) != len(greenlets): if timeout is not None and len(done) != len(greenlets):
raise TimeoutExpired(self.args, timeout) raise TimeoutExpired(self.args, timeout)
if self.stdout: for pipe in (self.stdout, self.stderr):
try: if pipe:
self.stdout.close() try:
except RuntimeError: pipe.close()
pass except RuntimeError:
if self.stderr: pass
try:
self.stderr.close()
except RuntimeError:
pass
self.wait() self.wait()
stdout_value = self._stdout_buffer
self._stdout_buffer = None def _get_output_value(pipe_name):
stderr_value = self._stderr_buffer buf_name = '_' + pipe_name + '_buffer'
self._stderr_buffer = None buf_value = getattr(self, buf_name)
# XXX: Under python 3 in universal newlines mode we should be setattr(self, buf_name, None)
# returning str, not bytes if buf_value:
return (None if stdout is None else stdout_value or b'', buf_value = self._communicate_empty_value.join(buf_value)
None if stderr is None else stderr_value or b'') else:
buf_value = self._communicate_empty_value
return buf_value
stdout_value = _get_output_value('stdout')
stderr_value = _get_output_value('stderr')
return (None if stdout is None else stdout_value,
None if stderr is None else stderr_value)
def poll(self): def poll(self):
"""Check if child process has terminated. Set and return :attr:`returncode` attribute.""" """Check if child process has terminated. Set and return :attr:`returncode` attribute."""
......
...@@ -133,6 +133,8 @@ if PYPY3: ...@@ -133,6 +133,8 @@ if PYPY3:
else: else:
skipOnPyPy3 = _do_not_skip skipOnPyPy3 = _do_not_skip
skipIf = unittest.skipIf
EXPECT_POOR_TIMER_RESOLUTION = PYPY3 or RUNNING_ON_APPVEYOR EXPECT_POOR_TIMER_RESOLUTION = PYPY3 or RUNNING_ON_APPVEYOR
class ExpectedException(Exception): class ExpectedException(Exception):
......
...@@ -4,6 +4,10 @@ import errno ...@@ -4,6 +4,10 @@ import errno
import greentest import greentest
import gevent import gevent
from gevent import subprocess from gevent import subprocess
if not hasattr(subprocess, 'mswindows'):
# PyPy3, native python subprocess
subprocess.mswindows = False
import time import time
import gc import gc
import tempfile import tempfile
...@@ -230,6 +234,36 @@ class Test(greentest.TestCase): ...@@ -230,6 +234,36 @@ class Test(greentest.TestCase):
test_subprocess_in_native_thread.ignore_leakcheck = True test_subprocess_in_native_thread.ignore_leakcheck = True
def __test_no_output(self, kwargs, kind):
proc = subprocess.Popen([sys.executable, '-c', 'pass'],
stdout=subprocess.PIPE,
**kwargs)
stdout, stderr = proc.communicate()
self.assertIsInstance(stdout, kind)
self.assertIsNone(stderr)
def test_universal_newlines_text_mode_no_output_is_always_str(self):
# If the file is in universal_newlines mode, we should always get a str when
# there is no output.
# https://github.com/gevent/gevent/pull/939
kwargs = {'universal_newlines': True}
self.__test_no_output({'universal_newlines': True}, str)
@greentest.skipIf(sys.version_info[:2] < (3, 6), "Need encoding argument")
def test_encoded_text_mode_no_output_is_str(self):
# If the file is in universal_newlines mode, we should always get a str when
# there is no output.
# https://github.com/gevent/gevent/pull/939
self.__test_no_output({'encoding': 'utf-8'}, str)
def test_default_mode_no_output_is_always_str(self):
# If the file is in default mode, we should always get a str when
# there is no output.
# https://github.com/gevent/gevent/pull/939
self.__test_no_output({}, bytes)
class RunFuncTestCase(greentest.TestCase): class RunFuncTestCase(greentest.TestCase):
# Based on code from python 3.6 # Based on code from python 3.6
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment