Skip to content

Commit 7fed7bd

Browse files
andyclegggpshead
authored andcommitted
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.
1 parent ae3087c commit 7fed7bd

4 files changed

Lines changed: 105 additions & 68 deletions

File tree

Doc/library/subprocess.rst

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ compatibility with older versions, see the :ref:`call-function-trio` section.
6161

6262
The *input* argument is passed to :meth:`Popen.communicate` and thus to the
6363
subprocess's stdin. If used it must be a byte sequence, or a string if
64-
*encoding* or *errors* is specified or *universal_newlines* is true. When
64+
*encoding* or *errors* is specified or *text* is true. When
6565
used, the internal :class:`Popen` object is automatically created with
6666
``stdin=PIPE``, and the *stdin* argument may not be used as well.
6767

@@ -70,10 +70,11 @@ compatibility with older versions, see the :ref:`call-function-trio` section.
7070
exception hold the arguments, the exit code, and stdout and stderr if they
7171
were captured.
7272

73-
If *encoding* or *errors* are specified, or *universal_newlines* is true,
73+
If *encoding* or *errors* are specified, or *text* is true,
7474
file objects for stdin, stdout and stderr are opened in text mode using the
7575
specified *encoding* and *errors* or the :class:`io.TextIOWrapper` default.
76-
Otherwise, file objects are opened in binary mode.
76+
The *universal_newlines* argument is equivalent to *text* and is provided
77+
for backwards compatibility. By default, file objects are opened in binary mode.
7778

7879
Examples::
7980

@@ -95,6 +96,10 @@ compatibility with older versions, see the :ref:`call-function-trio` section.
9596

9697
Added *encoding* and *errors* parameters
9798

99+
.. versionchanged:: 3.7
100+
101+
Added the *text* parameter, as a more understandable alias of *universal_newlines*
102+
98103
.. class:: CompletedProcess
99104

100105
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.
114119
.. attribute:: stdout
115120

116121
Captured stdout from the child process. A bytes sequence, or a string if
117-
:func:`run` was called with an encoding or errors. ``None`` if stdout was not
118-
captured.
122+
:func:`run` was called with an encoding, errors, or text=True.
123+
``None`` if stdout was not captured.
119124

120125
If you ran the process with ``stderr=subprocess.STDOUT``, stdout and
121126
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.
124129
.. attribute:: stderr
125130

126131
Captured stderr from the child process. A bytes sequence, or a string if
127-
:func:`run` was called with an encoding or errors. ``None`` if stderr was not
128-
captured.
132+
:func:`run` was called with an encoding, errors, or text=True.
133+
``None`` if stderr was not captured.
129134

130135
.. method:: check_returncode()
131136

Lib/subprocess.py

