Skip to content

Commit ee503cb

Browse files
authored
add a convert_to_undecoded_string preprocessor (#100)
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.
1 parent d43bd40 commit ee503cb

File tree

4 files changed

+61
-0
lines changed

4 files changed

+61
-0
lines changed

CHANGELOG

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Version 2.9.0
4+
5+
(released on 2026-01-26)
6+
7+
- Add optional `convert_to_undecoded_string` preprocessor.
8+
39
## Version 2.8.2
410

511
(released on 2026-01-26)

cli_helpers/tabular_output/preprocessors.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,26 @@ def convert_to_string(data, headers, **_):
5555
)
5656

5757

58+
def convert_to_undecoded_string(data, headers, **_):
59+
"""Convert all *data* and *headers* to hex, if needed.
60+
61+
Binary data is converted to a hexadecimal representation via
62+
:func:`binascii.hexlify`.
63+
64+
Unlike convert_to_string(), None values are left as Nones.
65+
66+
:param iterable data: An :term:`iterable` (e.g. list) of rows.
67+
:param iterable headers: The column headers.
68+
:return: The processed data and headers.
69+
:rtype: tuple
70+
71+
"""
72+
return (
73+
([utils.to_undecoded_string(v) for v in row] for row in data),
74+
[utils.to_undecoded_string(h) for h in headers],
75+
)
76+
77+
5878
def override_missing_value(
5979
data,
6080
headers,

cli_helpers/utils.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,17 @@ def bytes_to_string(b):
3434
return b
3535

3636

37+
def to_hex_if_bin(b):
38+
"""Convert bytes *b* to a string.
39+
40+
Pass b through if not bytes.
41+
42+
"""
43+
if isinstance(b, binary_type):
44+
return "0x" + binascii.hexlify(b).decode("ascii")
45+
return b
46+
47+
3748
def to_string(value):
3849
"""Convert *value* to a string."""
3950
if isinstance(value, binary_type):
@@ -42,6 +53,19 @@ def to_string(value):
4253
return text_type(value)
4354

4455

56+
def to_undecoded_string(value):
57+
"""Convert *value* to an undecoded string, respecting Nones."""
58+
# preserve Nones so that
59+
# * this can run before override_missing_value when stringifying
60+
# * Nones are preserved in formats such as CSV
61+
if value is None:
62+
return None
63+
elif isinstance(value, binary_type):
64+
return to_hex_if_bin(value)
65+
else:
66+
return text_type(value)
67+
68+
4569
def truncate_string(value, max_width=None, skip_multiline_string=True):
4670
"""Truncate string values."""
4771
if skip_multiline_string and isinstance(value, text_type) and "\n" in value:

tests/tabular_output/test_preprocessors.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
align_decimals,
1212
bytes_to_string,
1313
convert_to_string,
14+
convert_to_undecoded_string,
1415
quote_whitespaces,
1516
override_missing_value,
1617
override_tab_value,
@@ -38,6 +39,16 @@ def test_convert_to_string():
3839
assert expected == (list(results[0]), results[1])
3940

4041

42+
def test_convert_to_undecoded_string():
43+
"""Test the convert_to_undecoded_string() function."""
44+
data = [[1, "John"], [2, b"Jill"], [3, None]]
45+
headers = [0, "name"]
46+
expected = ([["1", "John"], ["2", "0x4a696c6c"], ["3", None]], ["0", "name"])
47+
results = convert_to_undecoded_string(data, headers)
48+
49+
assert expected == (list(results[0]), results[1])
50+
51+
4152
def test_override_missing_values():
4253
"""Test the override_missing_values() function."""
4354
data = [[1, None], [2, "Jill"]]

0 commit comments

Comments
 (0)