From 6c3f5e647934413dfc46a218f1540a1cab31e52e Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Sat, 23 Feb 2019 22:45:17 -0800 Subject: [PATCH] Added --table and --fmt options for table output using tabulate --- docs/cli.rst | 31 +++++++++++++++++++++++++++--- setup.py | 2 +- sqlite_utils/cli.py | 34 +++++++++++++++++++++++--------- tests/test_cli.py | 47 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 101 insertions(+), 13 deletions(-) diff --git a/docs/cli.rst b/docs/cli.rst index 013a54a..7228fc3 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -60,7 +60,7 @@ If you want to pretty-print the output further, you can pipe it through ``python Running queries and returning CSV ================================= -You can use the ``--csv`` option to return results as CSV:: +You can use the ``--csv`` option (or ``-c`` shortcut) to return results as CSV:: $ sqlite-utils dogs.db "select * from dogs" --csv id,age,name @@ -73,6 +73,31 @@ This will default to including the column names as a header row. To exclude the 1,4,Cleo 2,2,Pancakes +.. _cli_query_table: + +Running queries and outputting a table +====================================== + +You can use the ``--table`` option (or ``-t`` shortcut) to output query results as a table:: + + $ sqlite-utils dogs.db "select * from dogs" --table + id age name + ---- ----- -------- + 1 4 Cleo + 2 2 Pancakes + +You can use the ``--fmt`` (or ``-f``) option to specify different table formats, for example ``rst`` for reStructuredText:: + + $ sqlite-utils dogs.db "select * from dogs" --table --fmt rst + ==== ===== ======== + id age name + ==== ===== ======== + 1 4 Cleo + 2 2 Pancakes + ==== ===== ======== + +For a full list of table format options, run ``sqlite-utils query --help``. + .. _cli_rows: Returning all rows in a table @@ -84,7 +109,7 @@ You can return every row in a specified table using the ``rows`` subcommand:: [{"id": 1, "age": 4, "name": "Cleo"}, {"id": 2, "age": 2, "name": "Pancakes"}] -This command accepts the same output options as ``query`` - so you can pass ``--nl``, ``--csv`` and ``--no-headers``. +This command accepts the same output options as ``query`` - so you can pass ``--nl``, ``--csv``, ``--no-headers``, ``--table`` and ``--fmt``. .. _cli_tables: @@ -124,7 +149,7 @@ Use ``--columns`` to include a list of columns in each table:: {"table": "Gosh2", "count": 0, "columns": ["c1", "c2", "c3"]}, {"table": "dogs", "count": 2, "columns": ["id", "age", "name"]}] -The ``--nl``, ``--csv`` and ``--no-headers`` options are all available. +The ``--nl``, ``--csv`` and ``--table`` options are all available. .. _cli_inserting_data: diff --git a/setup.py b/setup.py index 75b2614..ed86ab7 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ setup( version=VERSION, license="Apache License, Version 2.0", packages=find_packages(), - install_requires=["click", "click-default-group"], + install_requires=["click", "click-default-group", "tabulate"], setup_requires=["pytest-runner"], extras_require={"test": ["pytest", "black"]}, entry_points=""" diff --git a/sqlite_utils/cli.py b/sqlite_utils/cli.py index 2d5d5df..3393b53 100644 --- a/sqlite_utils/cli.py +++ b/sqlite_utils/cli.py @@ -5,6 +5,7 @@ import itertools import json import sys import csv as csv_std +import tabulate import sqlite3 @@ -23,8 +24,17 @@ def output_options(fn): is_flag=True, default=False, ), - click.option("--csv", is_flag=True, help="Output CSV"), + click.option("-c", "--csv", is_flag=True, help="Output CSV"), click.option("--no-headers", is_flag=True, help="Omit CSV headers"), + click.option("-t", "--table", is_flag=True, help="Output as a table"), + click.option( + "-f", + "--fmt", + help="Table format - one of {}".format( + ", ".join(tabulate.tabulate_formats) + ), + default="simple", + ), ) ): fn = decorator(fn) @@ -60,7 +70,7 @@ def cli(): is_flag=True, default=False, ) -def tables(path, fts4, fts5, counts, nl, arrays, csv, no_headers, columns): +def tables(path, fts4, fts5, counts, nl, arrays, csv, no_headers, table, fmt, columns): """List the tables in the database""" db = sqlite_utils.Database(path) headers = ["table"] @@ -82,7 +92,9 @@ def tables(path, fts4, fts5, counts, nl, arrays, csv, no_headers, columns): row.append(cols) yield row - if csv: + if table: + print(tabulate.tabulate(_iter(), headers=headers, tablefmt=fmt)) + elif csv: writer = csv_std.writer(sys.stdout) if not no_headers: writer.writerow(headers) @@ -175,7 +187,7 @@ def insert_upsert_options(fn): click.argument("json_file", type=click.File(), required=True), click.option("--pk", help="Column to use as the primary key, e.g. id"), click.option("--nl", is_flag=True, help="Expect newline-delimited JSON"), - click.option("--csv", is_flag=True, help="Expect CSV"), + click.option("-c", "--csv", is_flag=True, help="Expect CSV"), click.option( "--batch-size", type=int, default=100, help="Commit every X records" ), @@ -244,12 +256,14 @@ def upsert(path, table, json_file, pk, nl, csv, batch_size): ) @click.argument("sql") @output_options -def query(path, sql, nl, arrays, csv, no_headers): +def query(path, sql, nl, arrays, csv, no_headers, table, fmt): "Execute SQL query and return the results as JSON" db = sqlite_utils.Database(path) cursor = iter(db.conn.execute(sql)) headers = [c[0] for c in cursor.description] - if csv: + if table: + print(tabulate.tabulate(list(cursor), headers=headers, tablefmt=fmt)) + elif csv: writer = csv_std.writer(sys.stdout) if not no_headers: writer.writerow([c[0] for c in cursor.description]) @@ -266,19 +280,21 @@ def query(path, sql, nl, arrays, csv, no_headers): type=click.Path(file_okay=True, dir_okay=False, allow_dash=False), required=True, ) -@click.argument("table") +@click.argument("dbtable") @output_options @click.pass_context -def rows(ctx, path, table, nl, arrays, csv, no_headers): +def rows(ctx, path, dbtable, nl, arrays, csv, no_headers, table, fmt): "Output all rows in the specified table" ctx.invoke( query, path=path, - sql="select * from [{}]".format(table), + sql="select * from [{}]".format(dbtable), nl=nl, arrays=arrays, csv=csv, no_headers=no_headers, + table=table, + fmt=fmt, ) diff --git a/tests/test_cli.py b/tests/test_cli.py index 23c6891..d72c96d 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -69,6 +69,53 @@ def test_tables_counts_and_columns_csv(db_path): ) == result.output.strip() +@pytest.mark.parametrize( + "fmt,expected", + [ + ( + "simple", + ( + "c1 c2 c3\n" + "----- ----- ----------\n" + "verb0 noun0 adjective0\n" + "verb1 noun1 adjective1\n" + "verb2 noun2 adjective2\n" + "verb3 noun3 adjective3" + ), + ), + ( + "rst", + ( + "===== ===== ==========\n" + "c1 c2 c3\n" + "===== ===== ==========\n" + "verb0 noun0 adjective0\n" + "verb1 noun1 adjective1\n" + "verb2 noun2 adjective2\n" + "verb3 noun3 adjective3\n" + "===== ===== ==========" + ), + ), + ], +) +def test_output_table(db_path, fmt, expected): + db = Database(db_path) + with db.conn: + db["rows"].insert_all( + [ + { + "c1": "verb{}".format(i), + "c2": "noun{}".format(i), + "c3": "adjective{}".format(i), + } + for i in range(4) + ] + ) + result = CliRunner().invoke(cli.cli, ["rows", db_path, "rows", "-t", "-f", fmt]) + assert 0 == result.exit_code + assert expected == result.output.strip() + + def test_enable_fts(db_path): assert None == Database(db_path)["Gosh"].detect_fts() result = CliRunner().invoke(