From 5e3f535141d585356cf9a394a8ac17a5d858efbf Mon Sep 17 00:00:00 2001 From: zigai Date: Fri, 23 Oct 2026 21:48:16 +0200 Subject: [PATCH] fix(runtime): separate CommandError from InterfacyError --- interfacy/common/console.py | 11 ++++++----- interfacy/exceptions.py | 8 ++++++-- interfacy/runtime/invocation.py | 6 +++--- tests/unit/runtime/test_invocation.py | 4 +++- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/interfacy/common/console.py b/interfacy/common/console.py index 1f04f7a..876f469 100644 --- a/interfacy/common/console.py +++ b/interfacy/common/console.py @@ -5,8 +5,6 @@ from stdl.st import colored -from interfacy.exceptions import CommandError - def info(message: str) -> None: print(message, file=sys.stdout) @@ -39,12 +37,14 @@ def log_exception(tag: str, exc: BaseException, *, full_traceback: bool) -> None ] error("\n".join(["Traceback (most recent call last):", *frames])) - message = colored( - str(exc) if isinstance(exc, CommandError) else type(exc).__name__, color="red" - ) + message = colored(type(exc).__name__, color="red") error(message) +def log_command_error(exc: BaseException) -> None: + error(colored(str(exc), color="red")) + + def log_interrupt(*, silent: bool) -> None: if silent: return @@ -57,6 +57,7 @@ def log_interrupt(*, silent: bool) -> None: "error", "info", "log", + "log_command_error", "log_error", "log_exception", "log_interrupt", diff --git a/interfacy/exceptions.py b/interfacy/exceptions.py index b97e066..e82c86f 100644 --- a/interfacy/exceptions.py +++ b/interfacy/exceptions.py @@ -35,8 +35,12 @@ class InterfacyError(Exception): """Base exception for Interfacy errors.""" -class CommandError(InterfacyError): - """A command failure whose message is safe to display verbatim to the user.""" +class CommandError(Exception): + """Raise from a command for a failure whose message is written for the user. + + ``run()`` prints the message, without chained causes, and exits with + ``ExitCode.COMMAND_FAILED``. Other command exceptions print only their type. + """ class InterfacyExit(BaseException): diff --git a/interfacy/runtime/invocation.py b/interfacy/runtime/invocation.py index 029cd35..f0b9a30 100644 --- a/interfacy/runtime/invocation.py +++ b/interfacy/runtime/invocation.py @@ -7,7 +7,7 @@ from inspect import isawaitable from typing import Any, NoReturn -from interfacy.common.console import error +from interfacy.common.console import error, log_command_error from interfacy.common.resources import ResourceStack from interfacy.declarations.executable_flags import ExecutableAction from interfacy.exceptions import ( @@ -77,8 +77,6 @@ def __init__(self, code: int, error: Exception) -> None: def _execution_error_code(error: Exception) -> ExitCode: - if isinstance(error, CommandError): - return ExitCode.COMMAND_FAILED if isinstance(error, PipeInputError): return ExitCode.USAGE if isinstance(error, _CONFIGURATION_ERRORS): @@ -148,6 +146,8 @@ def run(self, parse: ParseStep, execute: ExecuteStep) -> NoReturn: exit_code = rendering_error.code else: error(rendered.rstrip()) + elif isinstance(failure, CommandError): + log_command_error(failure) else: self._policy.log_exception(failure) else: diff --git a/tests/unit/runtime/test_invocation.py b/tests/unit/runtime/test_invocation.py index ef61c67..b5ded9a 100644 --- a/tests/unit/runtime/test_invocation.py +++ b/tests/unit/runtime/test_invocation.py @@ -131,7 +131,9 @@ def command() -> None: ) assert "private note" not in captured.err assert "private cause" not in captured.err - assert ("Traceback" in captured.err) == full_traceback + assert ("Traceback" in captured.err) == ( + full_traceback and not issubclass(failure_type, CommandError) + ) @pytest.mark.parametrize("mode", ["sync", "async"])