From c5a7461d7b55f873d31f3fb86397d1ef4c6e73fc Mon Sep 17 00:00:00 2001 From: Roland Walker Date: Mon, 26 Jan 2026 06:00:03 -0500 Subject: [PATCH] add a convert_to_undecoded_string preprocessor This preprocessor is not used by default, and will have to be added by the application at runtime as an argument to format_output(). It is designed to be run before other preprocessors, which one reason why it preserves Nones. Unlike the default convert_to_string(), it does not attempt to decode some bytes as UTF-8. It therefore treats all byte values the same way. --- CHANGELOG | 6 ++++++ cli_helpers/tabular_output/preprocessors.py | 20 +++++++++++++++++ cli_helpers/utils.py | 24 +++++++++++++++++++++ tests/tabular_output/test_preprocessors.py | 11 ++++++++++ 4 files changed, 61 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index b2e27e8..5764c6b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,11 @@ # Changelog +## Version 2.9.0 + +(released on 2026-01-26) + +- Add optional `convert_to_undecoded_string` preprocessor. + ## Version 2.8.2 (released on 2026-01-26) diff --git a/cli_helpers/tabular_output/preprocessors.py b/cli_helpers/tabular_output/preprocessors.py index a47fec0..05944d0 100644 --- a/cli_helpers/tabular_output/preprocessors.py +++ b/cli_helpers/tabular_output/preprocessors.py @@ -55,6 +55,26 @@ def convert_to_string(data, headers, **_): ) +def convert_to_undecoded_string(data, headers, **_): + """Convert all *data* and *headers* to hex, if needed. + + Binary data is converted to a hexadecimal representation via + :func:`binascii.hexlify`. + + Unlike convert_to_string(), None values are left as Nones. + + :param iterable data: An :term:`iterable` (e.g. list) of rows. + :param iterable headers: The column headers. + :return: The processed data and headers. + :rtype: tuple + + """ + return ( + ([utils.to_undecoded_string(v) for v in row] for row in data), + [utils.to_undecoded_string(h) for h in headers], + ) + + def override_missing_value( data, headers, diff --git a/cli_helpers/utils.py b/cli_helpers/utils.py index 053bdea..96e629e 100644 --- a/cli_helpers/utils.py +++ b/cli_helpers/utils.py @@ -34,6 +34,17 @@ def bytes_to_string(b): return b +def to_hex_if_bin(b): + """Convert bytes *b* to a string. + + Pass b through if not bytes. + + """ + if isinstance(b, binary_type): + return "0x" + binascii.hexlify(b).decode("ascii") + return b + + def to_string(value): """Convert *value* to a string.""" if isinstance(value, binary_type): @@ -42,6 +53,19 @@ def to_string(value): return text_type(value) +def to_undecoded_string(value): + """Convert *value* to an undecoded string, respecting Nones.""" + # preserve Nones so that + # * this can run before override_missing_value when stringifying + # * Nones are preserved in formats such as CSV + if value is None: + return None + elif isinstance(value, binary_type): + return to_hex_if_bin(value) + else: + return text_type(value) + + def truncate_string(value, max_width=None, skip_multiline_string=True): """Truncate string values.""" if skip_multiline_string and isinstance(value, text_type) and "\n" in value: diff --git a/tests/tabular_output/test_preprocessors.py b/tests/tabular_output/test_preprocessors.py index 5ebd06d..4e044a8 100644 --- a/tests/tabular_output/test_preprocessors.py +++ b/tests/tabular_output/test_preprocessors.py @@ -11,6 +11,7 @@ align_decimals, bytes_to_string, convert_to_string, + convert_to_undecoded_string, quote_whitespaces, override_missing_value, override_tab_value, @@ -38,6 +39,16 @@ def test_convert_to_string(): assert expected == (list(results[0]), results[1]) +def test_convert_to_undecoded_string(): + """Test the convert_to_undecoded_string() function.""" + data = [[1, "John"], [2, b"Jill"], [3, None]] + headers = [0, "name"] + expected = ([["1", "John"], ["2", "0x4a696c6c"], ["3", None]], ["0", "name"]) + results = convert_to_undecoded_string(data, headers) + + assert expected == (list(results[0]), results[1]) + + def test_override_missing_values(): """Test the override_missing_values() function.""" data = [[1, None], [2, "Jill"]]