Lines changed: 52 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -320,8 +320,11 @@ def check_output(*popenargs, timeout=None, **kwargs):
320320
... input=b"when in the course of fooman events\n")
321321
b'when in the course of barman events\n'
322322
323-
If universal_newlines=True is passed, the "input" argument must be a
324-
string and the return value will be a string rather than bytes.
323+
By default, all communication is in bytes, and therefore any "input"
324+
should be bytes, and the return value wil be bytes. If in text mode,
325+
any "input" should be a string, and the return value will be a string
326+
decoded according to locale encoding, or by "encoding" if set. Text mode
327+
is triggered by setting any of text, encoding, errors or universal_newlines.
325328
"""
326329
if 'stdout' in kwargs:
327330
raise ValueError('stdout argument not allowed, it will be overridden.')
@@ -384,15 +387,17 @@ def run(*popenargs, input=None, timeout=None, check=False, **kwargs):
384387
exception will be raised.
385388
386389
There is an optional argument "input", allowing you to
387-
pass a string to the subprocess's stdin. If you use this argument
390+
pass bytes or a string to the subprocess's stdin. If you use this argument
388391
you may not also use the Popen constructor's "stdin" argument, as
389392
it will be used internally.
390393
391-
The other arguments are the same as for the Popen constructor.
394+
By default, all communication is in bytes, and therefore any "input" should
395+
be bytes, and the stdout and stderr will be bytes. If in text mode, any
396+
"input" should be a string, and stdout and stderr will be strings decoded
397+
according to locale encoding, or by "encoding" if set. Text mode is
398+
triggered by setting any of text, encoding, errors or universal_newlines.
392399
393-
If universal_newlines=True is passed, the "input" argument must be a
394-
string and stdout/stderr in the returned object will be strings rather than
395-
bytes.
400+
The other arguments are the same as for the Popen constructor.
396401
"""
397402
if input is not None:
398403
if 'stdin' in kwargs:
@@ -513,7 +518,7 @@ def getstatusoutput(cmd):
513518
(-15, '')
514519
"""
515520
try:
516-
data = check_output(cmd, shell=True, universal_newlines=True, stderr=STDOUT)
521+
data = check_output(cmd, shell=True, text=True, stderr=STDOUT)
517522
exitcode = 0
518523
except CalledProcessError as ex:
519524
data = ex.output
@@ -565,8 +570,10 @@ class Popen(object):
565570
566571
env: Defines the environment variables for the new process.
567572
568-
universal_newlines: If true, use universal line endings for file
569-
objects stdin, stdout and stderr.
573+
text: If true, decode stdin, stdout and stderr using the given encoding
574+
(if set) or the system default otherwise.
575+
576+
universal_newlines: Alias of text, provided for backwards compatibility.
570577
571578
startupinfo and creationflags (Windows only)
572579
@@ -587,10 +594,10 @@ class Popen(object):
587594
def __init__(self, args, bufsize=-1, executable=None,
588595
stdin=None, stdout=None, stderr=None,
589596
preexec_fn=None, close_fds=_PLATFORM_DEFAULT_CLOSE_FDS,
590-
shell=False, cwd=None, env=None, universal_newlines=False,
597+
shell=False, cwd=None, env=None, universal_newlines=None,
591598
startupinfo=None, creationflags=0,
592599
restore_signals=True, start_new_session=False,
593-
pass_fds=(), *, encoding=None, errors=None):
600+
pass_fds=(), *, encoding=None, errors=None, text=None):
594601
"""Create new Popen instance."""
595602
_cleanup()
596603
# Held while anything is calling waitpid before returncode has been
@@ -642,10 +649,16 @@ def __init__(self, args, bufsize=-1, executable=None,
642649
self.stderr = None
643650
self.pid = None
644651
self.returncode = None
645-
self.universal_newlines = universal_newlines
646652
self.encoding = encoding
647653
self.errors = errors
648654

655+
# Validate the combinations of text and universal_newlines
656+
if (text is not None and universal_newlines is not None
657+
and bool(universal_newlines) != bool(text)):
658+
raise SubprocessError('Cannot disambiguate when both text '
659+
'and universal_newlines are supplied but '
660+
'different. Pass one or the other.')
661+
649662
# Input and output objects. The general principle is like
650663
# this:
651664
#
@@ -677,25 +690,25 @@ def __init__(self, args, bufsize=-1, executable=None,
677690
if errread != -1:
678691
errread = msvcrt.open_osfhandle(errread.Detach(), 0)
679692

680-
text_mode = encoding or errors or universal_newlines
693+
self.text_mode = encoding or errors or text or universal_newlines
681694

682695
self._closed_child_pipe_fds = False
683696

684697
try:
685698
if p2cwrite != -1:
686699
self.stdin = io.open(p2cwrite, 'wb', bufsize)
687-
if text_mode:
700+
if self.text_mode:
688701
self.stdin = io.TextIOWrapper(self.stdin, write_through=True,
689702
line_buffering=(bufsize == 1),
690703
encoding=encoding, errors=errors)
691704
if c2pread != -1:
692705
self.stdout = io.open(c2pread, 'rb', bufsize)
693-
if text_mode:
706+
if self.text_mode:
694707
self.stdout = io.TextIOWrapper(self.stdout,
695708
encoding=encoding, errors=errors)
696709
if errread != -1:
697710
self.stderr = io.open(errread, 'rb', bufsize)
698-
if text_mode:
711+
if self.text_mode:
699712
self.stderr = io.TextIOWrapper(self.stderr,
700713
encoding=encoding, errors=errors)
701714

@@ -735,6 +748,16 @@ def __init__(self, args, bufsize=-1, executable=None,
735748

736749
raise
737750

751+
@property
752+
def universal_newlines(self):
753+
# universal_newlines as retained as an alias of text_mode for API
754+
# compatability. bpo-31756
755+
return self.text_mode
756+
757+
@universal_newlines.setter
758+
def universal_newlines(self, universal_newlines):
759+
self.text_mode = bool(universal_newlines)
760+
738761
def _translate_newlines(self, data, encoding, errors):
739762
data = data.decode(encoding, errors)
740763
return data.replace("\r\n", "\n").replace("\r", "\n")
@@ -805,12 +828,16 @@ def communicate(self, input=None, timeout=None):
805828
reached. Wait for process to terminate.
806829
807830
The optional "input" argument should be data to be sent to the
808-
child process (if self.universal_newlines is True, this should
809-
be a string; if it is False, "input" should be bytes), or
810-
None, if no data should be sent to the child.
811-
812-
communicate() returns a tuple (stdout, stderr). These will be
813-
bytes or, if self.universal_newlines was True, a string.
831+
child process, or None, if no data should be sent to the child.
832+
communicate() returns a tuple (stdout, stderr).
833+
834+
By default, all communication is in bytes, and therefore any
835+
"input" should be bytes, and the (stdout, stderr) will be bytes.
836+
If in text mode (indicated by self.text_mode), any "input" should
837+
be a string, and (stdout, stderr) will be strings decoded
838+
according to locale encoding, or by "encoding" if set. Text mode
839+
is triggered by setting any of text, encoding, errors or
840+
universal_newlines.
814841
"""
815842

