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: 7 additions & 1 deletion src/together/lib/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from together.lib.cli.utils._exit import CliDiagnosticExit
from together.lib.cli.utils.config import CLIConfig
from together.lib.cli.utils._prompt import PromptParameter
from together.lib.cli.utils._console import console
from together.lib.cli.utils._console import CliBrokenPipeError, console
from together.lib.cli.utils._api_error import try_handle_server_error_message
from together.lib.cli.utils._completion import _is_agent_or_ci, install_completion
from together.lib.cli.utils._help_examples import (
Expand Down Expand Up @@ -157,7 +157,7 @@
except Exception as e:
if "api_key" in str(e):
client = AsyncTogether(
api_key="0000000000000000000000000000000000000000",

Check failure on line 160 in src/together/lib/cli/__init__.py

View check run for this annotation

Broly - Code Security Scanner / Broly Security Scan

[CRITICAL] Hardcoded secret

Hardcoded secret: Hardcoded secret (CWE-798)
base_url=base_url,
timeout=timeout,
max_retries=max_retries if max_retries is not None else 0,
Expand Down Expand Up @@ -357,6 +357,12 @@
{"command": parsed_command, "arguments": explicit_args, "is_beta_command": is_beta_command},
)
command_succeeded = True
except CliBrokenPipeError:
track_cli(
CliTrackingEvents.CommandUserAborted,
{"command": parsed_command, "arguments": explicit_args, "is_beta_command": is_beta_command},
)
sys.exit(1)
except KeyboardInterrupt:
track_cli(
CliTrackingEvents.CommandUserAborted,
Expand Down
16 changes: 15 additions & 1 deletion src/together/lib/cli/utils/_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import os
import sys
from typing import Any, TextIO, Literal, cast
from typing_extensions import override

from rich.theme import Theme
from rich.console import Console
Expand All @@ -11,6 +12,19 @@
StreamName = Literal["stdout", "stderr"]


class CliBrokenPipeError(BrokenPipeError):
"""Raised when Rich stops rendering because an output consumer closed its pipe."""


class _CliConsole(Console):
@override
def on_broken_pipe(self) -> None:
try:
super().on_broken_pipe()
except SystemExit:
raise CliBrokenPipeError from None


class _EncodingSafeStream:
"""Proxy writes through the current stdio stream without encoding crashes."""

Expand Down Expand Up @@ -141,7 +155,7 @@ def build_theme(theme_name: CliThemeName | None = None) -> Theme:
def create_console(theme_name: CliThemeName | None = None, *, stderr: bool = False) -> Console:
stream_name: StreamName = "stderr" if stderr else "stdout"
stream = cast(TextIO, _EncodingSafeStream(stream_name))
return Console(theme=build_theme(theme_name), highlight=False, file=stream, stderr=stderr)
return _CliConsole(theme=build_theme(theme_name), highlight=False, file=stream, stderr=stderr)


cli_theme_name: CliThemeName = resolve_cli_theme()
Expand Down
19 changes: 19 additions & 0 deletions tests/cli/test_command_telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from tests.cli.utils import CliRunner
from together.lib.cli import app as tg_app
from together.lib.cli._track_cli import CliTrackingEvents
from together.lib.cli.utils._console import CliBrokenPipeError


def _reset_telemetry_command_specs() -> None:
Expand Down Expand Up @@ -211,3 +212,21 @@ def _interrupt() -> None:
CliTrackingEvents.CommandStarted.value,
CliTrackingEvents.CommandUserAborted.value,
]


@pytest.mark.usefixtures("isolated_cli_config")
def test_command_broken_pipe_emits_started_then_user_aborted(
track_cli_capture: list[tuple[CliTrackingEvents, dict[str, Any]]],
cli_runner: CliRunner,
monkeypatch: pytest.MonkeyPatch,
) -> None:
def _broken_pipe() -> None:
raise CliBrokenPipeError

monkeypatch.setattr("together.lib.cli.api.telemetry.status.status", _broken_pipe)
r = cli_runner.invoke(["telemetry", "status"])
assert r.exit_code == 1
assert _event_kinds(track_cli_capture) == [
CliTrackingEvents.CommandStarted.value,
CliTrackingEvents.CommandUserAborted.value,
]
11 changes: 11 additions & 0 deletions tests/cli/test_console_theme.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from together.lib.cli.utils._console import (
_DARK_STYLES,
_LIGHT_STYLES,
CliBrokenPipeError,
build_theme,
create_console,
resolve_cli_theme,
Expand Down Expand Up @@ -127,3 +128,13 @@ def test_console_replaces_characters_unsupported_by_stream_encoding(monkeypatch:
cp1252_output.flush()

assert raw_output.getvalue().decode("cp1252") == "? ? café\n"


def test_console_exposes_rich_broken_pipe_as_distinct_exception(monkeypatch: pytest.MonkeyPatch) -> None:
def raise_system_exit(_console: Console) -> None:
raise SystemExit(1)

monkeypatch.setattr(Console, "on_broken_pipe", raise_system_exit)

with pytest.raises(CliBrokenPipeError):
create_console().on_broken_pipe()
Loading