Commit 0bce68b3 authored by Jason Madden's avatar Jason Madden

Python 2, subprocess: Let unbuffered binary writes to popen.stdin loop to write all the data.

More like what Python 2 standard library does. Beware of relying on that though during an upgrade.

Fixes #1711
parent 9bc5e65d
Python 2: Make ``gevent.subprocess.Popen.stdin`` objects have a
``write`` method that guarantees to write the entire argument in
binary, unbuffered mode. This may require multiple trips around the
event loop, but more closely matches the behaviour of the Python 2
standard library (and gevent prior to 1.5). The number of bytes
written is still returned (instead of ``None``).
This diff is collapsed.
...@@ -14,6 +14,7 @@ from gevent._compat import reraise ...@@ -14,6 +14,7 @@ from gevent._compat import reraise
from gevent._fileobjectcommon import cancel_wait_ex from gevent._fileobjectcommon import cancel_wait_ex
from gevent._fileobjectcommon import FileObjectBase from gevent._fileobjectcommon import FileObjectBase
from gevent._fileobjectcommon import OpenDescriptor from gevent._fileobjectcommon import OpenDescriptor
from gevent._fileobjectcommon import WriteIsWriteallMixin
from gevent._hub_primitives import wait_on_watcher from gevent._hub_primitives import wait_on_watcher
from gevent.hub import get_hub from gevent.hub import get_hub
from gevent.os import _read from gevent.os import _read
...@@ -213,27 +214,40 @@ class GreenFileDescriptorIO(RawIOBase): ...@@ -213,27 +214,40 @@ class GreenFileDescriptorIO(RawIOBase):
) )
class GreenFileDescriptorIOWriteall(WriteIsWriteallMixin,
GreenFileDescriptorIO):
pass
class GreenOpenDescriptor(OpenDescriptor): class GreenOpenDescriptor(OpenDescriptor):
def open_raw(self): def _do_open_raw(self):
if self.is_fd(): if self.is_fd():
fileio = GreenFileDescriptorIO(self.fobj, self, closefd=self.closefd) fileio = GreenFileDescriptorIO(self._fobj, self, closefd=self.closefd)
else: else:
closefd = False
# Either an existing file object or a path string (which # Either an existing file object or a path string (which
# we open to get a file object). In either case, the other object # we open to get a file object). In either case, the other object
# owns the descriptor and we must not close it. # owns the descriptor and we must not close it.
closefd = False closefd = False
if hasattr(self.fobj, 'fileno'):
raw = self.fobj raw = OpenDescriptor._do_open_raw(self)
else:
raw = OpenDescriptor.open_raw(self)
fileno = raw.fileno() fileno = raw.fileno()
fileio = GreenFileDescriptorIO(fileno, self, closefd=closefd) fileio = GreenFileDescriptorIO(fileno, self, closefd=closefd)
fileio._keep_alive = raw fileio._keep_alive = raw
return fileio return fileio
def _make_atomic_write(self, result, raw):
# Our return value from _do_open_raw is always a new
# object that we own, so we're always free to change
# the class.
assert result is not raw or self._raw_object_is_new(raw)
if result.__class__ is GreenFileDescriptorIO:
result.__class__ = GreenFileDescriptorIOWriteall
else:
result = OpenDescriptor._make_atomic_write(self, result, raw)
return result
class FileObjectPosix(FileObjectBase): class FileObjectPosix(FileObjectBase):
""" """
...@@ -309,9 +323,9 @@ class FileObjectPosix(FileObjectBase): ...@@ -309,9 +323,9 @@ class FileObjectPosix(FileObjectBase):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
descriptor = GreenOpenDescriptor(*args, **kwargs) descriptor = GreenOpenDescriptor(*args, **kwargs)
FileObjectBase.__init__(self, descriptor)
# This attribute is documented as available for non-blocking reads. # This attribute is documented as available for non-blocking reads.
self.fileio, buffered_fobj = descriptor.open_raw_and_wrapped() self.fileio = descriptor.opened_raw()
FileObjectBase.__init__(self, buffered_fobj, descriptor.closefd)
def _do_close(self, fobj, closefd): def _do_close(self, fobj, closefd):
try: try:
......
...@@ -455,6 +455,14 @@ def FileObject(*args, **kwargs): ...@@ -455,6 +455,14 @@ def FileObject(*args, **kwargs):
# Defer importing FileObject until we need it # Defer importing FileObject until we need it
# to allow it to be configured more easily. # to allow it to be configured more easily.
from gevent.fileobject import FileObject as _FileObject from gevent.fileobject import FileObject as _FileObject
if not PY3:
# Make write behave like the old Python 2 file
# write and loop to consume output, even when not
# buffered.
__FileObject = _FileObject
def _FileObject(*args, **kwargs):
kwargs['atomic_write'] = True
return __FileObject(*args, **kwargs)
globals()['FileObject'] = _FileObject globals()['FileObject'] = _FileObject
return _FileObject(*args) return _FileObject(*args)
...@@ -557,6 +565,12 @@ class Popen(object): ...@@ -557,6 +565,12 @@ class Popen(object):
.. seealso:: :class:`subprocess.Popen` .. seealso:: :class:`subprocess.Popen`
This class should have the same interface as the standard library class. This class should have the same interface as the standard library class.
.. caution::
The default values of some arguments, notably ``buffering``, differ
between Python 2 and Python 3. For the most consistent behaviour across
versions, it's best to explicitly pass the desired values.
.. caution:: .. caution::
On Python 2, the ``read`` method of the ``stdout`` and ``stderr`` attributes On Python 2, the ``read`` method of the ``stdout`` and ``stderr`` attributes
...@@ -602,6 +616,15 @@ class Popen(object): ...@@ -602,6 +616,15 @@ class Popen(object):
Add the *group*, *extra_groups*, *user*, and *umask* arguments. These Add the *group*, *extra_groups*, *user*, and *umask* arguments. These
were added to Python 3.9, but are available in any gevent version, provided were added to Python 3.9, but are available in any gevent version, provided
the underlying platform support is present. the underlying platform support is present.
.. versionchanged:: NEXT
On Python 2 only, if unbuffered binary communication is requested,
the ``stdin`` attribute of this object will have a ``write`` method that
actually performs internal buffering and looping, similar to the standard library.
It guarantees to write all the data given to it in a single call (but internally
it may make many system calls and/or trips around the event loop to accomplish this).
See :issue:`1711`.
""" """
if GenericAlias is not None: if GenericAlias is not None:
...@@ -751,6 +774,7 @@ class Popen(object): ...@@ -751,6 +774,7 @@ class Popen(object):
encoding=self.encoding, errors=self.errors) encoding=self.encoding, errors=self.errors)
else: else:
self.stdin = FileObject(p2cwrite, 'wb', bufsize) self.stdin = FileObject(p2cwrite, 'wb', bufsize)
if c2pread != -1: if c2pread != -1:
if universal_newlines or text_mode: if universal_newlines or text_mode:
if PY3: if PY3:
......
...@@ -11,6 +11,10 @@ import unittest ...@@ -11,6 +11,10 @@ import unittest
import gevent import gevent
from gevent import fileobject from gevent import fileobject
from gevent._fileobjectcommon import OpenDescriptor from gevent._fileobjectcommon import OpenDescriptor
try:
from gevent._fileobjectposix import GreenOpenDescriptor
except ImportError:
GreenOpenDescriptor = None
from gevent._compat import PY2 from gevent._compat import PY2
from gevent._compat import PY3 from gevent._compat import PY3
...@@ -387,8 +391,11 @@ class TestTextMode(unittest.TestCase): ...@@ -387,8 +391,11 @@ class TestTextMode(unittest.TestCase):
class TestOpenDescriptor(greentest.TestCase): class TestOpenDescriptor(greentest.TestCase):
def _getTargetClass(self):
return OpenDescriptor
def _makeOne(self, *args, **kwargs): def _makeOne(self, *args, **kwargs):
return OpenDescriptor(*args, **kwargs) return self._getTargetClass()(*args, **kwargs)
def _check(self, regex, kind, *args, **kwargs): def _check(self, regex, kind, *args, **kwargs):
with self.assertRaisesRegex(kind, regex): with self.assertRaisesRegex(kind, regex):
...@@ -411,14 +418,33 @@ class TestOpenDescriptor(greentest.TestCase): ...@@ -411,14 +418,33 @@ class TestOpenDescriptor(greentest.TestCase):
vase('take a newline', mode='rb', newline='\n'), vase('take a newline', mode='rb', newline='\n'),
) )
def test_atomicwrite_fd(self):
from gevent._fileobjectcommon import WriteallMixin
# It basically only does something when buffering is otherwise disabled
desc = self._makeOne(1, 'wb',
buffering=0,
closefd=False,
atomic_write=True)
self.assertTrue(desc.atomic_write)
fobj = desc.opened()
self.assertIsInstance(fobj, WriteallMixin)
def pop(): def pop():
for regex, kind, kwargs in TestOpenDescriptor.CASES: for regex, kind, kwargs in TestOpenDescriptor.CASES:
setattr( setattr(
TestOpenDescriptor, 'test_' + regex, TestOpenDescriptor, 'test_' + regex.replace(' ', '_'),
lambda self, _re=regex, _kind=kind, _kw=kwargs: self._check(_re, _kind, 1, **_kw) lambda self, _re=regex, _kind=kind, _kw=kwargs: self._check(_re, _kind, 1, **_kw)
) )
pop() pop()
@unittest.skipIf(GreenOpenDescriptor is None, "No support for non-blocking IO")
class TestGreenOpenDescripton(TestOpenDescriptor):
def _getTargetClass(self):
return GreenOpenDescriptor
if __name__ == '__main__': if __name__ == '__main__':
greentest.main() greentest.main()
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