816843
if self._communication_started and input:
@@ -1533,7 +1560,7 @@ def _communicate(self, input, endtime, orig_timeout):
15331560

15341561
# Translate newlines, if requested.
15351562
# This also turns bytes into strings.
1536-
if self.encoding or self.errors or self.universal_newlines:
1563+
if self.text_mode:
15371564
if stdout is not None:
15381565
stdout = self._translate_newlines(stdout,
15391566
self.stdout.encoding,
@@ -1553,8 +1580,7 @@ def _save_input(self, input):
15531580
if self.stdin and self._input is None:
15541581
self._input_offset = 0
15551582
self._input = input
1556-
if input is not None and (
1557-
self.encoding or self.errors or self.universal_newlines):
1583+
if input is not None and self.text_mode:
15581584
self._input = self._input.encode(self.stdin.encoding,
15591585
self.stdin.errors)
15601586

Lib/test/test_subprocess.py

Lines changed: 38 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -845,41 +845,44 @@ def test_writes_before_communicate(self):
845845
self.assertEqual(stdout, b"bananasplit")
846846
self.assertStderrEqual(stderr, b"")
847847

848-
def test_universal_newlines(self):
849-
p = subprocess.Popen([sys.executable, "-c",
850-
'import sys,os;' + SETBINARY +
851-
'buf = sys.stdout.buffer;'
852-
'buf.write(sys.stdin.readline().encode());'
853-
'buf.flush();'
854-
'buf.write(b"line2\\n");'
855-
'buf.flush();'
856-
'buf.write(sys.stdin.read().encode());'
857-
'buf.flush();'
858-
'buf.write(b"line4\\n");'
859-
'buf.flush();'
860-
'buf.write(b"line5\\r\\n");'
861-
'buf.flush();'
862-
'buf.write(b"line6\\r");'
863-
'buf.flush();'
864-
'buf.write(b"\\nline7");'
865-
'buf.flush();'
866-
'buf.write(b"\\nline8");'],
867-
stdin=subprocess.PIPE,
868-
stdout=subprocess.PIPE,
869-
universal_newlines=1)
870-
with p:
871-
p.stdin.write("line1\n")
872-
p.stdin.flush()
873-
self.assertEqual(p.stdout.readline(), "line1\n")
874-
p.stdin.write("line3\n")
875-
p.stdin.close()
876-
self.addCleanup(p.stdout.close)
877-
self.assertEqual(p.stdout.readline(),
878-
"line2\n")
879-
self.assertEqual(p.stdout.read(6),
880-
"line3\n")
881-
self.assertEqual(p.stdout.read(),
882-
"line4\nline5\nline6\nline7\nline8")
848+
def test_universal_newlines_and_text(self):
849+
args = [
850+
sys.executable, "-c",
851+
'import sys,os;' + SETBINARY +
852+
'buf = sys.stdout.buffer;'
853+
'buf.write(sys.stdin.readline().encode());'
854+
'buf.flush();'
855+
'buf.write(b"line2\\n");'
856+
'buf.flush();'
857+
'buf.write(sys.stdin.read().encode());'
858+
'buf.flush();'
859+
'buf.write(b"line4\\n");'
860+
'buf.flush();'
861+
'buf.write(b"line5\\r\\n");'
862+
'buf.flush();'
863+
'buf.write(b"line6\\r");'
864+
'buf.flush();'
865+
'buf.write(b"\\nline7");'
866+
'buf.flush();'
867+
'buf.write(b"\\nline8");']
868+
869+
for extra_kwarg in ('universal_newlines', 'text'):
870+
p = subprocess.Popen(args, **{'stdin': subprocess.PIPE,
871+
'stdout': subprocess.PIPE,
872+
extra_kwarg: True})
873+
with p:
874+
p.stdin.write("line1\n")
875+
p.stdin.flush()
876+
self.assertEqual(p.stdout.readline(), "line1\n")
877+
p.stdin.write("line3\n")
878+
p.stdin.close()
879+
self.addCleanup(p.stdout.close)
880+
self.assertEqual(p.stdout.readline(),
881+
"line2\n")
882+
self.assertEqual(p.stdout.read(6),
883+
"line3\n")
884+
self.assertEqual(p.stdout.read(),
885+
"line4\nline5\nline6\nline7\nline8")
883886

884887
def test_universal_newlines_communicate(self):
885888
# universal newlines through communicate()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add a ``subprocess.Popen(text=False)`` keyword argument to `subprocess`
2+
functions to be more explicit about when the library should attempt to
3+
decode outputs into text. Patch by Andrew Clegg.

0 commit comments

Comments
 (0)