diff --git a/datasette/renderer.py b/datasette/renderer.py index 0e01f52f..4cb519b9 100644 --- a/datasette/renderer.py +++ b/datasette/renderer.py @@ -30,6 +30,10 @@ def convert_specific_columns_to_json(rows, columns, json_cols): return new_rows +def _facet_without_infinites(facet): + return {**facet, "results": [remove_infinites(r) for r in facet["results"]]} + + def json_renderer(request, args, data, error, truncated=None): """Render a response as JSON""" status_code = 200 @@ -44,8 +48,23 @@ def json_renderer(request, args, data, error, truncated=None): ) # unless _json_infinity=1 requested, replace infinity with None - if "rows" in data and not value_as_boolean(args.get("_json_infinity", "0")): - data["rows"] = [remove_infinites(row) for row in data["rows"]] + if not value_as_boolean(args.get("_json_infinity", "0")): + if "rows" in data: + data["rows"] = [remove_infinites(row) for row in data["rows"]] + # Facet values and labels can be infinities too + if isinstance(data.get("facet_results"), dict): + data["facet_results"] = { + **data["facet_results"], + "results": { + name: _facet_without_infinites(facet) + for name, facet in data["facet_results"]["results"].items() + }, + } + if isinstance(data.get("sorted_facet_results"), list): + data["sorted_facet_results"] = [ + _facet_without_infinites(facet) + for facet in data["sorted_facet_results"] + ] # Deal with the _shape option shape = args.get("_shape", "objects") diff --git a/tests/test_table_api.py b/tests/test_table_api.py index 58a5ddc3..840076ff 100644 --- a/tests/test_table_api.py +++ b/tests/test_table_api.py @@ -1290,6 +1290,40 @@ async def test_infinity_returned_as_null(ds_client): ] +@pytest.mark.asyncio +@pytest.mark.parametrize("extra", ("facet_results", "sorted_facet_results")) +async def test_infinity_facet_values_returned_as_null(ds_client, extra): + # https://github.com/simonw/datasette/issues/2954 + response = await ds_client.get( + "/fixtures/infinity.json?_facet=value&_extra=" + extra + ) + assert response.status_code == 200 + + def reject_constant(token): + raise ValueError(f"Non-JSON numeric literal: {token}") + + data = json.loads(response.text, parse_constant=reject_constant) + facets = data[extra] + facet = facets["results"]["value"] if extra == "facet_results" else facets[0] + values = sorted( + ((r["value"], r["label"]) for r in facet["results"]), + key=lambda pair: (pair[0] is not None, pair), + ) + assert values == [(None, None), (None, None), (1.5, 1.5)] + + +@pytest.mark.asyncio +async def test_infinity_facet_values_returned_as_invalid_json_if_requested(ds_client): + response = await ds_client.get( + "/fixtures/infinity.json?_facet=value&_extra=facet_results&_json_infinity=1" + ) + values = { + r["value"] + for r in response.json()["facet_results"]["results"]["value"]["results"] + } + assert values == {float("inf"), float("-inf"), 1.5} + + @pytest.mark.asyncio async def test_infinity_returned_as_invalid_json_if_requested(ds_client): response = await ds_client.get(