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
20 changes: 20 additions & 0 deletions .changelog/019.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: get_live_print_context
ts: 2026-06-01 12:26:33.762426+00:00
type: make_public
details: created in _internal/live_print_context.py
full_path: _internal.live_print_context.get_live_print_context
group: console
---
name: live_print_scope
ts: 2026-06-01 12:26:33.764529+00:00
type: make_public
details: created in _internal/live_print_context.py
full_path: _internal.live_print_context.live_print_scope
group: console
---
name: LivePrintContext
ts: 2026-06-01 12:26:37.231694+00:00
type: make_public
details: created in _internal/live_print_context.py
full_path: _internal.live_print_context.LivePrintContext
group: console
4 changes: 4 additions & 0 deletions .groups.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,16 @@ groups:
- name: console
owned_modules:
- _internal._run_env
- _internal.live_print_context
- _internal.rich_live
- _internal.rich_progress
- _internal.typer_command
owned_refs:
- _internal._run_env.disable_interactive_shell
- _internal._run_env.interactive_shell
- _internal.live_print_context.LivePrintContext
- _internal.live_print_context.get_live_print_context
- _internal.live_print_context.live_print_scope
- _internal.rich_live.RemoveLivePart
- _internal.rich_live.add_renderable
- _internal.rich_live.get_live_console
Expand Down
28 changes: 28 additions & 0 deletions ask_shell/_internal/live_print_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from __future__ import annotations

from contextlib import contextmanager
from contextvars import ContextVar
from dataclasses import dataclass
from typing import Iterator


@dataclass
class LivePrintContext:
prefix: str = ""
suppress: bool = False


_live_print_ctx: ContextVar[LivePrintContext | None] = ContextVar("live_print_ctx", default=None)


def get_live_print_context() -> LivePrintContext | None:
return _live_print_ctx.get()


@contextmanager
def live_print_scope(*, prefix: str = "", suppress: bool = False) -> Iterator[None]:
token = _live_print_ctx.set(LivePrintContext(prefix=prefix, suppress=suppress))
try:
yield
finally:
_live_print_ctx.reset(token)
37 changes: 37 additions & 0 deletions ask_shell/_internal/live_print_context_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from unittest.mock import MagicMock, patch

from ask_shell._internal.live_print_context import live_print_scope
from ask_shell._internal.rich_live import log_to_live, print_to_live


@patch("ask_shell._internal.rich_live.get_live_console")
def test_live_print_scope_prefix(mock_get_console: MagicMock) -> None:
console = MagicMock()
mock_get_console.return_value = console
with live_print_scope(prefix="[dir] "):
print_to_live("x")
console.print.assert_called_once()
assert console.print.call_args[0][0] == "[dir] "


@patch("ask_shell._internal.rich_live.get_live_console")
def test_live_print_scope_suppress(mock_get_console: MagicMock) -> None:
console = MagicMock()
mock_get_console.return_value = console
with live_print_scope(suppress=True):
print_to_live("x")
log_to_live("y")
console.print.assert_not_called()
console.log.assert_not_called()


@patch("ask_shell._internal.rich_live.get_live_console")
def test_nested_live_print_scope(mock_get_console: MagicMock) -> None:
console = MagicMock()
mock_get_console.return_value = console
with live_print_scope(prefix="outer "):
print_to_live("a")
with live_print_scope(prefix="inner "):
print_to_live("b")
print_to_live("c")
assert [c[0][0] for c in console.print.call_args_list] == ["outer ", "inner ", "outer "]
22 changes: 20 additions & 2 deletions ask_shell/_internal/rich_live.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from zero_3rdparty.id_creator import simple_id

from ask_shell._internal._run_env import resolve_terminal_dimensions
from ask_shell._internal.live_print_context import get_live_print_context

_live: Live | None = None
_lock = RLock()
Expand Down Expand Up @@ -180,6 +181,17 @@ def get_live_console() -> Console:
return get_live().console


def _objects_for_live_print(*objects: Any) -> tuple[Any, ...] | None:
ctx = get_live_print_context()
if ctx is None:
return objects
if ctx.suppress:
return None
if ctx.prefix:
return (ctx.prefix, *objects)
return objects


