Skip to content

Commit 6c598ac

Browse files
Solving technical debt.
Signed-off-by: Carlos Andrés Planchón Prestes <carlosandresplanchonprestes@gmail.com>
1 parent a93b0cc commit 6c598ac

6 files changed

Lines changed: 201 additions & 131 deletions

File tree

‎README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ Every command that prints rows takes the same options:
7777
the JSON format NULL becomes `null`, BLOBs are base64 strings and values are
7878
never truncated.
7979

80+
When a table is wider than the terminal, the widest columns are narrowed and
81+
their values wrapped so that every column and label stays visible. Use
82+
`--width` or another format to get the values on one line.
83+
8084
## Queries
8185

8286
```sh

‎sqlitexplorer/cli.py‎

Lines changed: 71 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@
99
from __future__ import annotations
1010

1111
import difflib
12+
import functools
13+
import inspect
1214
import shutil
1315
import sys
1416
import time
15-
from collections.abc import Iterator, Sequence
17+
from collections.abc import Callable, Iterator, Sequence
1618
from contextlib import contextmanager
1719
from pathlib import Path
1820
from typing import Annotated, NoReturn
@@ -205,26 +207,46 @@ def _reporting_errors() -> Iterator[None]:
205207
_fail(str(error))
206208

207209

208-
def _options(
209-
output_format: OutputFormat,
210-
null: str,
211-
truncate: int | None,
212-
page: int | None,
213-
page_size: int | None,
214-
pager: bool,
215-
color: bool | None,
216-
width: int | None,
217-
) -> OutputOptions:
218-
return OutputOptions(
219-
format=output_format,
220-
null=null,
221-
truncate=truncate,
222-
page=page,
223-
page_size=page_size,
224-
pager=pager,
225-
color=color,
226-
width=width,
227-
)
210+
OUTPUT_PARAMETERS: dict[str, tuple[object, object]] = {
211+
"output_format": (FormatOption, OutputFormat.TABLE),
212+
"null": (NullOption, "NULL"),
213+
"truncate": (TruncateOption, None),
214+
"page": (PageOption, None),
215+
"page_size": (PageSizeOption, None),
216+
"pager": (PagerOption, False),
217+
"color": (ColorOption, None),
218+
"width": (WidthOption, None),
219+
}
220+
221+
222+
def with_output_options(command: Callable[..., None]) -> Callable[..., None]:
223+
"""Expose the shared output options on *command* and hand them over as ``options``.
224+
225+
Typer has no option groups, so instead of repeating the eight output
226+
parameters in every command this decorator appends them to the signature
227+
Typer inspects, and packs the values it receives into an
228+
:class:`OutputOptions` passed to the command as its ``options`` argument.
229+
"""
230+
signature = inspect.signature(command, eval_str=True)
231+
own = [parameter for parameter in signature.parameters.values() if parameter.name != "options"]
232+
shared = [
233+
inspect.Parameter(
234+
name, inspect.Parameter.KEYWORD_ONLY, default=default, annotation=annotation
235+
)
236+
for name, (annotation, default) in OUTPUT_PARAMETERS.items()
237+
]
238+
239+
@functools.wraps(command)
240+
def wrapper(**arguments: object) -> None:
241+
values = {name: arguments.pop(name) for name in OUTPUT_PARAMETERS}
242+
values["format"] = values.pop("output_format")
243+
command(options=OutputOptions(**values), **arguments)
244+
245+
wrapper.__signature__ = signature.replace(parameters=[*own, *shared]) # type: ignore[attr-defined]
246+
wrapper.__annotations__ = {
247+
parameter.name: parameter.annotation for parameter in [*own, *shared]
248+
}
249+
return wrapper
228250

229251

230252
def _pairs(values: Sequence[str] | None, *, option: str) -> list[tuple[str, str]]:
@@ -274,20 +296,14 @@ def _split_list(value: str | None) -> list[str] | None:
274296

275297

276298
@app.command()
299+
@with_output_options
277300
def tables(
278301
database: DatabaseArg,
279302
include_internal: AllOption = False,
280-
output_format: FormatOption = OutputFormat.TABLE,
281-
null: NullOption = "NULL",
282-
truncate: TruncateOption = None,
283-
page: PageOption = None,
284-
page_size: PageSizeOption = None,
285-
pager: PagerOption = False,
286-
color: ColorOption = None,
287-
width: WidthOption = None,
303+
*,
304+
options: OutputOptions,
288305
) -> None:
289306
"""List the tables and views of the database with their row counts."""
290-
options = _options(output_format, null, truncate, page, page_size, pager, color, width)
291307
with _reporting_errors(), open_database(database) as db:
292308
emit(db.tables(include_internal=include_internal), options, empty="(no tables)")
293309

@@ -315,84 +331,61 @@ def schema(
315331

316332

317333
@app.command()
334+
@with_output_options
318335
def describe(
319336
database: DatabaseArg,
320337
table: TableArg,
321-
output_format: FormatOption = OutputFormat.TABLE,
322-
null: NullOption = "NULL",
323-
truncate: TruncateOption = None,
324-
page: PageOption = None,
325-
page_size: PageSizeOption = None,
326-
pager: PagerOption = False,
327-
color: ColorOption = None,
328-
width: WidthOption = None,
338+
*,
339+
options: OutputOptions,
329340
) -> None:
330341
"""Show the columns of a table or view: type, NOT NULL, default and primary key."""
331-
options = _options(output_format, null, truncate, page, page_size, pager, color, width)
332342
with _reporting_errors(), open_database(database) as db:
333343
emit(db.columns(table), options)
334344

335345

336346
@app.command()
347+
@with_output_options
337348
def indexes(
338349
database: DatabaseArg,
339350
table: OptionalTableArg = None,
340-
output_format: FormatOption = OutputFormat.TABLE,
341-
null: NullOption = "NULL",
342-
truncate: TruncateOption = None,
343-
page: PageOption = None,
344-
page_size: PageSizeOption = None,
345-
pager: PagerOption = False,
346-
color: ColorOption = None,
347-
width: WidthOption = None,
351+
*,
352+
options: OutputOptions,
348353
) -> None:
349354
"""List the indexes of a table, or of every table, with the columns they cover."""
350-
options = _options(output_format, null, truncate, page, page_size, pager, color, width)
351355
with _reporting_errors(), open_database(database) as db:
352356
emit(db.indexes(table), options, empty="(no indexes)")
353357

354358

355359
@app.command()
360+
@with_output_options
356361
def foreign_keys(
357362
database: DatabaseArg,
358363
table: OptionalTableArg = None,
359-
output_format: FormatOption = OutputFormat.TABLE,
360-
null: NullOption = "NULL",
361-
truncate: TruncateOption = None,
362-
page: PageOption = None,
363-
page_size: PageSizeOption = None,
364-
pager: PagerOption = False,
365-
color: ColorOption = None,
366-
width: WidthOption = None,
364+
*,
365+
options: OutputOptions,
367366
) -> None:
368367
"""List the foreign keys declared by a table, or by every table."""
369-
options = _options(output_format, null, truncate, page, page_size, pager, color, width)
370368
with _reporting_errors(), open_database(database) as db:
371369
emit(db.foreign_keys(table), options, empty="(no foreign keys)")
372370

373371

374372
@app.command()
373+
@with_output_options
375374
def info(
376375
database: DatabaseArg,
377376
check: Annotated[
378377
bool, typer.Option("--check", help="Run PRAGMA integrity_check (slow on big files).")
379378
] = False,
380-
output_format: FormatOption = OutputFormat.TABLE,
381-
null: NullOption = "NULL",
382-
truncate: TruncateOption = None,
383-
page: PageOption = None,
384-
page_size: PageSizeOption = None,
385-
pager: PagerOption = False,
386-
color: ColorOption = None,
387-
width: WidthOption = None,
379+
*,
380+
options: OutputOptions,
388381
) -> None:
389382
"""Show facts about the database file: size, pragmas and object counts."""
390-
options = _options(output_format, null, truncate, page, page_size, pager, color, width)
391383
with _reporting_errors(), open_database(database) as db:
392384
emit(db.info(check=check), options)
393385

394386

395387
@app.command()
388+
@with_output_options
396389
def show(
397390
database: DatabaseArg,
398391
table: TableArg,
@@ -411,17 +404,10 @@ def show(
411404
typer.Option("--limit", "-n", min=0, help="Maximum number of rows to print."),
412405
] = None,
413406
offset: Annotated[int, typer.Option("--offset", min=0, help="Number of rows to skip.")] = 0,
414-
output_format: FormatOption = OutputFormat.TABLE,
415-
null: NullOption = "NULL",
416-
truncate: TruncateOption = None,
417-
page: PageOption = None,
418-
page_size: PageSizeOption = None,
419-
pager: PagerOption = False,
420-
color: ColorOption = None,
421-
width: WidthOption = None,
407+
*,
408+
options: OutputOptions,
422409
) -> None:
423410
"""Print the rows of a table or view."""
424-
options = _options(output_format, null, truncate, page, page_size, pager, color, width)
425411
with _reporting_errors(), open_database(database) as db:
426412
result = db.rows(
427413
table,
@@ -436,28 +422,23 @@ def show(
436422

437423

438424
@app.command()
425+
@with_output_options
439426
def stats(
440427
database: DatabaseArg,
441428
table: TableArg,
442429
top: Annotated[
443430
int, typer.Option("--top", min=0, help="How many frequent values to list per column.")
444431
] = 3,
445-
output_format: FormatOption = OutputFormat.TABLE,
446-
null: NullOption = "NULL",
447-
truncate: TruncateOption = None,
448-
page: PageOption = None,
449-
page_size: PageSizeOption = None,
450-
pager: PagerOption = False,
451-
color: ColorOption = None,
452-
width: WidthOption = None,
432+
*,
433+
options: OutputOptions,
453434
) -> None:
454435
"""Per-column statistics: nulls, distinct values, min, max and most frequent values."""
455-
options = _options(output_format, null, truncate, page, page_size, pager, color, width)
456436
with _reporting_errors(), open_database(database) as db:
457437
emit(db.stats(table, top=top), options)
458438

459439

460440
@app.command()
441+
@with_output_options
461442
def search(
462443
database: DatabaseArg,
463444
text: Annotated[
@@ -475,17 +456,10 @@ def search(
475456
limit: Annotated[
476457
int | None, typer.Option("--limit", "-n", min=1, help="Stop after N matches.")
477458
] = None,
478-
output_format: FormatOption = OutputFormat.TABLE,
479-
null: NullOption = "NULL",
480-
truncate: TruncateOption = None,
481-
page: PageOption = None,
482-
page_size: PageSizeOption = None,
483-
pager: PagerOption = False,
484-
color: ColorOption = None,
485-
width: WidthOption = None,
459+
*,
460+
options: OutputOptions,
486461
) -> None:
487462
"""Find a text in every column of every table."""
488-
options = _options(output_format, null, truncate, page, page_size, pager, color, width)
489463
with _reporting_errors(), open_database(database) as db:
490464
emit(db.search(text, tables=table, limit=limit), options, empty="(no matches)")
491465

@@ -494,6 +468,7 @@ def search(
494468

495469

496470
@app.command()
471+
@with_output_options
497472
def query(
498473
database: DatabaseArg,
499474
sql: Annotated[
@@ -524,21 +499,14 @@ def query(
524499
float | None,
525500
typer.Option("--watch", min=0.1, help="Re-run every N seconds until Ctrl-C."),
526501
] = None,
527-
output_format: FormatOption = OutputFormat.TABLE,
528-
null: NullOption = "NULL",
529-
truncate: TruncateOption = None,
530-
page: PageOption = None,
531-
page_size: PageSizeOption = None,
532-
pager: PagerOption = False,
533-
color: ColorOption = None,
534-
width: WidthOption = None,
502+
*,
503+
options: OutputOptions,
535504
) -> None:
536505
"""Run SQL statements and print their results."""
537506
statements = _read_sql(sql, file)
538-
if watch is not None and (pager or write):
507+
if watch is not None and (options.pager or write):
539508
_fail("--watch cannot be combined with --pager or --write")
540509
parameters = _parameters(params)
541-
options = _options(output_format, null, truncate, page, page_size, pager, color, width)
542510
with _reporting_errors(), open_database(database, write=write) as db:
543511
_attach_all(db, attach, write=write)
544512
try:

0 commit comments

Comments
 (0)