Skip to content
4 changes: 4 additions & 0 deletions changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ Upcoming

Features:
---------
* Add a ``-t``/``--tuples-only`` command line option that prints the rows and
nothing else, matching psql: no column headers, no title, no status footer
and no timing line. The configured table format is left untouched, so ``\T``
still reports it and can still change it mid-session.
* Add support for executing SQL commands from file and exit.
* Command line option `-f` or `--file`.
* Multiple files can be specified.
Expand Down
42 changes: 36 additions & 6 deletions pgcli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@

OutputSettings = namedtuple(
"OutputSettings",
"table_format dcmlfmt floatfmt column_date_formats missingval expanded max_width case_function style_output max_field_width",
"table_format dcmlfmt floatfmt column_date_formats missingval expanded max_width case_function style_output "
"max_field_width tuples_only",
)
OutputSettings.__new__.__defaults__ = (
None,
Expand All @@ -132,6 +133,7 @@
lambda x: x,
None,
DEFAULT_MAX_FIELD_WIDTH,
False,
)


Expand Down Expand Up @@ -221,6 +223,7 @@ def __init__(
application_name="pgcli",
single_connection=False,
less_chatty=None,
tuples_only=None,
prompt=None,
prompt_dsn=None,
auto_vertical_output=False,
Expand Down Expand Up @@ -280,6 +283,13 @@ def __init__(
self.min_num_menu_lines = c["main"].as_int("min_num_menu_lines")
self.multiline_continuation_char = c["main"]["multiline_continuation_char"]
self.table_format = c["main"]["table_format"]
# psql's -t prints the rows and nothing else: no column headers, no
# title, no status footer and no timing line. The table format is left
# 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:
self.pgspecial.timing_enabled = False
self.syntax_style = c["main"]["syntax_style"]
self.cli_style = c["colors"]
self.wider_completion_menu = c["main"].as_bool("wider_completion_menu")
Expand Down Expand Up @@ -1334,6 +1344,7 @@ def _evaluate_command(self, text):
case_function=(self.completer.case if self.settings["case_column_headers"] else lambda x: x),
style_output=self.style_output,
max_field_width=self.max_field_width,
tuples_only=self.tuples_only,
)

# Hide query text for named queries in quiet mode
Expand Down Expand Up @@ -1601,6 +1612,14 @@ def echo_via_pager(self, text, color=None):
default=False,
help="Skip intro on startup and goodbye on exit.",
)
@click.option(
"-t",
"--tuples-only",
"tuples_only",
is_flag=True,
default=False,
help="Print rows only: no column headers, no status footer and no timing, like psql.",
)
@click.option("--prompt", help='Prompt format (Default: "\\u@\\h:\\d> ").')
@click.option(
"--prompt-dsn",
Expand Down Expand Up @@ -1672,6 +1691,7 @@ def cli(
row_limit,
application_name,
less_chatty,
tuples_only,
prompt,
prompt_dsn,
list_databases,
Expand Down Expand Up @@ -1741,6 +1761,7 @@ def cli(
application_name=application_name,
single_connection=single_connection,
less_chatty=less_chatty,
tuples_only=tuples_only,
prompt=prompt,
prompt_dsn=prompt_dsn,
auto_vertical_output=auto_vertical_output,
Expand Down Expand Up @@ -2052,7 +2073,15 @@ def exception_formatter(e, verbose_errors: bool = False):
def format_output(title, cur, headers, status, settings, explain_mode=False):
output = []
expanded = settings.expanded or settings.table_format == "vertical"
table_format = "vertical" if settings.expanded else settings.table_format
if settings.tuples_only:
# Rows and nothing else, so an unadorned format. This wins over
# expanded output: with the headers suppressed there is no label
# column left for the vertical formatter to lay out.
table_format = "plain"
elif settings.expanded:
table_format = "vertical"
else:
table_format = settings.table_format
max_width = settings.max_width
case_function = settings.case_function
if explain_mode:
Expand Down Expand Up @@ -2110,11 +2139,12 @@ def format_status(cur, status):
dialect = "excel" if platform.system() == "Windows" else "unix"
output_kwargs["dialect"] = dialect

if title: # Only print the title if it's not None.
# The title is printed unless there is none, or -t asked for rows only.
if title and not settings.tuples_only:
output.append(title)

if cur:
headers = [case_function(x) for x in headers]
headers = [] if settings.tuples_only else [case_function(x) for x in headers]
if max_width is not None:
cur = list(cur)
column_types = None
Expand Down Expand Up @@ -2148,8 +2178,8 @@ def format_status(cur, status):

output = itertools.chain(output, formatted)

# Only print the status if it's not None
if status:
# Likewise the status footer.
if status and not settings.tuples_only:
output = itertools.chain(output, [format_status(cur, status)])

return output
Expand Down
69 changes: 69 additions & 0 deletions tests/test_tuples_only.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from unittest.mock import patch

from click.testing import CliRunner

from pgcli.main import cli, format_output, OutputSettings, PGCli


def test_tuples_only_flag_passed_to_pgcli():
"""Test that -t passes tuples_only=True to PGCli."""
runner = CliRunner()
with patch.object(PGCli, "__init__", autospec=True, return_value=None) as mock_pgcli:
runner.invoke(cli, ["-t", "mydb"])
call_kwargs = mock_pgcli.call_args[1]
assert call_kwargs["tuples_only"] is True


def test_tuples_only_long_form():
"""Test that --tuples-only passes tuples_only=True to PGCli."""
runner = CliRunner()
with patch.object(PGCli, "__init__", autospec=True, return_value=None) as mock_pgcli:
runner.invoke(cli, ["--tuples-only", "mydb"])
call_kwargs = mock_pgcli.call_args[1]
assert call_kwargs["tuples_only"] is True


def test_tuples_only_not_set_by_default():
"""Test that tuples_only is False when -t is not used."""
runner = CliRunner()
with patch.object(PGCli, "__init__", autospec=True, return_value=None) as mock_pgcli:
runner.invoke(cli, ["mydb"])
call_kwargs = mock_pgcli.call_args[1]
assert call_kwargs["tuples_only"] is False


def test_tuples_only_leaves_the_configured_table_format_alone():
"""-t must not hijack the table format: \\T still reports what is configured."""
assert PGCli(tuples_only=True).table_format == PGCli().table_format


def test_tuples_only_turns_off_timing():
"""psql's -t prints no timing line."""
assert PGCli(tuples_only=True).pgspecial.timing_enabled is False


def test_tuples_only_prints_rows_only():
"""No title, no column headers, no status footer, no table borders."""
settings = OutputSettings(table_format="psql", tuples_only=True)
output = list(format_output("Title", [(1, "one"), (2, "two")], ["a", "b"], "SELECT 2", settings))

assert output == ["1 one", "2 two"]


def test_without_tuples_only_everything_is_printed():
"""The counterpart of the test above: by default nothing is suppressed."""
settings = OutputSettings(table_format="psql", tuples_only=False)
output = "\n".join(format_output("Title", [(1, "one")], ["a", "b"], "SELECT 1", settings))

assert "Title" in output
assert "a" in output and "b" in output
assert "SELECT 1" in output


def test_tuples_only_wins_over_expanded_output():
"""With the headers gone the vertical formatter has no label column left,
so -t falls back to the unadorned format rather than failing."""
settings = OutputSettings(table_format="psql", expanded=True, tuples_only=True)
output = list(format_output("Title", [(1, "one")], ["a", "b"], "SELECT 1", settings))

assert output == ["1 one"]
Loading