From 4770d8f127d25c7f42100bccc6d94ebc4c76b77d Mon Sep 17 00:00:00 2001 From: Doug Harris Date: Tue, 31 Dec 2024 11:28:21 -0500 Subject: [PATCH] Adding `column_date_formats` config option (#1402) Allows per-column formatting for date, time, datetime like columns. Bumping cli_helpers to 2.4.0 for the timestamp formatting preprocessor. --- AUTHORS | 1 + changelog.rst | 1 + pgcli/main.py | 15 ++++++++++++-- pgcli/pgclirc | 5 +++++ pyproject.toml | 2 +- tests/test_main.py | 51 ++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 72 insertions(+), 3 deletions(-) diff --git a/AUTHORS b/AUTHORS index 97d972fbd..1802f7d48 100644 --- a/AUTHORS +++ b/AUTHORS @@ -139,6 +139,7 @@ Contributors: * Chris Novakovic * Josh Lynch (josh-lynch) * Fabio (3ximus) + * Doug Harris (dougharris) Creator: -------- diff --git a/changelog.rst b/changelog.rst index 434afd8ea..c01ec3b6d 100644 --- a/changelog.rst +++ b/changelog.rst @@ -6,6 +6,7 @@ Features * Add a `--ping` command line option; allows pgcli to replace `pg_isready` * Changed the packaging metadata from setup.py to pyproject.toml * Add bash completion for services defined in the service file `~/.pg_service.conf` +* Added support for per-column date/time formatting using `column_date_formats` in config Bug fixes: ---------- diff --git a/pgcli/main.py b/pgcli/main.py index d4c6dbf6d..e398ef56f 100644 --- a/pgcli/main.py +++ b/pgcli/main.py @@ -19,7 +19,11 @@ from typing import Optional from cli_helpers.tabular_output import TabularOutputFormatter -from cli_helpers.tabular_output.preprocessors import align_decimals, format_numbers +from cli_helpers.tabular_output.preprocessors import ( + align_decimals, + format_numbers, + format_timestamps, +) from cli_helpers.utils import strip_ansi from .explain_output_formatter import ExplainOutputFormatter import click @@ -111,12 +115,13 @@ OutputSettings = namedtuple( "OutputSettings", - "table_format dcmlfmt floatfmt 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", ) OutputSettings.__new__.__defaults__ = ( None, None, None, + None, "", False, None, @@ -264,6 +269,7 @@ def __init__( self.on_error = c["main"]["on_error"].upper() self.decimal_format = c["data_formats"]["decimal"] self.float_format = c["data_formats"]["float"] + self.column_date_formats = c["column_date_formats"] auth.keyring_initialize(c["main"].as_bool("keyring"), logger=self.logger) self.show_bottom_toolbar = c["main"].as_bool("show_bottom_toolbar") @@ -1179,6 +1185,7 @@ def _evaluate_command(self, text): table_format=self.table_format, dcmlfmt=self.decimal_format, floatfmt=self.float_format, + column_date_formats=self.column_date_formats, missingval=self.null_string, expanded=expanded, max_width=max_width, @@ -1830,6 +1837,7 @@ def format_status(cur, status): "missing_value": settings.missingval, "integer_format": settings.dcmlfmt, "float_format": settings.floatfmt, + "column_date_formats": settings.column_date_formats, "preprocessors": (format_numbers, format_arrays), "disable_numparse": True, "preserve_whitespace": True, @@ -1839,6 +1847,9 @@ def format_status(cur, status): if not settings.floatfmt: output_kwargs["preprocessors"] = (align_decimals,) + if settings.column_date_formats: + output_kwargs["preprocessors"] += (format_timestamps,) + if table_format == "csv": # The default CSV dialect is "excel" which is not handling newline values correctly # Nevertheless, we want to keep on using "excel" on Windows since it uses '\r\n' diff --git a/pgcli/pgclirc b/pgcli/pgclirc index dd8b15f13..927045b96 100644 --- a/pgcli/pgclirc +++ b/pgcli/pgclirc @@ -240,3 +240,8 @@ output.null = "#808080" [data_formats] decimal = "" float = "" + +# Per column formats for date/timestamp columns +[column_date_formats] +# use strftime format, e.g. +# created = "%Y-%m-%d" diff --git a/pyproject.toml b/pyproject.toml index d714282a3..67e52bb75 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ dependencies = [ "psycopg-binary >= 3.0.14; sys_platform == 'win32'", "sqlparse >=0.3.0,<0.6", "configobj >= 5.0.6", - "cli_helpers[styles] >= 2.2.1", + "cli_helpers[styles] >= 2.4.0", # setproctitle is used to mask the password when running `ps` in command line. # But this is not necessary in Windows since the password is never shown in the # task manager. Also setproctitle is a hard dependency to install in Windows, diff --git a/tests/test_main.py b/tests/test_main.py index 3683d491f..102ebcd3e 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -76,6 +76,57 @@ def test_format_output(): assert list(results) == expected +def test_column_date_formats(): + settings = OutputSettings( + table_format="psql", + column_date_formats={ + "date_col": "%Y-%m-%d", + "datetime_col": "%I:%M:%S %m/%d/%y", + }, + ) + data = [ + ("name1", "2024-12-13T18:32:22", "2024-12-13T19:32:22", "2024-12-13T20:32:22"), + ("name2", "2025-02-13T02:32:22", "2025-02-13T02:32:22", "2025-02-13T02:32:22"), + ] + headers = ["name", "date_col", "datetime_col", "unchanged_col"] + + results = format_output("Title", data, headers, "test status", settings) + expected = [ + "Title", + "+-------+------------+-------------------+---------------------+", + "| name | date_col | datetime_col | unchanged_col |", + "|-------+------------+-------------------+---------------------|", + "| name1 | 2024-12-13 | 07:32:22 12/13/24 | 2024-12-13T20:32:22 |", + "| name2 | 2025-02-13 | 02:32:22 02/13/25 | 2025-02-13T02:32:22 |", + "+-------+------------+-------------------+---------------------+", + "test status", + ] + assert list(results) == expected + + +def test_no_column_date_formats(): + """Test that not setting any column date formats returns unaltered datetime columns""" + settings = OutputSettings(table_format="psql") + data = [ + ("name1", "2024-12-13T18:32:22", "2024-12-13T19:32:22", "2024-12-13T20:32:22"), + ("name2", "2025-02-13T02:32:22", "2025-02-13T02:32:22", "2025-02-13T02:32:22"), + ] + headers = ["name", "date_col", "datetime_col", "unchanged_col"] + + results = format_output("Title", data, headers, "test status", settings) + expected = [ + "Title", + "+-------+---------------------+---------------------+---------------------+", + "| name | date_col | datetime_col | unchanged_col |", + "|-------+---------------------+---------------------+---------------------|", + "| name1 | 2024-12-13T18:32:22 | 2024-12-13T19:32:22 | 2024-12-13T20:32:22 |", + "| name2 | 2025-02-13T02:32:22 | 2025-02-13T02:32:22 | 2025-02-13T02:32:22 |", + "+-------+---------------------+---------------------+---------------------+", + "test status", + ] + assert list(results) == expected + + def test_format_output_truncate_on(): settings = OutputSettings( table_format="psql", dcmlfmt="d", floatfmt="g", max_field_width=10