From 56ab82c77453c7a05abab4277d99a3d1681b34b3 Mon Sep 17 00:00:00 2001 From: Diego Date: Tue, 15 Sep 2026 15:52:44 -0300 Subject: [PATCH 1/3] Add --no-timings and --no-status -t/--tuples-only already turns off the timing line and the status footer, but it also drops the headers, the title and the table formatting. Scripts that post-process pgcli output often want the table intact and only one of the two footers gone, and there was no way to ask for that. The two flags are independent of each other and of -t, which keeps working as the psql-compatible shorthand for both. 8 tests, 2 of which fail if the flags are wired to -t instead of standing on their own. --- changelog.rst | 5 +++ pgcli/main.py | 34 +++++++++++++-- tests/test_no_timings_no_status.py | 67 ++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 tests/test_no_timings_no_status.py diff --git a/changelog.rst b/changelog.rst index ee5d1fcf5..a008bcf0f 100644 --- a/changelog.rst +++ b/changelog.rst @@ -8,6 +8,11 @@ Features: * You can specify multiple times. * Runs one statement at a time, like `-f`, and can be combined with `-f`: both run, the same way psql does. +* Add ``--no-timings`` and ``--no-status`` to suppress the timing line and the + status footer independently. ``-t``/``--tuples-only`` already turns off both, + along with the headers and the title, but there was no way to keep the table + formatting and drop only one of the two, which scripts that post-process the + output often want. * Add support for forcing destructive commands without confirmation. * Command line option `-y` or `--yes`. * Skips the destructive command confirmation prompt when enabled. diff --git a/pgcli/main.py b/pgcli/main.py index 3ff820ce2..23f3a21dc 100644 --- a/pgcli/main.py +++ b/pgcli/main.py @@ -120,7 +120,7 @@ OutputSettings = namedtuple( "OutputSettings", "table_format dcmlfmt floatfmt column_date_formats missingval expanded max_width case_function style_output " - "max_field_width tuples_only", + "max_field_width tuples_only show_status", ) OutputSettings.__new__.__defaults__ = ( None, @@ -134,6 +134,7 @@ None, DEFAULT_MAX_FIELD_WIDTH, False, + True, ) @@ -224,6 +225,8 @@ def __init__( single_connection=False, less_chatty=None, tuples_only=None, + no_timings=False, + no_status=False, prompt=None, prompt_dsn=None, auto_vertical_output=False, @@ -290,8 +293,14 @@ def __init__( # alone here and switched to an unadorned one at output time, so \T # still reports (and can change) the configured format. self.tuples_only = bool(tuples_only) - if self.tuples_only: + # --no-timings and --no-status turn off one thing each, for scripts that + # want the rows plus one of the two. -t is the psql-compatible shorthand + # that turns off both, along with the headers and the title. + self.show_status = not no_status + if no_timings or self.tuples_only: self.pgspecial.timing_enabled = False + if self.tuples_only: + self.show_status = False self.syntax_style = c["main"]["syntax_style"] self.cli_style = c["colors"] self.wider_completion_menu = c["main"].as_bool("wider_completion_menu") @@ -1370,6 +1379,7 @@ def _evaluate_command(self, text): style_output=self.style_output, max_field_width=self.max_field_width, tuples_only=self.tuples_only, + show_status=self.show_status, ) # Hide query text for named queries in quiet mode @@ -1650,6 +1660,20 @@ def echo_via_pager(self, text, color=None): default=False, help="Print rows only: no column headers, no status footer and no timing, like psql.", ) +@click.option( + "--no-timings", + "no_timings", + is_flag=True, + default=False, + help="Do not print the timing line after each query.", +) +@click.option( + "--no-status", + "no_status", + is_flag=True, + default=False, + help="Do not print the status footer (SELECT 3, UPDATE 1, ...) after each query.", +) @click.option("--prompt", help='Prompt format (Default: "\\u@\\h:\\d> ").') @click.option( "--prompt-dsn", @@ -1737,6 +1761,8 @@ def cli( application_name, less_chatty, tuples_only, + no_timings: bool, + no_status: bool, prompt, prompt_dsn, list_databases, @@ -1809,6 +1835,8 @@ def cli( single_connection=single_connection, less_chatty=less_chatty, tuples_only=tuples_only, + no_timings=no_timings, + no_status=no_status, prompt=prompt, prompt_dsn=prompt_dsn, auto_vertical_output=auto_vertical_output, @@ -2229,7 +2257,7 @@ def format_status(cur, status): output = itertools.chain(output, formatted) # Likewise the status footer. - if status and not settings.tuples_only: + if status and settings.show_status and not settings.tuples_only: output = itertools.chain(output, [format_status(cur, status)]) return output diff --git a/tests/test_no_timings_no_status.py b/tests/test_no_timings_no_status.py new file mode 100644 index 000000000..dc87aecda --- /dev/null +++ b/tests/test_no_timings_no_status.py @@ -0,0 +1,67 @@ +from unittest.mock import patch + +from click.testing import CliRunner + +from pgcli.main import cli, format_output, OutputSettings, PGCli + + +def test_no_timings_flag_passed_to_pgcli(): + runner = CliRunner() + with patch.object(PGCli, "__init__", autospec=True, return_value=None) as mock_pgcli: + runner.invoke(cli, ["--no-timings", "mydb"]) + assert mock_pgcli.call_args[1]["no_timings"] is True + + +def test_no_status_flag_passed_to_pgcli(): + runner = CliRunner() + with patch.object(PGCli, "__init__", autospec=True, return_value=None) as mock_pgcli: + runner.invoke(cli, ["--no-status", "mydb"]) + assert mock_pgcli.call_args[1]["no_status"] is True + + +def test_both_default_to_false(): + runner = CliRunner() + with patch.object(PGCli, "__init__", autospec=True, return_value=None) as mock_pgcli: + runner.invoke(cli, ["mydb"]) + assert mock_pgcli.call_args[1]["no_timings"] is False + assert mock_pgcli.call_args[1]["no_status"] is False + + +def test_no_timings_turns_off_timing_only(): + """The point of having two flags: each one leaves the other alone.""" + cli_obj = PGCli(no_timings=True) + + assert cli_obj.pgspecial.timing_enabled is False + assert cli_obj.show_status is True + + +def test_no_status_turns_off_status_only(): + cli_obj = PGCli(no_status=True) + + assert cli_obj.show_status is False + assert cli_obj.pgspecial.timing_enabled is True + + +def test_tuples_only_still_turns_off_both(): + """-t stays the psql-compatible shorthand for both.""" + cli_obj = PGCli(tuples_only=True) + + assert cli_obj.show_status is False + assert cli_obj.pgspecial.timing_enabled is False + + +def test_no_status_suppresses_only_the_footer(): + settings = OutputSettings(table_format="psql", show_status=False) + output = "\n".join(format_output("Title", [(1,)], ["a"], "SELECT 1", settings)) + + assert "SELECT 1" not in output + # Rows, headers and title are untouched. + assert "Title" in output + assert "a" in output and "1" in output + + +def test_status_footer_is_printed_by_default(): + settings = OutputSettings(table_format="psql") + output = "\n".join(format_output("Title", [(1,)], ["a"], "SELECT 1", settings)) + + assert "SELECT 1" in output From 1f03e94e2ef7ca42f18340412a5563c9e38924b6 Mon Sep 17 00:00:00 2001 From: Diego Date: Thu, 15 Oct 2026 15:09:25 -0300 Subject: [PATCH 2/3] Keep SQL errors visible under --no-status The error of a failed statement travels in the same `status` field as the "SELECT 3" footer, so --no-status dropped it and a broken query printed nothing at all. The status of a failed statement is now always shown; --no-status still drops the footer of a good one. Two tests against a live server: the error survives --no-status (it fails without the change), and a good query's footer is still dropped. --- pgcli/main.py | 4 +++- tests/test_no_timings_no_status.py | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/pgcli/main.py b/pgcli/main.py index 23f3a21dc..853918bad 100644 --- a/pgcli/main.py +++ b/pgcli/main.py @@ -1379,7 +1379,9 @@ def _evaluate_command(self, text): style_output=self.style_output, max_field_width=self.max_field_width, tuples_only=self.tuples_only, - show_status=self.show_status, + # For a failed statement `status` is the error message, not + # the "SELECT 3" footer, so --no-status must not drop it. + show_status=self.show_status or not success, ) # Hide query text for named queries in quiet mode diff --git a/tests/test_no_timings_no_status.py b/tests/test_no_timings_no_status.py index dc87aecda..0100ae8d0 100644 --- a/tests/test_no_timings_no_status.py +++ b/tests/test_no_timings_no_status.py @@ -3,6 +3,7 @@ from click.testing import CliRunner from pgcli.main import cli, format_output, OutputSettings, PGCli +from utils import dbtest def test_no_timings_flag_passed_to_pgcli(): @@ -65,3 +66,24 @@ def test_status_footer_is_printed_by_default(): output = "\n".join(format_output("Title", [(1,)], ["a"], "SELECT 1", settings)) assert "SELECT 1" in output + + +@dbtest +def test_no_status_keeps_errors_visible(executor): + # The error of a failed statement travels in `status`, the same field as + # the footer --no-status drops, so a broken query used to print nothing. + cli_obj = PGCli(pgexecute=executor, no_status=True) + output, query = cli_obj._evaluate_command("select nope") + + assert not query.successful + assert any('column "nope" does not exist' in line for line in output) + + +@dbtest +def test_no_status_still_drops_the_footer_of_a_good_query(executor): + cli_obj = PGCli(pgexecute=executor, no_status=True) + output, query = cli_obj._evaluate_command("select 42") + + assert query.successful + assert any("42" in line for line in output) + assert not any("SELECT 1" in line for line in output) From 5b6b71718a88d0fef68a9f417952d094485fbea1 Mon Sep 17 00:00:00 2001 From: Diego Date: Thu, 15 Oct 2026 15:09:46 -0300 Subject: [PATCH 3/3] Move the --no-timings/--no-status changelog entry to Upcoming The merge of main carried it into the released 4.7.0 section. Also note that a failed statement still prints its error under --no-status. --- changelog.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/changelog.rst b/changelog.rst index 26fda9475..cd22926f1 100644 --- a/changelog.rst +++ b/changelog.rst @@ -5,6 +5,12 @@ Features: --------- * Add a ``\service`` prompt token for connections made using ``service=...`` or ``PGSERVICE``. For example, set ``prompt = '\service> '``. +* Add ``--no-timings`` and ``--no-status`` to suppress the timing line and the + status footer independently. ``-t``/``--tuples-only`` already turns off both, + along with the headers and the title, but there was no way to keep the table + formatting and drop only one of the two, which scripts that post-process the + output often want. A failed statement still prints its error under + ``--no-status``. 4.7.1 (2026-09-20) ================== @@ -28,11 +34,6 @@ Features: * You can specify multiple times. * Runs one statement at a time, like `-f`, and can be combined with `-f`: both run, the same way psql does. -* Add ``--no-timings`` and ``--no-status`` to suppress the timing line and the - status footer independently. ``-t``/``--tuples-only`` already turns off both, - along with the headers and the title, but there was no way to keep the table - formatting and drop only one of the two, which scripts that post-process the - output often want. * Add support for forcing destructive commands without confirmation. * Command line option `-y` or `--yes`. * Skips the destructive command confirmation prompt when enabled.