Commit 7fed7bd8 authored by andyclegg's avatar andyclegg Committed by Gregory P. Smith

bpo-31756: subprocess.run should alias universal_newlines to text (#4049)

Improve human friendliness of the Popen API: Add text=False as a
keyword-only argument to subprocess.Popen along with a Popen
attribute .text_mode and set this based on the
encoding/errors/universal_newlines/text arguments.

The universal_newlines parameter and attribute are maintained for
backwards compatibility.
parent ae3087c6
...@@ -61,7 +61,7 @@ compatibility with older versions, see the :ref:`call-function-trio` section. ...@@ -61,7 +61,7 @@ compatibility with older versions, see the :ref:`call-function-trio` section.
The *input* argument is passed to :meth:`Popen.communicate` and thus to the The *input* argument is passed to :meth:`Popen.communicate` and thus to the
subprocess's stdin. If used it must be a byte sequence, or a string if subprocess's stdin. If used it must be a byte sequence, or a string if
*encoding* or *errors* is specified or *universal_newlines* is true. When *encoding* or *errors* is specified or *text* is true. When
used, the internal :class:`Popen` object is automatically created with used, the internal :class:`Popen` object is automatically created with
``stdin=PIPE``, and the *stdin* argument may not be used as well. ``stdin=PIPE``, and the *stdin* argument may not be used as well.
...@@ -70,10 +70,11 @@ compatibility with older versions, see the :ref:`call-function-trio` section. ...@@ -70,10 +70,11 @@ compatibility with older versions, see the :ref:`call-function-trio` section.
exception hold the arguments, the exit code, and stdout and stderr if they exception hold the arguments, the exit code, and stdout and stderr if they
were captured. were captured.
If *encoding* or *errors* are specified, or *universal_newlines* is true, If *encoding* or *errors* are specified, or *text* is true,
file objects for stdin, stdout and stderr are opened in text mode using the file objects for stdin, stdout and stderr are opened in text mode using the
specified *encoding* and *errors* or the :class:`io.TextIOWrapper` default. specified *encoding* and *errors* or the :class:`io.TextIOWrapper` default.
Otherwise, file objects are opened in binary mode. The *universal_newlines* argument is equivalent to *text* and is provided
for backwards compatibility. By default, file objects are opened in binary mode.
Examples:: Examples::
...@@ -95,6 +96,10 @@ compatibility with older versions, see the :ref:`call-function-trio` section. ...@@ -95,6 +96,10 @@ compatibility with older versions, see the :ref:`call-function-trio` section.
Added *encoding* and *errors* parameters Added *encoding* and *errors* parameters
.. versionchanged:: 3.7
Added the *text* parameter, as a more understandable alias of *universal_newlines*
.. class:: CompletedProcess .. class:: CompletedProcess
The return value from :func:`run`, representing a process that has finished. The return value from :func:`run`, representing a process that has finished.
...@@ -114,8 +119,8 @@ compatibility with older versions, see the :ref:`call-function-trio` section. ...@@ -114,8 +119,8 @@ compatibility with older versions, see the :ref:`call-function-trio` section.
.. attribute:: stdout .. attribute:: stdout
Captured stdout from the child process. A bytes sequence, or a string if Captured stdout from the child process. A bytes sequence, or a string if
:func:`run` was called with an encoding or errors. ``None`` if stdout was not :func:`run` was called with an encoding, errors, or text=True.
captured. ``None`` if stdout was not captured.
If you ran the process with ``stderr=subprocess.STDOUT``, stdout and If you ran the process with ``stderr=subprocess.STDOUT``, stdout and
stderr will be combined in this attribute, and :attr:`stderr` will be stderr will be combined in this attribute, and :attr:`stderr` will be
...@@ -124,8 +129,8 @@ compatibility with older versions, see the :ref:`call-function-trio` section. ...@@ -124,8 +129,8 @@ compatibility with older versions, see the :ref:`call-function-trio` section.
.. attribute:: stderr .. attribute:: stderr
Captured stderr from the child process. A bytes sequence, or a string if Captured stderr from the child process. A bytes sequence, or a string if
:func:`run` was called with an encoding or errors. ``None`` if stderr was not :func:`run` was called with an encoding, errors, or text=True.
captured. ``None`` if stderr was not captured.
.. method:: check_returncode() .. method:: check_returncode()
......
...@@ -320,8 +320,11 @@ def check_output(*popenargs, timeout=None, **kwargs): ...@@ -320,8 +320,11 @@ def check_output(*popenargs, timeout=None, **kwargs):
... input=b"when in the course of fooman events\n") ... input=b"when in the course of fooman events\n")
b'when in the course of barman events\n' b'when in the course of barman events\n'
If universal_newlines=True is passed, the "input" argument must be a By default, all communication is in bytes, and therefore any "input"
string and the return value will be a string rather than bytes. should be bytes, and the return value wil be bytes. If in text mode,
any "input" should be a string, and the return value will be a string
decoded according to locale encoding, or by "encoding" if set. Text mode
is triggered by setting any of text, encoding, errors or universal_newlines.
""" """
if 'stdout' in kwargs: if 'stdout' in kwargs:
raise ValueError('stdout argument not allowed, it will be overridden.') raise ValueError('stdout argument not allowed, it will be overridden.')
...@@ -384,15 +387,17 @@ def run(*popenargs, input=None, timeout=None, check=False, **kwargs): ...@@ -384,15 +387,17 @@ def run(*popenargs, input=None, timeout=None, check=False, **kwargs):
exception will be raised. exception will be raised.
There is an optional argument "input", allowing you to There is an optional argument "input", allowing you to
pass a string to the subprocess's stdin. If you use this argument pass bytes or a string to the subprocess's stdin. If you use this argument
you may not also use the Popen constructor's "stdin" argument, as you may not also use the Popen constructor's "stdin" argument, as
it will be used internally. it will be used internally.
The other arguments are the same as for the Popen constructor. By default, all communication is in bytes, and therefore any "input" should
be bytes, and the stdout and stderr will be bytes. If in text mode, any
"input" should be a string, and stdout and stderr will be strings decoded
according to locale encoding, or by "encoding" if set. Text mode is
triggered by setting any of text, encoding, errors or universal_newlines.
If universal_newlines=True is passed, the "input" argument must be a The other arguments are the same as for the Popen constructor.
string and stdout/stderr in the returned object will be strings rather than
bytes.
""" """
if input is not None: if input is not None:
if 'stdin' in kwargs: if 'stdin' in kwargs:
...@@ -513,7 +518,7 @@ def getstatusoutput(cmd): ...@@ -513,7 +518,7 @@ def getstatusoutput(cmd):
(-15, '') (-15, '')
""" """
try: try:
data = check_output(cmd, shell=True, universal_newlines=True, stderr=STDOUT) data = check_output(cmd, shell=True, text=True, stderr=STDOUT)
exitcode = 0 exitcode = 0
except CalledProcessError as ex: except CalledProcessError as ex:
data = ex.output data = ex.output
...@@ -565,8 +570,10 @@ class Popen(object): ...@@ -565,8 +570,10 @@ class Popen(object):
env: Defines the environment variables for the new process. env: Defines the environment variables for the new process.
universal_newlines: If true, use universal line endings for file text: If true, decode stdin, stdout and stderr using the given encoding
objects stdin, stdout and stderr. (if set) or the system default otherwise.
universal_newlines: Alias of text, provided for backwards compatibility.
startupinfo and creationflags (Windows only) startupinfo and creationflags (Windows only)
...@@ -587,10 +594,10 @@ class Popen(object): ...@@ -587,10 +594,10 @@ class Popen(object):
def __init__(self, args, bufsize=-1, executable=None, def __init__(self, args, bufsize=-1, executable=None,
stdin=None, stdout=None, stderr=None, stdin=None, stdout=None, stderr=None,
preexec_fn=None, close_fds=_PLATFORM_DEFAULT_CLOSE_FDS, preexec_fn=None, close_fds=_PLATFORM_DEFAULT_CLOSE_FDS,
shell=False, cwd=None, env=None, universal_newlines=False, shell=False, cwd=None, env=None, universal_newlines=None,
startupinfo=None, creationflags=0, startupinfo=None, creationflags=0,
restore_signals=True, start_new_session=False, restore_signals=True, start_new_session=False,
pass_fds=(), *, encoding=None, errors=None): pass_fds=(), *, encoding=None, errors=None, text=None):
"""Create new Popen instance.""" """Create new Popen instance."""
_cleanup() _cleanup()
# Held while anything is calling waitpid before returncode has been # Held while anything is calling waitpid before returncode has been
...@@ -642,10 +649,16 @@ class Popen(object): ...@@ -642,10 +649,16 @@ class Popen(object):
self.stderr = None self.stderr = None
self.pid = None self.pid = None
self.returncode = None self.returncode = None
self.universal_newlines = universal_newlines
self.encoding = encoding self.encoding = encoding
self.errors = errors self.errors = errors
# Validate the combinations of text and universal_newlines
if (text is not None and universal_newlines is not None
and bool(universal_newlines) != bool(text)):
raise SubprocessError('Cannot disambiguate when both text '
'and universal_newlines are supplied but '
'different. Pass one or the other.')
# Input and output objects. The general principle is like # Input and output objects. The general principle is like
# this: # this:
# #
...@@ -677,25 +690,25 @@ class Popen(object): ...@@ -677,25 +690,25 @@ class Popen(object):
if errread != -1: if errread != -1:
errread = msvcrt.open_osfhandle(errread.Detach(), 0) errread = msvcrt.open_osfhandle(errread.Detach(), 0)
text_mode = encoding or errors or universal_newlines self.text_mode = encoding or errors or text or universal_newlines
self._closed_child_pipe_fds = False self._closed_child_pipe_fds = False
try: try:
if p2cwrite != -1: if p2cwrite != -1:
self.stdin = io.open(p2cwrite, 'wb', bufsize) self.stdin = io.open(p2cwrite, 'wb', bufsize)
if text_mode: if self.text_mode:
self.stdin = io.TextIOWrapper(self.stdin, write_through=True, self.stdin = io.TextIOWrapper(self.stdin, write_through=True,
line_buffering=(bufsize == 1), line_buffering=(bufsize == 1),
encoding=encoding, errors=errors) encoding=encoding, errors=errors)
if c2pread != -1: if c2pread != -1:
self.stdout = io.open(c2pread, 'rb', bufsize) self.stdout = io.open(c2pread, 'rb', bufsize)
if text_mode: if self.text_mode:
self.stdout = io.TextIOWrapper(self.stdout, self.stdout = io.TextIOWrapper(self.stdout,
encoding=encoding, errors=errors) encoding=encoding, errors=errors)
if errread != -1: if errread != -1:
self.stderr = io.open(errread, 'rb', bufsize) self.stderr = io.open(errread, 'rb', bufsize)
if text_mode: if self.text_mode:
self.stderr = io.TextIOWrapper(self.stderr, self.stderr = io.TextIOWrapper(self.stderr,
encoding=encoding, errors=errors) encoding=encoding, errors=errors)
...@@ -735,6 +748,16 @@ class Popen(object): ...@@ -735,6 +748,16 @@ class Popen(object):
raise raise
@property
def universal_newlines(self):
# universal_newlines as retained as an alias of text_mode for API
# compatability. bpo-31756
return self.text_mode
@universal_newlines.setter
def universal_newlines(self, universal_newlines):
self.text_mode = bool(universal_newlines)
def _translate_newlines(self, data, encoding, errors): def _translate_newlines(self, data, encoding, errors):
data = data.decode(encoding, errors) data = data.decode(encoding, errors)
return data.replace("\r\n", "\n").replace("\r", "\n") return data.replace("\r\n", "\n").replace("\r", "\n")
...@@ -805,12 +828,16 @@ class Popen(object): ...@@ -805,12 +828,16 @@ class Popen(object):
reached. Wait for process to terminate. reached. Wait for process to terminate.
The optional "input" argument should be data to be sent to the The optional "input" argument should be data to be sent to the
child process (if self.universal_newlines is True, this should child process, or None, if no data should be sent to the child.
be a string; if it is False, "input" should be bytes), or communicate() returns a tuple (stdout, stderr).
None, if no data should be sent to the child.
By default, all communication is in bytes, and therefore any
communicate() returns a tuple (stdout, stderr). These will be "input" should be bytes, and the (stdout, stderr) will be bytes.
bytes or, if self.universal_newlines was True, a string. If in text mode (indicated by self.text_mode), any "input" should
be a string, and (stdout, stderr) will be strings decoded
according to locale encoding, or by "encoding" if set. Text mode
is triggered by setting any of text, encoding, errors or
universal_newlines.
""" """
if self._communication_started and input: if self._communication_started and input:
...@@ -1533,7 +1560,7 @@ class Popen(object): ...@@ -1533,7 +1560,7 @@ class Popen(object):
# Translate newlines, if requested. # Translate newlines, if requested.
# This also turns bytes into strings. # This also turns bytes into strings.
if self.encoding or self.errors or self.universal_newlines: if self.text_mode:
if stdout is not None: if stdout is not None:
stdout = self._translate_newlines(stdout, stdout = self._translate_newlines(stdout,
self.stdout.encoding, self.stdout.encoding,
...@@ -1553,8 +1580,7 @@ class Popen(object): ...@@ -1553,8 +1580,7 @@ class Popen(object):
if self.stdin and self._input is None: if self.stdin and self._input is None:
self._input_offset = 0 self._input_offset = 0
self._input = input self._input = input
if input is not None and ( if input is not None and self.text_mode:
self.encoding or self.errors or self.universal_newlines):
self._input = self._input.encode(self.stdin.encoding, self._input = self._input.encode(self.stdin.encoding,
self.stdin.errors) self.stdin.errors)
......
...@@ -845,8 +845,9 @@ class ProcessTestCase(BaseTestCase): ...@@ -845,8 +845,9 @@ class ProcessTestCase(BaseTestCase):
self.assertEqual(stdout, b"bananasplit") self.assertEqual(stdout, b"bananasplit")
self.assertStderrEqual(stderr, b"") self.assertStderrEqual(stderr, b"")
def test_universal_newlines(self): def test_universal_newlines_and_text(self):
p = subprocess.Popen([sys.executable, "-c", args = [
sys.executable, "-c",
'import sys,os;' + SETBINARY + 'import sys,os;' + SETBINARY +
'buf = sys.stdout.buffer;' 'buf = sys.stdout.buffer;'
'buf.write(sys.stdin.readline().encode());' 'buf.write(sys.stdin.readline().encode());'
...@@ -863,10 +864,12 @@ class ProcessTestCase(BaseTestCase): ...@@ -863,10 +864,12 @@ class ProcessTestCase(BaseTestCase):
'buf.flush();' 'buf.flush();'
'buf.write(b"\\nline7");' 'buf.write(b"\\nline7");'
'buf.flush();' 'buf.flush();'
'buf.write(b"\\nline8");'], 'buf.write(b"\\nline8");']
stdin=subprocess.PIPE,
stdout=subprocess.PIPE, for extra_kwarg in ('universal_newlines', 'text'):
universal_newlines=1) p = subprocess.Popen(args, **{'stdin': subprocess.PIPE,
'stdout': subprocess.PIPE,
extra_kwarg: True})
with p: with p:
p.stdin.write("line1\n") p.stdin.write("line1\n")
p.stdin.flush() p.stdin.flush()
......
Add a ``subprocess.Popen(text=False)`` keyword argument to `subprocess`
functions to be more explicit about when the library should attempt to
decode outputs into text. Patch by Andrew Clegg.
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