Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .changelog/014.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: ShellRunSummary
ts: 2026-05-15 13:24:34.831384+00:00
type: make_public
details: created in settings.py
full_path: settings.ShellRunSummary
group: __ROOT__
---
name: AskShellSettings
ts: 2026-05-15 13:24:35.322058+00:00
type: additional_change
auto_generated: true
change_kind: optional_field_added
details: 'added optional field ''shell_run_summary'' (default: <ShellRunSummary.ALL: ''all''>)'
field_name: shell_run_summary
group: __ROOT__
---
name: ShellConfig
ts: 2026-05-15 13:24:35.322069+00:00
type: additional_change
auto_generated: true
change_kind: optional_field_added
details: 'added optional field ''mute_shell_summary'' (default: False)'
field_name: mute_shell_summary
group: shell
---
name: run
ts: 2026-05-15 13:24:35.322077+00:00
type: additional_change
auto_generated: true
change_kind: optional_param_added
details: 'added optional param ''mute_shell_summary'' (default: None)'
field_name: mute_shell_summary
group: shell
---
name: run_and_wait
ts: 2026-05-15 13:24:35.322079+00:00
type: additional_change
auto_generated: true
change_kind: optional_param_added
details: 'added optional param ''mute_shell_summary'' (default: None)'
field_name: mute_shell_summary
group: shell
1 change: 1 addition & 0 deletions .groups.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ groups:
- settings
owned_refs:
- settings.AskShellSettings
- settings.ShellRunSummary
- name: ask
owned_modules:
- _internal.interactive
Expand Down
2 changes: 2 additions & 0 deletions ask_shell/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from ask_shell import shell_events
from ask_shell import ask
from ask_shell.settings import AskShellSettings
from ask_shell.settings import ShellRunSummary

