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
8 changes: 8 additions & 0 deletions .changelog/020.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
name: shell
ts: 2026-06-01 15:20:08.795987+00:00
type: fix
author: Espen Albert
changelog_message: 'fix: Propagates live_print_scope to shell message_callbacks'
group: shell
message: 'fix: Propagates live_print_scope to shell message_callbacks'
short_sha: 6c245d
4 changes: 3 additions & 1 deletion ask_shell/_internal/_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,10 +415,12 @@ def _execute_run(shell_run: ShellRun) -> ShellRun:
config = shell_run.config
queue = shell_run._queue

ctx = shell_run._callback_context

def queue_consumer():
for message in queue:
try:
shell_run._on_event(message)
ctx.run(shell_run._on_event, message)
except BaseException as e:
logger.warning(f"Error processing message '{type(message).__name__}' for {shell_run}: {e!r}")
logger.exception(e)
Expand Down
35 changes: 34 additions & 1 deletion ask_shell/_internal/_run_test.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,43 @@
from rich.ansi import AnsiDecoder

from ask_shell._internal._run import _log_line
from ask_shell._internal._run import _log_line, run_and_wait
from ask_shell._internal.events import ShellRunStdOutput
from ask_shell._internal.live_print_context import get_live_print_context, live_print_scope


def test_log_line_strips_ansi_when_enabled():
decoder = AnsiDecoder()
raw = "\x1b[32mgreen\x1b[0m\n"
assert _log_line(raw, ansi_content=True, decoder=decoder) == "green\n"
assert _log_line(raw, ansi_content=False, decoder=decoder) == raw


def test_message_callback_inherits_live_print_scope() -> None:
seen: list[str | None] = []

def cb(message):
match message:
case ShellRunStdOutput(is_stdout=True):
ctx = get_live_print_context()
seen.append(ctx.prefix if ctx else None)
return False

with live_print_scope(prefix="[test] "):
run_and_wait("echo ok", message_callbacks=[cb], mute_shell_summary=True)
assert "[test] " in seen


def test_message_callback_nested_scope() -> None:
seen: list[str | None] = []

def cb(message):
match message:
case ShellRunStdOutput(is_stdout=True):
ctx = get_live_print_context()
seen.append(ctx.prefix if ctx else None)
return False

with live_print_scope(prefix="outer "):
with live_print_scope(prefix="inner "):
run_and_wait("echo ok", message_callbacks=[cb], mute_shell_summary=True)
assert seen == ["inner "]
2 changes: 2 additions & 0 deletions ask_shell/_internal/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import contextvars
import logging
import os
import platform
Expand Down Expand Up @@ -293,6 +294,7 @@ class ShellRun:
_stderr_log_path: Path | None = field(init=False, default=None)
_queue: ShellRunQueueT = field(init=False, default_factory=ClosableQueue)
_lock: RLock = field(init=False, default_factory=RLock)
_callback_context: contextvars.Context = field(default_factory=contextvars.copy_context, init=False)

@property
def has_started(self) -> bool:
Expand Down
31 changes: 30 additions & 1 deletion docs/examples/console/live_print_scope.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Prefix or suppress live-console scroll lines per scope, including a

`live_print_scope` stores per-scope metadata in a [`contextvars`](https://docs.python.org/3/library/contextvars.html) `ContextVar`. [`print_to_live`](../../console/index.md#print_to_live_def) and [`log_to_live`](../../console/index.md#log_to_live_def) read the active scope: they prepend `prefix` to each line and skip output when `suppress` is true.

Use it when parallel work (for example tfdo multi-directory orchestration) needs tagged scroll lines without editing every `print_to_live` call site. Pair it with [`run_pool`](../../shell/run_pool.md): `submit` copies the submitter's context into the worker thread.
Use it when parallel work (for example tfdo multi-directory orchestration) needs tagged scroll lines without editing every `print_to_live` call site. Pair it with [`run_pool`](../../shell/run_pool.md): `submit` copies the submitter's context into the worker thread. With [`run_and_wait`](../../shell/index.md), `message_callbacks` see the scope active when the run was created.

## Read the active scope

Expand Down Expand Up @@ -74,3 +74,32 @@ with run_pool("demo", total=2, pool_thread_count=2, max_concurrent_submits=2) as
print(sorted(results))
#> ['a', 'b']
```

## Context with run_and_wait and message_callbacks

`ShellRun` snapshots the active context when `run` / `run_and_wait` creates it. The queue consumer runs `message_callbacks` inside that snapshot, so handlers see the same `live_print_scope` as the caller even though dispatch runs on a pool thread.

```python
from ask_shell._internal._run import run_and_wait
from ask_shell._internal.events import ShellRunStdOutput
from ask_shell.console import get_live_print_context, live_print_scope

seen: list[str | None] = []


def on_stdout(message):
match message:
case ShellRunStdOutput(is_stdout=True):
ctx = get_live_print_context()
seen.append(ctx.prefix if ctx else None)
return False


with live_print_scope(prefix="[dir] "):
run_and_wait("echo ok", message_callbacks=[on_stdout])

print(seen)
#> ['[dir] ']
```

The snapshot is fixed for the life of that `ShellRun`; changing `live_print_scope` after `run_and_wait` returns does not affect callbacks for an in-flight run. Retries reuse the same snapshot.
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#L275)
- [source](../../ask_shell/_internal/models.py#L276)
> **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#L584)
- [source](../../ask_shell/_internal/_run.py#L586)
> **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#L591)
- [source](../../ask_shell/_internal/_run.py#L593)
> **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#L526)
- [source](../../ask_shell/_internal/models.py#L528)
- [Example: Stop retrying with a custom error by raising AbortRetryError from should_retry](../examples/shell/AbortRetryError.md)
> **Since:** 0.5.0

Expand Down
2 changes: 1 addition & 1 deletion docs/shell/run.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<!-- === DO_NOT_EDIT: pkg-ext run_def === -->
## function: run
- [source](../../ask_shell/_internal/_run.py#L451)
- [source](../../ask_shell/_internal/_run.py#L453)
> **Since:** 0.3.0

```python
Expand Down
2 changes: 1 addition & 1 deletion docs/shell/run_and_wait.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<!-- === DO_NOT_EDIT: pkg-ext run_and_wait_def === -->
## function: run_and_wait
- [source](../../ask_shell/_internal/_run.py#L517)
- [source](../../ask_shell/_internal/_run.py#L519)
> **Since:** 0.3.0

```python
Expand Down
2 changes: 1 addition & 1 deletion docs/shell/shellconfig.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<!-- === DO_NOT_EDIT: pkg-ext shellconfig_def === -->
## class: ShellConfig
- [source](../../ask_shell/_internal/models.py#L127)
- [source](../../ask_shell/_internal/models.py#L128)
> **Since:** 0.3.0

```python
Expand Down
2 changes: 1 addition & 1 deletion docs/shell/shellerror.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<!-- === DO_NOT_EDIT: pkg-ext shellerror_def === -->
## exception: ShellError
- [source](../../ask_shell/_internal/models.py#L490)
- [source](../../ask_shell/_internal/models.py#L492)
> **Since:** 0.3.0

```python
Expand Down
Loading