def print_to_live(
*objects: Any,
sep: str = " ",
Expand All @@ -197,8 +209,11 @@ def print_to_live(
soft_wrap: bool | None = None,
new_line_start: bool = False,
):
live_objects = _objects_for_live_print(*objects)
if live_objects is None:
return
get_live_console().print(
*objects,
*live_objects,
sep=sep,
end=end,
style=style,
Expand Down Expand Up @@ -228,8 +243,11 @@ def log_to_live(
log_locals: bool = False,
_stack_offset: int = 1,
) -> None:
live_objects = _objects_for_live_print(*objects)
if live_objects is None:
return
get_live_console().log(
*objects,
*live_objects,
sep=sep,
end=end,
style=style,
Expand Down
4 changes: 3 additions & 1 deletion ask_shell/_internal/run_pool.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import contextvars
import logging
import time
from concurrent.futures import Future, ThreadPoolExecutor
Expand Down Expand Up @@ -95,7 +96,8 @@ def submit(self, fn: SubmitFunc[T_co], /, *args, **kwargs) -> Future[T_co]:
sleep_time=self.sleep_time,
sleep_callback=self.sleep_callback,
)
future = self.pool.submit(fn, *args, **kwargs)
ctx = contextvars.copy_context()
future = self.pool.submit(ctx.run, fn, *args, **kwargs)
future.add_done_callback(self._on_submit_done)
with self._lock:
self._futures.append(future)
Expand Down
33 changes: 28 additions & 5 deletions ask_shell/_internal/run_pool_test.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from concurrent.futures import Future, ThreadPoolExecutor
from threading import Thread
from threading import Barrier, Thread
from unittest.mock import MagicMock, patch

from ask_shell._internal._run import get_pool, max_run_count_for_workers, wait_if_many_runs
from ask_shell._internal.live_print_context import get_live_print_context, live_print_scope
from ask_shell._internal.rich_progress import new_task
from ask_shell._internal.run_pool import run_pool

Expand Down Expand Up @@ -87,7 +88,7 @@ def task_fn():

mock_pool = MagicMock(spec=ThreadPoolExecutor)
mock_pool._max_workers = 20
mock_pool.submit = MagicMock(side_effect=lambda fn, *a, **kw: _immediate_future(fn, *a, **kw))
mock_pool.submit = MagicMock(side_effect=_submit_via_context_run)

with patch(f"{_module}.{get_pool.__name__}", return_value=mock_pool):
rp = run_pool(task_name="single", total=1, max_concurrent_submits=1, threads_used_per_submit=4, sleep_time=0.01)
Expand Down Expand Up @@ -121,7 +122,7 @@ def task_fn(index: int):

mock_pool = MagicMock(spec=ThreadPoolExecutor)
mock_pool._max_workers = 20
mock_pool.submit = MagicMock(side_effect=lambda fn, *a, **kw: _immediate_future(fn, *a, **kw))
mock_pool.submit = MagicMock(side_effect=_submit_via_context_run)

with patch(f"{_module}.{get_pool.__name__}", return_value=mock_pool):
rp = run_pool(task_name="multi", total=3, max_concurrent_submits=3, threads_used_per_submit=4, sleep_time=0.01)
Expand All @@ -146,8 +147,30 @@ def do_submits():
assert sorted(results) == [0, 1, 2]


def _immediate_future(fn, *args, **kwargs):
"""Run fn synchronously and return a resolved Future with done callbacks."""
def test_run_pool_submit_copies_context() -> None:
results: list[str] = []
barrier = Barrier(2)

def _read_prefix() -> None:
barrier.wait()
ctx = get_live_print_context()
results.append(ctx.prefix if ctx else "")

with run_pool("test", total=2, pool_thread_count=2, max_concurrent_submits=2) as pool:
with live_print_scope(prefix="a"):
f_a = pool.submit(_read_prefix)
with live_print_scope(prefix="b"):
f_b = pool.submit(_read_prefix)
f_a.result()
f_b.result()
assert sorted(results) == ["a", "b"]


def _submit_via_context_run(run_fn, fn, /, *args, **kwargs):
return _immediate_future(run_fn, fn, *args, **kwargs)


def _immediate_future(fn, /, *args, **kwargs):
fut = Future()
try:
result = fn(*args, **kwargs)
Expand Down
6 changes: 6 additions & 0 deletions ask_shell/console.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Generated by pkg-ext
from ask_shell._internal._run_env import disable_interactive_shell as _disable_interactive_shell
from ask_shell._internal._run_env import interactive_shell as _interactive_shell
from ask_shell._internal.live_print_context import LivePrintContext as _LivePrintContext
from ask_shell._internal.live_print_context import get_live_print_context as _get_live_print_context
from ask_shell._internal.live_print_context import live_print_scope as _live_print_scope
from ask_shell._internal.rich_live import RemoveLivePart as _RemoveLivePart
from ask_shell._internal.rich_live import add_renderable as _add_renderable
from ask_shell._internal.rich_live import get_live_console as _get_live_console
Expand All @@ -11,6 +14,9 @@

disable_interactive_shell = _disable_interactive_shell
interactive_shell = _interactive_shell
LivePrintContext = _LivePrintContext
get_live_print_context = _get_live_print_context
live_print_scope = _live_print_scope
RemoveLivePart = _RemoveLivePart
add_renderable = _add_renderable
get_live_console = _get_live_console
Expand Down
76 changes: 70 additions & 6 deletions docs/console/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@
<!-- === OK_EDIT: pkg-ext header === -->

<!-- === DO_NOT_EDIT: pkg-ext symbols === -->
- [`LivePrintContext`](#liveprintcontext_def)
- [`RemoveLivePart`](#removelivepart_def)
- [`add_renderable`](#add_renderable_def)
- [`configure_logging`](#configure_logging_def)
- [`disable_interactive_shell`](#disable_interactive_shell_def)
- [`get_live_console`](#get_live_console_def)
- [`get_live_print_context`](#get_live_print_context_def)
- [`interactive_shell`](#interactive_shell_def)
- [`live_print_scope`](#live_print_scope_def)
- [`log_to_live`](#log_to_live_def)
- [`new_task`](#new_task_def)
- [`print_to_live`](#print_to_live_def)
Expand All @@ -23,7 +26,7 @@
<a id="removelivepart_def"></a>

### class: `RemoveLivePart`
- [source](../../ask_shell/_internal/rich_live.py#L157)
- [source](../../ask_shell/_internal/rich_live.py#L158)
> **Since:** 0.3.0

```python
Expand All @@ -41,7 +44,7 @@ class RemoveLivePart:
<a id="add_renderable_def"></a>

### function: `add_renderable`
- [source](../../ask_shell/_internal/rich_live.py#L161)
- [source](../../ask_shell/_internal/rich_live.py#L162)
- [Example: Mount a dynamic Rich renderable in ask-shell's live region with add_renderable, plus an apply-live demo with CI heartbeats](../examples/console/add_renderable.md)
> **Since:** 0.3.0

Expand Down Expand Up @@ -78,7 +81,7 @@ def configure_logging(app: Typer, *, settings: AskShellSettings | None = None, a
<a id="get_live_console_def"></a>

### function: `get_live_console`
- [source](../../ask_shell/_internal/rich_live.py#L179)
- [source](../../ask_shell/_internal/rich_live.py#L180)
> **Since:** 0.3.0

```python
Expand Down Expand Up @@ -114,7 +117,7 @@ def interactive_shell() -> bool:
<a id="log_to_live_def"></a>

### function: `log_to_live`
- [source](../../ask_shell/_internal/rich_live.py#L219)
- [source](../../ask_shell/_internal/rich_live.py#L234)
> **Since:** 0.3.0

```python
Expand Down Expand Up @@ -168,7 +171,7 @@ class new_task:
<a id="print_to_live_def"></a>

### function: `print_to_live`
- [source](../../ask_shell/_internal/rich_live.py#L183)
- [source](../../ask_shell/_internal/rich_live.py#L195)
> **Since:** 0.3.0

```python
Expand Down Expand Up @@ -201,4 +204,65 @@ Force non-interactive mode for the remainder of this process.
| Version | Change |
|---------|--------|
| 0.9.0 | Made public |
<!-- === OK_EDIT: pkg-ext disable_interactive_shell_def === -->
<!-- === OK_EDIT: pkg-ext disable_interactive_shell_def === -->
<!-- === DO_NOT_EDIT: pkg-ext liveprintcontext_def === -->
<a id="liveprintcontext_def"></a>

### class: `LivePrintContext`
- [source](../../ask_shell/_internal/live_print_context.py#L9)
> **Since:** unreleased

```python
class LivePrintContext:
prefix: str = ''
suppress: bool = False
```

| Field | Type | Default | Since |
|---|---|---|---|
| prefix | `str` | `''` | unreleased |
| suppress | `bool` | `False` | unreleased |

### Changes

| Version | Change |
|---------|--------|
| unreleased | Made public |
<!-- === OK_EDIT: pkg-ext liveprintcontext_def === -->
<!-- === DO_NOT_EDIT: pkg-ext get_live_print_context_def === -->
<a id="get_live_print_context_def"></a>

### function: `get_live_print_context`
- [source](../../ask_shell/_internal/live_print_context.py#L18)
> **Since:** unreleased

```python
def get_live_print_context() -> LivePrintContext | None:
...
```

### Changes

| Version | Change |
|---------|--------|
| unreleased | Made public |
<!-- === OK_EDIT: pkg-ext get_live_print_context_def === -->
<!-- === DO_NOT_EDIT: pkg-ext live_print_scope_def === -->
<a id="live_print_scope_def"></a>

### function: `live_print_scope`
- [source](../../ask_shell/_internal/live_print_context.py#L22)
- [Example: Prefix or suppress live-console scroll lines per scope, including across run_pool workers](../examples/console/live_print_scope.md)
> **Since:** unreleased

```python
def live_print_scope(*, prefix: str = '', suppress: bool = False) -> Iterator[None]:
...
```

### Changes

| Version | Change |
|---------|--------|
| unreleased | Made public |
<!-- === OK_EDIT: pkg-ext live_print_scope_def === -->
Loading
Loading