Skip to content

Commit 15f8a93

Browse files
gpsheadclaude
andcommitted
Support universal_newlines and use _translate_newlines in run_pipeline
- Factor out _translate_newlines() as a module-level function, have Popen's method delegate to it for code sharing - Remove rejection of universal_newlines kwarg in run_pipeline(), treat it the same as text=True (consistent with Popen behavior) - Use _translate_newlines() for text mode decoding in run_pipeline() to properly handle \r\n and \r newline sequences - Update documentation to remove mention of universal_newlines rejection - Update test to verify universal_newlines=True works like text=True Co-authored-by: Claude <noreply@anthropic.com>
1 parent 978cd76 commit 15f8a93

File tree

3 files changed

+23
-29
lines changed

3 files changed

+23
-29
lines changed

Doc/library/subprocess.rst

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -317,9 +317,6 @@ underlying :class:`Popen` interface can be used directly.
317317
in the returned :class:`PipelineResult`'s :attr:`~PipelineResult.stdout`
318318
attribute. Other keyword arguments are passed to each :class:`Popen` call.
319319

320-
Unlike :func:`run`, this function does not accept *universal_newlines*.
321-
Use ``text=True`` instead.
322-
323320
Examples::
324321

325322
>>> import subprocess

Lib/subprocess.py

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,12 @@ def _make_input_view(input_data):
354354
return memoryview(input_data)
355355

356356

357+
def _translate_newlines(data, encoding, errors):
358+
"""Decode bytes to str and translate newlines to \n."""
359+
data = data.decode(encoding, errors)
360+
return data.replace("\r\n", "\n").replace("\r", "\n")
361+
362+
357363
def _communicate_io_posix(selector, stdin, input_view, input_offset,
358364
output_buffers, endtime):
359365
"""
@@ -984,13 +990,6 @@ def run_pipeline(*commands, input=None, capture_output=False, timeout=None,
984990
if len(commands) < 2:
985991
raise ValueError('run_pipeline requires at least 2 commands')
986992

987-
# Reject universal_newlines - use text= instead
988-
if kwargs.get('universal_newlines') is not None:
989-
raise TypeError(
990-
"run_pipeline() does not support 'universal_newlines'. "
991-
"Use 'text=True' instead."
992-
)
993-
994993
# Validate no conflicting arguments
995994
if input is not None:
996995
if kwargs.get('stdin') is not None:
@@ -1071,8 +1070,9 @@ def run_pipeline(*commands, input=None, capture_output=False, timeout=None,
10711070
else:
10721071
endtime = None
10731072

1074-
# Determine if we're in text mode
1075-
text_mode = kwargs.get('text') or kwargs.get('encoding') or kwargs.get('errors')
1073+
# Determine if we're in text mode (text= or universal_newlines=)
1074+
text_mode = (kwargs.get('text') or kwargs.get('universal_newlines')
1075+
or kwargs.get('encoding') or kwargs.get('errors'))
10761076
encoding = kwargs.get('encoding')
10771077
errors_param = kwargs.get('errors', 'strict')
10781078
if text_mode and encoding is None:
@@ -1115,13 +1115,11 @@ def run_pipeline(*commands, input=None, capture_output=False, timeout=None,
11151115
stdout = results.get(last_proc.stdout)
11161116
stderr = results.get(stderr_reader)
11171117

1118-
# Decode stdout if in text mode (Popen text mode only applies to
1119-
# streams it creates, but we read via _communicate_streams which
1120-
# always returns bytes)
1118+
# Translate newlines if in text mode (decode and convert \r\n to \n)
11211119
if text_mode and stdout is not None:
1122-
stdout = stdout.decode(encoding, errors_param)
1120+
stdout = _translate_newlines(stdout, encoding, errors_param)
11231121
if text_mode and stderr is not None:
1124-
stderr = stderr.decode(encoding, errors_param)
1122+
stderr = _translate_newlines(stderr, encoding, errors_param)
11251123

11261124
# Wait for all processes to complete (use remaining time from deadline)
11271125
returncodes = []
@@ -1686,8 +1684,7 @@ def universal_newlines(self, universal_newlines):
16861684
self.text_mode = bool(universal_newlines)
16871685

16881686
def _translate_newlines(self, data, encoding, errors):
1689-
data = data.decode(encoding, errors)
1690-
return data.replace("\r\n", "\n").replace("\r", "\n")
1687+
return _translate_newlines(data, encoding, errors)
16911688

16921689
def __enter__(self):
16931690
return self

Lib/test/test_subprocess.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2123,16 +2123,16 @@ def test_pipeline_capture_output_conflict(self):
21232123
)
21242124
self.assertIn('capture_output', str(cm.exception))
21252125

2126-
def test_pipeline_rejects_universal_newlines(self):
2127-
"""Test that universal_newlines is not supported"""
2128-
with self.assertRaises(TypeError) as cm:
2129-
subprocess.run_pipeline(
2130-
[sys.executable, '-c', 'pass'],
2131-
[sys.executable, '-c', 'pass'],
2132-
universal_newlines=True
2133-
)
2134-
self.assertIn('universal_newlines', str(cm.exception))
2135-
self.assertIn('text=True', str(cm.exception))
2126+
def test_pipeline_universal_newlines(self):
2127+
"""Test that universal_newlines=True works like text=True"""
2128+
result = subprocess.run_pipeline(
2129+
[sys.executable, '-c', 'print("hello")'],
2130+
[sys.executable, '-c', 'import sys; print(sys.stdin.read().upper())'],
2131+
capture_output=True, universal_newlines=True
2132+
)
2133+
self.assertIsInstance(result.stdout, str)
2134+
self.assertIn('HELLO', result.stdout)
2135+
self.assertEqual(result.returncodes, [0, 0])
21362136

21372137
def test_pipeline_result_repr(self):
21382138
"""Test PipelineResult string representation"""

0 commit comments

Comments
 (0)