diff --git a/CHANGELOG b/CHANGELOG index e756bfa..264948a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ # Changelog +- Register the JSON formats so they are actually usable. +- Make JSON formats able to encode Decimals and None/NULLs. + ## Version 2.5.0 - Added noheader CSV and TSV output formats. diff --git a/cli_helpers/tabular_output/json_output_adapter.py b/cli_helpers/tabular_output/json_output_adapter.py index 8176fce..9153fef 100644 --- a/cli_helpers/tabular_output/json_output_adapter.py +++ b/cli_helpers/tabular_output/json_output_adapter.py @@ -1,13 +1,22 @@ # -*- coding: utf-8 -*- """A JSON data output adapter""" +from decimal import Decimal from itertools import chain import json -from .preprocessors import bytes_to_string, override_missing_value, convert_to_string +from .preprocessors import bytes_to_string supported_formats = ("jsonl", "jsonl_escaped") -preprocessors = (override_missing_value, bytes_to_string, convert_to_string) +preprocessors = (bytes_to_string,) + + +class CustomEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, Decimal): + return float(o) + else: + return super(CustomEncoder, self).default(o) def adapter(data, headers, table_format="jsonl", **_kwargs): @@ -22,6 +31,7 @@ def adapter(data, headers, table_format="jsonl", **_kwargs): for row in chain(data): yield json.dumps( dict(zip(headers, row, strict=True)), + cls=CustomEncoder, separators=(",", ":"), ensure_ascii=ensure_ascii, ) diff --git a/cli_helpers/tabular_output/output_formatter.py b/cli_helpers/tabular_output/output_formatter.py index 6cadf6c..7fa2873 100644 --- a/cli_helpers/tabular_output/output_formatter.py +++ b/cli_helpers/tabular_output/output_formatter.py @@ -17,6 +17,7 @@ vertical_table_adapter, tabulate_adapter, tsv_output_adapter, + json_output_adapter, ) from decimal import Decimal @@ -253,3 +254,14 @@ def format_output(data, headers, format_name, **kwargs): tsv_output_adapter.preprocessors, {"table_format": tsv_format, "missing_value": "", "max_field_width": None}, ) + +for json_format in json_output_adapter.supported_formats: + TabularOutputFormatter.register_new_formatter( + json_format, + json_output_adapter.adapter, + json_output_adapter.preprocessors, + { + "table_format": json_format, + "max_field_width": None, + }, + ) diff --git a/tests/tabular_output/test_json_output_adapter.py b/tests/tabular_output/test_json_output_adapter.py index d0bd202..53dfeae 100644 --- a/tests/tabular_output/test_json_output_adapter.py +++ b/tests/tabular_output/test_json_output_adapter.py @@ -3,6 +3,8 @@ from __future__ import unicode_literals +from decimal import Decimal + from cli_helpers.tabular_output import json_output_adapter @@ -29,6 +31,28 @@ def test_unicode_with_jsonl(): ) +def test_decimal_with_jsonl(): + """Test that the jsonl wrapper can pass through Decimal values.""" + data = [["ab\r\nc", 1], ["d", Decimal(4.56)]] + headers = ["letters", "number"] + output = json_output_adapter.adapter(iter(data), headers, table_format="jsonl") + assert ( + "\n".join(output) + == """{"letters":"ab\\r\\nc","number":1}\n{"letters":"d","number":4.56}""" + ) + + +def test_null_with_jsonl(): + """Test that the jsonl wrapper can pass through null values.""" + data = [["ab\r\nc", None], ["d", None]] + headers = ["letters", "value"] + output = json_output_adapter.adapter(iter(data), headers, table_format="jsonl") + assert ( + "\n".join(output) + == """{"letters":"ab\\r\\nc","value":null}\n{"letters":"d","value":null}""" + ) + + def test_unicode_with_jsonl_esc(): """Test that the jsonl_escaped wrapper JSON-escapes non-ascii characters.""" data = [["观音", 1], ["Ποσειδῶν", 456]] diff --git a/tests/tabular_output/test_output_formatter.py b/tests/tabular_output/test_output_formatter.py index 8e1fa92..a76ca7c 100644 --- a/tests/tabular_output/test_output_formatter.py +++ b/tests/tabular_output/test_output_formatter.py @@ -177,6 +177,13 @@ def test_unsupported_format(): formatter.format_output((), (), format_name="foobar") +def test_supported_json_formats(): + """Test that the JSONl formats are known.""" + formatter = TabularOutputFormatter() + assert "jsonl" in formatter.supported_formats + assert "jsonl_escaped" in formatter.supported_formats + + def test_tabulate_ansi_escape_in_default_value(): """Test that ANSI escape codes work with tabulate."""