Skip to content

Commit df8f082

Browse files
gpsheadclaude
andcommitted
Fix memoryview and closed stdin handling in _communicate_streams_posix
Apply the same fixes from Popen._communicate() to _communicate_streams_posix for run_pipeline(): 1. Handle non-byte memoryview input by casting to byte view (gh-134453): Non-byte memoryviews (e.g., int32 arrays) had incorrect length tracking because len() returns element count, not byte count. Now cast to "b" view for correct progress tracking. 2. Handle ValueError on stdin.flush() when stdin is closed (gh-74389): Ignore ValueError from flush() if stdin is already closed, matching the BrokenPipeError handling. Add tests for memoryview input to run_pipeline: - test_pipeline_memoryview_input: basic byte memoryview - test_pipeline_memoryview_input_nonbyte: int32 array memoryview Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d420f29 commit df8f082

File tree

2 files changed

+54
-2
lines changed

2 files changed

+54
-2
lines changed

Lib/subprocess.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -540,15 +540,25 @@ def _communicate_streams_posix(stdin, input_data, read_streams,
540540
stdin.flush()
541541
except BrokenPipeError:
542542
pass
543+
except ValueError:
544+
# ignore ValueError: I/O operation on closed file.
545+
if not stdin.closed:
546+
raise
543547
if not input_data:
544548
try:
545549
stdin.close()
546550
except BrokenPipeError:
547551
pass
548552
stdin = None # Don't register with selector
549553

550-
# Prepare input data
551-
input_view = memoryview(input_data) if input_data else None
554+
# Prepare input data - cast to bytes view for correct length tracking
555+
if input_data:
556+
if not isinstance(input_data, memoryview):
557+
input_view = memoryview(input_data)
558+
else:
559+
input_view = input_data.cast("b") # byte view required
560+
else:
561+
input_view = None
552562

553563
with _PopenSelector() as selector:
554564
if stdin and input_data:

Lib/test/test_subprocess.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2019,6 +2019,48 @@ def test_pipeline_with_input(self):
20192019
self.assertEqual(result.stdout.strip(), '5')
20202020
self.assertEqual(result.returncodes, [0, 0])
20212021

2022+
def test_pipeline_memoryview_input(self):
2023+
"""Test pipeline with memoryview input (byte elements)"""
2024+
test_data = b"Hello, memoryview pipeline!"
2025+
mv = memoryview(test_data)
2026+
result = subprocess.run_pipeline(
2027+
[sys.executable, '-c',
2028+
'import sys; sys.stdout.buffer.write(sys.stdin.buffer.read())'],
2029+
[sys.executable, '-c',
2030+
'import sys; sys.stdout.buffer.write(sys.stdin.buffer.read().upper())'],
2031+
input=mv, capture_output=True
2032+
)
2033+
self.assertEqual(result.stdout, test_data.upper())
2034+
self.assertEqual(result.returncodes, [0, 0])
2035+
2036+
def test_pipeline_memoryview_input_nonbyte(self):
2037+
"""Test pipeline with non-byte memoryview input (e.g., int32).
2038+
2039+
This tests the fix for gh-134453 where non-byte memoryviews
2040+
had incorrect length tracking on POSIX, causing data truncation.
2041+
"""
2042+
import array
2043+
# Create an array of 32-bit integers large enough to trigger
2044+
# chunked writing behavior (> PIPE_BUF)
2045+
pipe_buf = getattr(select, 'PIPE_BUF', 512)
2046+
# Each 'i' element is 4 bytes, need more than pipe_buf bytes total
2047+
num_elements = (pipe_buf // 4) + 100
2048+
test_array = array.array('i', [0x41424344 for _ in range(num_elements)])
2049+
expected_bytes = test_array.tobytes()
2050+
mv = memoryview(test_array)
2051+
2052+
result = subprocess.run_pipeline(
2053+
[sys.executable, '-c',
2054+
'import sys; sys.stdout.buffer.write(sys.stdin.buffer.read())'],
2055+
[sys.executable, '-c',
2056+
'import sys; data = sys.stdin.buffer.read(); '
2057+
'sys.stdout.buffer.write(data)'],
2058+
input=mv, capture_output=True
2059+
)
2060+
self.assertEqual(result.stdout, expected_bytes,
2061+
msg=f"{len(result.stdout)=} != {len(expected_bytes)=}")
2062+
self.assertEqual(result.returncodes, [0, 0])
2063+
20222064
def test_pipeline_bytes_mode(self):
20232065
"""Test pipeline in binary mode"""
20242066
result = subprocess.run_pipeline(

0 commit comments

Comments
 (0)