Skip to content

Commit 6dd8dad

Browse files
committed
gh-47798: run_pipeline: identify the failing command in error output
Annotate spawn-time OSError with an exception note naming the PipelineCommand and its index, so a FileNotFoundError mid-pipeline tells the caller which command failed; the exception type is unchanged. Reformat PipelineError.__str__ as "argv (commands[i]) detail" so the argv leads and the index is unambiguously a Python list subscript.
1 parent eff75cc commit 6dd8dad

3 files changed

Lines changed: 16 additions & 6 deletions

File tree

Doc/library/subprocess.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ underlying :class:`Popen` interface can be used directly.
391391
... )
392392
Traceback (most recent call last):
393393
...
394-
subprocess.PipelineError: Pipeline failed: command 1 ['false'] returned 1
394+
subprocess.PipelineError: Pipeline failed: ['false'] (commands[1]) returned 1
395395

396396
.. versionadded:: next
397397

Lib/subprocess.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -241,8 +241,8 @@ def __str__(self):
241241
cmd_display = cmd.args
242242
else:
243243
cmd_display = cmd
244-
parts.append(f"command {i} {cmd_display!r} {detail}")
245-
return f"Pipeline failed: {', '.join(parts)}"
244+
parts.append(f"{cmd_display!r} (commands[{i}]) {detail}")
245+
return f"Pipeline failed: {'; '.join(parts)}"
246246

247247

248248
if _mswindows:
@@ -1199,8 +1199,14 @@ def run_pipeline(*commands, input=None, capture_output=False, timeout=None,
11991199
if cmd.shell:
12001200
cmd_kwargs['shell'] = True
12011201

1202-
proc = Popen(cmd.args, stdin=proc_stdin, stdout=proc_stdout,
1203-
stderr=proc_stderr, **cmd_kwargs)
1202+
try:
1203+
proc = Popen(cmd.args, stdin=proc_stdin, stdout=proc_stdout,
1204+
stderr=proc_stderr, **cmd_kwargs)
1205+
except OSError as e:
1206+
e.add_note(
1207+
f'raised while starting {cmd!r} '
1208+
f'(run_pipeline commands[{i}])')
1209+
raise
12041210
processes.append(proc)
12051211

12061212
# Close the parent's copy of the previous process's stdout

Lib/test/test_subprocess.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2360,7 +2360,7 @@ def test_pipeline_spawn_failure_cleans_up(self):
23602360
promptly rather than hanging until command 0's sleep finishes.
23612361
"""
23622362
start = time.monotonic()
2363-
with self.assertRaises(NONEXISTING_ERRORS):
2363+
with self.assertRaises(NONEXISTING_ERRORS) as cm:
23642364
subprocess.run_pipeline(
23652365
[sys.executable, '-c', 'import time; time.sleep(60)'],
23662366
NONEXISTING_CMD,
@@ -2370,6 +2370,10 @@ def test_pipeline_spawn_failure_cleans_up(self):
23702370
self.assertLess(elapsed, 30,
23712371
"run_pipeline did not promptly clean up the running first "
23722372
"command after the second command failed to spawn")
2373+
notes = getattr(cm.exception, '__notes__', [])
2374+
self.assertTrue(
2375+
any('commands[1]' in n for n in notes),
2376+
f'expected a which-command note on the OSError; got {notes!r}')
23732377

23742378
def test_pipeline_explicit_stdout_pipe(self):
23752379
"""Test pipeline with explicit stdout=PIPE"""

0 commit comments

Comments
 (0)