VERSION = "0.6.0"
__all__ = [
Expand All @@ -13,4 +14,5 @@
"shell_events",
"ask",
"AskShellSettings",
"ShellRunSummary",
]
4 changes: 4 additions & 0 deletions ask_shell/_internal/_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,7 @@ def run(
start_timeout: float | None = None,
terminal_width: int | None = None,
skip_interactive_check: bool | None = None,
mute_shell_summary: bool | None = None,
) -> ShellRun:
config = _as_config(
config,
Expand All @@ -510,6 +511,7 @@ def run(
skip_os_env=skip_os_env,
terminal_width=terminal_width,
skip_interactive_check=skip_interactive_check,
mute_shell_summary=mute_shell_summary,
)
assert not config.user_input, (
"run() does not support user_input (only 1 should be active at a time), use run_and_wait() instead"
Expand Down Expand Up @@ -549,6 +551,7 @@ def run_and_wait(
user_input: bool | None = None,
terminal_width: int | None = None,
skip_interactive_check: bool | None = None,
mute_shell_summary: bool | None = None,
) -> ShellRun:
config = _as_config(
script,
Expand Down Expand Up @@ -576,6 +579,7 @@ def run_and_wait(
terminal_width=terminal_width,
skip_interactive_check=skip_interactive_check,
skip_progress_output=skip_progress_output,
mute_shell_summary=mute_shell_summary,
)
run = ShellRun(config)
future = _pool.submit(_execute_run, run)
Expand Down
3 changes: 3 additions & 0 deletions ask_shell/_internal/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@ class ShellConfig(Entity):
default=False,
description="Skip transitive std out/err output, useful for large outputs that are not needed in the logs when running parallel scripts",
)
mute_shell_summary: bool = Field(
default=False, description="Skip shell summary logging useful when you want to ✅ or ❌ in the logs"
)
terminal_width: int | None = 999

# advanced settings
Expand Down
8 changes: 7 additions & 1 deletion ask_shell/_internal/rich_progress.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,16 @@ def log_task_done(
error: BaseException | None = None,
description_override: str | None = None,
extra_parts: list[str] | None = None,
soft_failure: bool = False,
):
exit_comji = "❌" if force_error or error is not None else "✅"
description = description_override or task.description
log_call = logger.info if exit_comji == "✅" else logger.error
if exit_comji == "✅":
log_call = logger.info
elif soft_failure:
log_call = logger.warning
else:
log_call = logger.error
message_parts = [f"{exit_comji} {description}"]
if rich_task := task._rich_task:
if finish_time := rich_task.finished_time:
Expand Down
6 changes: 6 additions & 0 deletions ask_shell/_internal/rich_run_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
ShellRunEventT,
)
from ask_shell._internal.rich_progress import ProgressManager, log_task_done, new_task
from ask_shell.settings import ShellRunSummary


def _deque_default() -> deque[str]:
Expand Down Expand Up @@ -144,11 +145,16 @@ def remove_run(self, run: ShellRun, error: BaseException | None = None) -> None:
task.update(stderr=run_info.stderr_str)
task.__exit__(None, None, None)
run = run_info.run
if run.config.mute_shell_summary:
return
if run.config.settings.shell_run_summary == ShellRunSummary.ERRORS_ONLY and run.clean_complete:
return
log_task_done(
task,
force_error=not run.clean_complete,
description_override=f"'{run.config.shell_input}'",
extra_parts=[
"" if run.current_attempt == 1 else f"attempt {run.current_attempt}",
],
soft_failure=run.config.allow_non_zero_exit and not run.clean_complete,
)
71 changes: 69 additions & 2 deletions ask_shell/_internal/rich_run_state_test.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import logging
from unittest.mock import Mock, patch

import pytest

from ask_shell._internal.events import ShellRunAfter, ShellRunStdOutput
from ask_shell._internal.models import (
ShellConfig,
ShellRun,
)
from ask_shell._internal.rich_live import get_live
from ask_shell._internal.rich_progress import log_task_done
from ask_shell._internal.rich_run_state import _RunState
from ask_shell.settings import ShellRunSummary

logger = logging.getLogger(__name__)
_module = _RunState.__module__


def test_run_with_output_is_logged_to_console(settings, capture_console, caplog):
Expand Down Expand Up @@ -79,3 +83,66 @@ def test_skip_progress_output(settings, capture_console):
state.remove_run(run, error=Exception("Test error"))
output = capture_console.end_capture()
assert "..." in output # Output is skipped


@pytest.mark.parametrize(
("summary", "exit_code", "expect_log"),
[
(ShellRunSummary.ERRORS_ONLY, 0, False),
(ShellRunSummary.ERRORS_ONLY, 1, True),
(ShellRunSummary.ALL, 0, True),
],
)
@patch(f"{_module}.{log_task_done.__name__}")
def test_remove_run_respects_shell_run_summary(mock_log_done, settings, summary, exit_code, expect_log):
cfg = ShellConfig(
shell_input='echo "x"',
settings=settings.model_copy(update={"shell_run_summary": summary}),
)
run = ShellRun(config=cfg)
proc = Mock(spec=["returncode"])
proc.returncode = exit_code
run.p_open = proc
state = _RunState()
state.add_run(run)
state.remove_run(run, error=None)
if expect_log:
mock_log_done.assert_called_once()
else:
mock_log_done.assert_not_called()


@patch(f"{_module}.{log_task_done.__name__}")
def test_remove_run_mute_shell_summary_skips_log(mock_log_done, settings):
cfg = ShellConfig(
shell_input='echo "x"',
settings=settings,
mute_shell_summary=True,
)
run = ShellRun(config=cfg)
proc = Mock(spec=["returncode"])
proc.returncode = 1
run.p_open = proc
state = _RunState()
state.add_run(run)
state.remove_run(run, error=None)
mock_log_done.assert_not_called()


@patch(f"{_module}.{log_task_done.__name__}")
def test_remove_run_allow_non_zero_exit_uses_soft_failure(mock_log_done, settings):
cfg = ShellConfig(
shell_input='echo "x"',
settings=settings,
allow_non_zero_exit=True,
)
run = ShellRun(config=cfg)
proc = Mock(spec=["returncode"])
proc.returncode = 1
run.p_open = proc
state = _RunState()
state.add_run(run)
state.remove_run(run, error=None)
mock_log_done.assert_called_once()
_, kwargs = mock_log_done.call_args
assert kwargs["soft_failure"]
5 changes: 4 additions & 1 deletion ask_shell/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def capture_console() -> Console: # type: ignore


def create_run_mocked_config(user_input: bool = False) -> ShellRun:
"""avoid ShellConfig validation erro"""
mock_settings = AskShellSettings.for_testing(global_callback_strings=[], remove_os_secrets=False)
return ShellRun(
config=Mock(
spec=ShellConfig,
Expand All @@ -88,5 +88,8 @@ def create_run_mocked_config(user_input: bool = False) -> ShellRun:
print_prefix="Mocked Run",
shell_input="echo 'Mocked Run Output'",
skip_progress_output=False,
mute_shell_summary=False,
allow_non_zero_exit=False,
settings=mock_settings,
)
)
11 changes: 11 additions & 0 deletions ask_shell/settings.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging
import os
from datetime import datetime, timedelta
from enum import StrEnum
from functools import cached_property, lru_cache
from pathlib import Path
from pydoc import locate
Expand Down Expand Up @@ -90,6 +91,11 @@ def as_upper(v: str) -> str:
]


class ShellRunSummary(StrEnum):
ALL = "all"
ERRORS_ONLY = "errors_only"


