From f170bbd0a73f9b279cec4f1b4cd059983641e8c1 Mon Sep 17 00:00:00 2001 From: sylvester Date: Wed, 18 Mar 2026 01:55:40 +0000 Subject: [PATCH] feat(cli): add --quiet/-q flag to suppress non-essential output Add a --quiet/-q global flag that suppresses update notices from check_for_updates() and other non-essential stderr output. The flag is hoisted like other global options and stored on CliContext for downstream use. Co-Authored-By: Claude Opus 4.6 (1M context) --- cli/main.py | 19 ++++++++-- cli/types/context.py | 1 + tests/test_main.py | 88 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 104 insertions(+), 4 deletions(-) diff --git a/cli/main.py b/cli/main.py index b5dc4e2..f96704e 100644 --- a/cli/main.py +++ b/cli/main.py @@ -31,7 +31,7 @@ ], } -_GLOBAL_FLAGS = {"-v", "--verbose"} +_GLOBAL_FLAGS = {"-v", "--verbose", "-q", "--quiet"} _GLOBAL_OPTIONS = {"--profile", "--format"} @@ -83,14 +83,26 @@ def _hoist_global_options(args: list[str]) -> list[str]: default=False, help="Show progress details (run IDs, spinners, status messages).", ) +@click.option( + "--quiet", + "-q", + is_flag=True, + default=False, + help="Suppress non-essential output (update notices, progress info).", +) @click.pass_context def cli( - ctx: click.Context, profile: str | None, output_format: str, verbose: bool + ctx: click.Context, + profile: str | None, + output_format: str, + verbose: bool, + quiet: bool, ) -> None: ctx.obj = CliContext( profile_override=profile, output_format=OutputFormat(output_format), verbose=verbose, + quiet=quiet, ) @@ -100,7 +112,8 @@ def cli( def main() -> None: """entry point; hoists global options then invokes click.""" sys.argv[1:] = _hoist_global_options(sys.argv[1:]) - check_for_updates() + if "-q" not in sys.argv and "--quiet" not in sys.argv: + check_for_updates() cli() diff --git a/cli/types/context.py b/cli/types/context.py index d9f3a60..c12729e 100644 --- a/cli/types/context.py +++ b/cli/types/context.py @@ -12,3 +12,4 @@ class CliContext: profile_override: str | None = None output_format: OutputFormat = field(default=OutputFormat.JSON) verbose: bool = False + quiet: bool = False diff --git a/tests/test_main.py b/tests/test_main.py index 6a396fa..667605d 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,6 +1,11 @@ from __future__ import annotations -from cli.main import _hoist_global_options +from unittest.mock import patch + +from click.testing import CliRunner + +from cli.main import _hoist_global_options, cli +from cli.types.context import CliContext class TestHoistGlobalOptions: @@ -46,3 +51,84 @@ def test_verbose_long_form(self): args = ["auth", "list", "--verbose"] result = _hoist_global_options(args) assert result[0] == "--verbose" + + def test_hoist_quiet_short(self): + result = _hoist_global_options(["realtime", "prices", "-q"]) + assert result == ["-q", "realtime", "prices"] + + def test_hoist_quiet_long(self): + result = _hoist_global_options(["realtime", "--quiet", "prices"]) + assert result == ["--quiet", "realtime", "prices"] + + def test_hoist_quiet_and_verbose(self): + result = _hoist_global_options(["realtime", "-q", "-v"]) + assert result == ["-q", "-v", "realtime"] + + def test_hoist_quiet_with_profile(self): + result = _hoist_global_options( + ["explorer", "run", "--quiet", "--profile", "dev"] + ) + assert result == ["--quiet", "--profile", "dev", "explorer", "run"] + + +class TestQuietFlag: + """--quiet / -q sets quiet=True on CliContext.""" + + @staticmethod + def _invoke_cli(args: list[str]) -> CliContext: + """Invoke cli group and return the CliContext stored in ctx.obj.""" + import click as _click + + captured: dict = {} + + @cli.command("_test_noop") + @_click.pass_context + def _noop(ctx: _click.Context) -> None: + captured["obj"] = ctx.obj + + try: + runner = CliRunner() + result = runner.invoke(cli, args + ["_test_noop"]) + assert result.exit_code == 0, result.output + return captured["obj"] + finally: + cli.commands.pop("_test_noop", None) # type: ignore[union-attr] + + def test_quiet_long_flag(self): + ctx = self._invoke_cli(["--quiet"]) + assert isinstance(ctx, CliContext) + assert ctx.quiet is True + + def test_quiet_short_flag(self): + ctx = self._invoke_cli(["-q"]) + assert isinstance(ctx, CliContext) + assert ctx.quiet is True + + def test_default_quiet_is_false(self): + ctx = self._invoke_cli([]) + assert isinstance(ctx, CliContext) + assert ctx.quiet is False + + +class TestMainQuietSuppressesUpdateCheck: + """main() skips check_for_updates when -q/--quiet is in sys.argv.""" + + @patch("cli.main.check_for_updates") + @patch("cli.main.cli") + def test_quiet_suppresses_update_check(self, mock_cli, mock_check): + with patch("cli.main.sys") as mock_sys: + mock_sys.argv = ["allium", "-q"] + from cli.main import main + + main() + mock_check.assert_not_called() + + @patch("cli.main.check_for_updates") + @patch("cli.main.cli") + def test_no_quiet_runs_update_check(self, mock_cli, mock_check): + with patch("cli.main.sys") as mock_sys: + mock_sys.argv = ["allium", "realtime"] + from cli.main import main + + main() + mock_check.assert_called_once()