diff --git a/news/spec_raise_subproc_error.rst b/news/spec_raise_subproc_error.rst new file mode 100644 index 000000000..3cc23faae --- /dev/null +++ b/news/spec_raise_subproc_error.rst @@ -0,0 +1,23 @@ +**Added:** + +* Added ``spec.raise_subproc_error`` for fine-tuning exceptions via ``SpecModifierAlias`` (#5494). + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/tests/procs/test_specs.py b/tests/procs/test_specs.py index e78320540..d3ee5f1c3 100644 --- a/tests/procs/test_specs.py +++ b/tests/procs/test_specs.py @@ -3,7 +3,7 @@ import itertools import signal import sys -from subprocess import Popen +from subprocess import CalledProcessError, Popen import pytest @@ -163,6 +163,51 @@ def test_interrupted_process_returncode(xonsh_session, captured, interactive): assert p.proc.returncode == -signal.SIGINT +@skip_if_on_windows +def test_proc_raise_subproc_error(xonsh_session): + xonsh_session.env["RAISE_SUBPROC_ERROR"] = False + + specs = cmds_to_specs(cmd := [["ls"]], captured="stdout") + specs[-1].raise_subproc_error = True + exception = None + try: + (p := _run_command_pipeline(specs, cmd)).end() + assert p.proc.returncode == 0 + except Exception as e: + exception = e + assert exception is None + + specs = cmds_to_specs(cmd := [["ls", "nofile"]], captured="stdout") + specs[-1].raise_subproc_error = False + exception = None + try: + (p := _run_command_pipeline(specs, cmd)).end() + assert p.proc.returncode > 0 + except Exception as e: + exception = e + assert exception is None + + specs = cmds_to_specs(cmd := [["ls", "nofile"]], captured="stdout") + specs[-1].raise_subproc_error = True + exception = None + try: + (p := _run_command_pipeline(specs, cmd)).end() + except Exception as e: + assert p.proc.returncode > 0 + exception = e + assert isinstance(exception, CalledProcessError) + + xonsh_session.env["RAISE_SUBPROC_ERROR"] = True + specs = cmds_to_specs(cmd := [["ls", "nofile"]], captured="stdout") + exception = None + try: + (p := _run_command_pipeline(specs, cmd)).end() + except Exception as e: + assert p.proc.returncode > 0 + exception = e + assert isinstance(exception, CalledProcessError) + + @skip_if_on_windows @pytest.mark.parametrize( "suspended_pipeline", diff --git a/xonsh/procs/pipelines.py b/xonsh/procs/pipelines.py index 4aba18a41..6262c5c06 100644 --- a/xonsh/procs/pipelines.py +++ b/xonsh/procs/pipelines.py @@ -627,14 +627,21 @@ class CommandPipeline: spec = self.spec rtn = self.returncode - if rtn is None or rtn == 0 or not XSH.env.get("RAISE_SUBPROC_ERROR"): + if rtn is None or rtn == 0: return - try: - raise subprocess.CalledProcessError(rtn, spec.args, output=self.output) - finally: - # this is need to get a working terminal in interactive mode - self._return_terminal() + raise_subproc_error = spec.raise_subproc_error + if callable(raise_subproc_error): + raise_subproc_error = raise_subproc_error(spec, self) + if raise_subproc_error is False: + return + + if raise_subproc_error or XSH.env.get("RAISE_SUBPROC_ERROR", True): + try: + raise subprocess.CalledProcessError(rtn, spec.args, output=self.output) + finally: + # this is need to get a working terminal in interactive mode + self._return_terminal() # # Properties diff --git a/xonsh/procs/specs.py b/xonsh/procs/specs.py index ee4e5d849..0d31396ba 100644 --- a/xonsh/procs/specs.py +++ b/xonsh/procs/specs.py @@ -421,6 +421,7 @@ class SubprocSpec: self.stack = None self.spec_modifiers = [] # List of SpecModifierAlias objects that applied to spec. self.output_format = XSH.env.get("XONSH_SUBPROC_OUTPUT_FORMAT", "stream_lines") + self.raise_subproc_error = None # Spec-based $RAISE_SUBPROC_ERROR. def __str__(self): s = self.__class__.__name__ + "(" + str(self.cmd) + ", "