class AskShellSettings(StaticSettings):
model_config = ConfigDict(populate_by_name=True) # type: ignore
log_level: LogLevelIgnoredCase = "UNSET"
Expand Down Expand Up @@ -136,6 +142,11 @@ class AskShellSettings(StaticSettings):
description="Runs once If `run_logs_dir` is not set. Can be 'yesterday' or a date string like '2023-01-01'. Will clean all logs up until the specified date but not that date itself.",
alias=f"{ENV_PREFIX}RUN_LOGS_CLEAN",
)
ENV_NAME_SHELL_RUN_SUMMARY: ClassVar[str] = f"{ENV_PREFIX}SHELL_RUN_SUMMARY"
shell_run_summary: ShellRunSummary = Field(
default=ShellRunSummary.ALL,
alias=ENV_NAME_SHELL_RUN_SUMMARY,
)

@model_validator(mode="after")
def ensure_vars_set(self) -> Self:
Expand Down
4 changes: 3 additions & 1 deletion docs/_root/askshellsettings.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<!-- === DO_NOT_EDIT: pkg-ext askshellsettings_def === -->
## class: AskShellSettings
- [source](../../ask_shell/settings.py#L93)
- [source](../../ask_shell/settings.py#L99)
> **Since:** 0.3.0

```python
Expand All @@ -19,6 +19,7 @@ class AskShellSettings(StaticSettings):
remove_os_secrets: bool = ...
run_logs_dir: Path | None = None
run_logs_clean: str = 'yesterday'
shell_run_summary: ShellRunSummary = <ShellRunSummary.ALL: 'all'>
```
<!-- === OK_EDIT: pkg-ext askshellsettings_def === -->

Expand Down Expand Up @@ -69,6 +70,7 @@ class AskShellSettings(StaticSettings):

| Version | Change |
|---------|--------|
| unreleased | added optional field 'shell_run_summary' (default: <ShellRunSummary.ALL: 'all'>) |
| 0.6.0 | field 'thread_count' default: 50 -> 100 |
| 0.3.2 | field 'run_logs_dir' default added: None |
| 0.3.2 | field 'STATIC_DIR' default added: None |
Expand Down
24 changes: 23 additions & 1 deletion docs/_root/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,26 @@

<!-- === DO_NOT_EDIT: pkg-ext symbols === -->
- [AskShellSettings](./askshellsettings.md)
<!-- === OK_EDIT: pkg-ext symbols === -->
- [`ShellRunSummary`](#shellrunsummary_def)
<!-- === OK_EDIT: pkg-ext symbols === -->
<!-- === DO_NOT_EDIT: pkg-ext symbol_details_header === -->
## Symbol Details
<!-- === OK_EDIT: pkg-ext symbol_details_header === -->
<!-- === DO_NOT_EDIT: pkg-ext shellrunsummary_def === -->
<a id="shellrunsummary_def"></a>

### class: `ShellRunSummary`
- [source](../../ask_shell/settings.py#L94)
> **Since:** unreleased

```python
class ShellRunSummary(StrEnum):
...
```

### Changes

| Version | Change |
|---------|--------|
| unreleased | Made public |
<!-- === OK_EDIT: pkg-ext shellrunsummary_def === -->
2 changes: 1 addition & 1 deletion docs/console/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ def log_to_live(*objects, sep: str = ' ', end: str = '\n', style: str | Style |
<a id="new_task_def"></a>

### class: `new_task`
- [source](../../ask_shell/_internal/rich_progress.py#L162)
- [source](../../ask_shell/_internal/rich_progress.py#L168)
> **Since:** 0.3.0

```python
Expand Down
8 changes: 4 additions & 4 deletions docs/shell/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
<a id="shellrun_def"></a>

### class: `ShellRun`
- [source](../../ask_shell/_internal/models.py#L272)
- [source](../../ask_shell/_internal/models.py#L275)
> **Since:** 0.3.0

```python
Expand Down Expand Up @@ -120,7 +120,7 @@ def kill_all_runs(immediate: bool = False, reason: str = '', abort_timeout: floa
<a id="run_error_def"></a>

### function: `run_error`
- [source](../../ask_shell/_internal/_run.py#L589)
- [source](../../ask_shell/_internal/_run.py#L593)
> **Since:** 0.3.0

```python
Expand Down Expand Up @@ -156,7 +156,7 @@ def stop_runs_and_pool(reason: str = 'atexit', immediate: bool = False):
<a id="wait_on_ok_errors_def"></a>

### function: `wait_on_ok_errors`
- [source](../../ask_shell/_internal/_run.py#L596)
- [source](../../ask_shell/_internal/_run.py#L600)
> **Since:** 0.3.0

```python
Expand All @@ -174,7 +174,7 @@ def wait_on_ok_errors(*runs, timeout: float | None = None, skip_kill_timeouts: b
<a id="abortretryerror_def"></a>

### exception: `AbortRetryError`
- [source](../../ask_shell/_internal/models.py#L523)
- [source](../../ask_shell/_internal/models.py#L526)
- [Example: Stop retrying with a custom error by raising AbortRetryError from should_retry](../examples/shell/AbortRetryError.md)
> **Since:** 0.5.0

Expand Down
Loading
Loading