diff --git a/.changelog/010.yaml b/.changelog/010.yaml new file mode 100644 index 0000000..42f22d6 --- /dev/null +++ b/.changelog/010.yaml @@ -0,0 +1,7 @@ +name: console +ts: 2026-03-18 07:44:00.342408+00:00 +type: fix +author: Espen Albert +changelog_message: 'fix(console): Treat zero exit code exceptions as success in progress tracking' +message: 'fix(console): Treat zero exit code exceptions as success in progress tracking' +short_sha: fe4ffb diff --git a/ask_shell/_internal/rich_progress.py b/ask_shell/_internal/rich_progress.py index 47c867e..60f6c49 100644 --- a/ask_shell/_internal/rich_progress.py +++ b/ask_shell/_internal/rich_progress.py @@ -31,6 +31,15 @@ def transient_progress() -> Progress: ) +def _is_clean_exit(exc: BaseException | None) -> bool: + if exc is None: + return True + if isinstance(exc, SystemExit) and exc.code in (None, 0): + return True + exit_code = getattr(exc, "exit_code", None) + return exit_code == 0 + + def log_task_done( task: new_task, *, @@ -206,5 +215,6 @@ def __enter__(self) -> Self: return self def __exit__(self, exc_type, exc_val, exc_tb): - self.complete() # Ensure we mark it as complete - self.manager.remove_task(self, error=exc_val) + self.complete() + error = exc_val if not _is_clean_exit(exc_val) else None + self.manager.remove_task(self, error=error) diff --git a/ask_shell/public_api_test.py b/ask_shell/public_api_test.py index caae7f6..c3f1def 100644 --- a/ask_shell/public_api_test.py +++ b/ask_shell/public_api_test.py @@ -116,7 +116,9 @@ def test_multiple_attempts(tmp_path): """error might look weird due to flushing""" script_path = tmp_path / "attempt.py" script_path.write_text(_attempt_script) - result = run_and_wait(ShellConfig(shell_input=f"{PYTHON_EXEC} {script_path}", attempts=3)) + result = run_and_wait( + ShellConfig(shell_input=f"{PYTHON_EXEC} {script_path}", attempts=3, retry_initial_wait=0, retry_jitter=0) + ) assert result.clean_complete @@ -124,7 +126,9 @@ def test_not_enough_attempts(tmp_path): script_path = tmp_path / "attempt.py" script_path.write_text(_attempt_script) with pytest.raises(ShellError) as exc: - run_and_wait(ShellConfig(shell_input=f"{PYTHON_EXEC} {script_path}", attempts=2)) + run_and_wait( + ShellConfig(shell_input=f"{PYTHON_EXEC} {script_path}", attempts=2, retry_initial_wait=0, retry_jitter=0) + ) assert "attempt in script: 2/3" in exc.value.stdout assert exc.value.exit_code == 1 @@ -142,6 +146,8 @@ def never_retry(_): shell_input=f"{PYTHON_EXEC} {script_path}", attempts=4, should_retry=never_retry, + retry_initial_wait=0, + retry_jitter=0, ) ) assert "attempt in script: 1/3" in exc.value.stdout @@ -159,6 +165,8 @@ def retry_if_attempt(run: ShellRun): shell_input=f"{PYTHON_EXEC} {script_path}", attempts=4, should_retry=retry_if_attempt, + retry_initial_wait=0, + retry_jitter=0, ) ) assert "attempt in script: 3/3" in result.stdout @@ -177,6 +185,8 @@ def abort_on_first_failure(run: ShellRun) -> bool: shell_input=f"{PYTHON_EXEC} {script_path}", attempts=4, should_retry=abort_on_first_failure, + retry_initial_wait=0, + retry_jitter=0, ) ) assert isinstance(exc.value.base_error, AbortRetryError) diff --git a/docs/console/index.md b/docs/console/index.md index 99c3d66..08f14ed 100644 --- a/docs/console/index.md +++ b/docs/console/index.md @@ -130,7 +130,7 @@ def log_to_live(*objects, sep: str = ' ', end: str = '\n', style: str | Style | ### class: `new_task` -- [source](../../ask_shell/_internal/rich_progress.py#L153) +- [source](../../ask_shell/_internal/rich_progress.py#L162) > **Since:** 0.3.0 ```python diff --git a/docs/examples/shell/AbortRetryError.md b/docs/examples/shell/AbortRetryError.md index fe20534..b28ca7b 100644 --- a/docs/examples/shell/AbortRetryError.md +++ b/docs/examples/shell/AbortRetryError.md @@ -87,6 +87,9 @@ result = run_and_wait( shell_input=f"{PYTHON_EXEC} {tmp / 'run.py'}", attempts=3, should_retry=should_retry_transient, + retry_initial_wait=0.01, + retry_max_wait=0.1, + retry_jitter=0, ) ) print(result.stdout) diff --git a/docs/examples/shell/backoff.md b/docs/examples/shell/backoff.md index 320a6c5..63dc570 100644 --- a/docs/examples/shell/backoff.md +++ b/docs/examples/shell/backoff.md @@ -33,15 +33,15 @@ with TemporaryDirectory() as tmp: result = run_and_wait( f"{sys.executable} {script_path}", attempts=4, - retry_initial_wait=0.1, - retry_max_wait=10, + retry_initial_wait=0.01, + retry_max_wait=1, retry_jitter=0, ) elapsed = time.monotonic() - start print(result.stdout) #> ok on attempt 4 - print(f"waited at least 0.7s: {elapsed >= 0.7}") # 0.1+0.2+0.4 - #> waited at least 0.7s: True - print(f"waited less than 2s: {elapsed < 2}") - #> waited less than 2s: True + print(f"waited at least 0.07s: {elapsed >= 0.07}") # 0.01 + 0.02 + 0.04 + #> waited at least 0.07s: True + print(f"waited less than 1s: {elapsed < 1}") + #> waited less than 1s: True ```