From 937c8583714216e926606b251bc9225271bdc5a7 Mon Sep 17 00:00:00 2001 From: "Yuichiro Tachibana (Tsuchiya)" Date: Thu, 25 Apr 2024 08:13:01 +0100 Subject: [PATCH] Use orjson to serialize dict including np.array (#8041) * Use orjson to serialize dict including np.array * add changeset * Update json_component demo and add an E2E test using it * Rename demo/json_component -> demo/json_component_blocks * Add json_component_interface demo and an E2E test using it * Fix to await assertion promises * Revert renaming of json_component demo * add changeset * Rename js/app/test/json_component_blocks.spec.ts -> js/app/test/json_component.spec.ts * Revert changes in routes.py and add orjson to json_component.py * Update gr.Checkbox.postprocess to ensure a bool value is returned * add changeset * Remove the if-block in gr.Checkbox.postprocess handling NumPy arrays as they are not reasonable values to be interpreted as checkbox's value * Update gr.JSON's docstring * Add test/components/test_json_component.py * Remove JSON component E2E tests * Update gr.JSON's docstring * docstring --------- Co-authored-by: gradio-pr-bot Co-authored-by: Abubakar Abid --- .changeset/free-trams-nail.md | 5 +++++ demo/json_component/run.ipynb | 2 +- demo/json_component/run.py | 25 ++++++++++++++++++++++--- gradio/components/checkbox.py | 2 +- gradio/components/json_component.py | 18 ++++++++++++++---- test/components/test_json_component.py | 25 +++++++++++++++++++++++++ 6 files changed, 68 insertions(+), 9 deletions(-) create mode 100644 .changeset/free-trams-nail.md create mode 100644 test/components/test_json_component.py diff --git a/.changeset/free-trams-nail.md b/.changeset/free-trams-nail.md new file mode 100644 index 0000000000..f8f764d1fc --- /dev/null +++ b/.changeset/free-trams-nail.md @@ -0,0 +1,5 @@ +--- +"gradio": patch +--- + +fix:Use orjson to serialize dict including np.array diff --git a/demo/json_component/run.ipynb b/demo/json_component/run.ipynb index c0959b07ed..20f149cda3 100644 --- a/demo/json_component/run.ipynb +++ b/demo/json_component/run.ipynb @@ -1 +1 @@ -{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: json_component"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["import gradio as gr \n", "\n", "with gr.Blocks() as demo:\n", " gr.JSON(value={\"Key 1\": \"Value 1\", \"Key 2\": {\"Key 3\": \"Value 2\", \"Key 4\": \"Value 3\"}, \"Key 5\": [\"Item 1\", \"Item 2\", \"Item 3\"]})\n", "\n", "demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5} \ No newline at end of file +{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: json_component"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "import numpy as np\n", "\n", "with gr.Blocks() as demo:\n", " inp = gr.JSON(\n", " label=\"InputJSON\",\n", " value={\n", " \"Key 1\": \"Value 1\",\n", " \"Key 2\": {\"Key 3\": \"Value 2\", \"Key 4\": \"Value 3\"},\n", " \"Key 5\": [\"Item 1\", \"Item 2\", \"Item 3\"],\n", " \"Key 6\": 123,\n", " \"Key 7\": 123.456,\n", " \"Key 8\": True,\n", " \"Key 9\": False,\n", " \"Key 10\": None,\n", " \"Key 11\": np.array([1, 2, 3]),\n", " }\n", " )\n", " out = gr.JSON(label=\"OutputJSON\")\n", " btn = gr.Button(\"Submit\")\n", " btn.click(lambda x: x, inp, out)\n", "\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5} \ No newline at end of file diff --git a/demo/json_component/run.py b/demo/json_component/run.py index d3fcd5ea9b..ba03c7f8ed 100644 --- a/demo/json_component/run.py +++ b/demo/json_component/run.py @@ -1,6 +1,25 @@ -import gradio as gr +import gradio as gr +import numpy as np with gr.Blocks() as demo: - gr.JSON(value={"Key 1": "Value 1", "Key 2": {"Key 3": "Value 2", "Key 4": "Value 3"}, "Key 5": ["Item 1", "Item 2", "Item 3"]}) + inp = gr.JSON( + label="InputJSON", + value={ + "Key 1": "Value 1", + "Key 2": {"Key 3": "Value 2", "Key 4": "Value 3"}, + "Key 5": ["Item 1", "Item 2", "Item 3"], + "Key 6": 123, + "Key 7": 123.456, + "Key 8": True, + "Key 9": False, + "Key 10": None, + "Key 11": np.array([1, 2, 3]), + } + ) + out = gr.JSON(label="OutputJSON") + btn = gr.Button("Submit") + btn.click(lambda x: x, inp, out) -demo.launch() \ No newline at end of file + +if __name__ == "__main__": + demo.launch() diff --git a/gradio/components/checkbox.py b/gradio/components/checkbox.py index f3a38f4f05..cfd9ba07c6 100644 --- a/gradio/components/checkbox.py +++ b/gradio/components/checkbox.py @@ -95,4 +95,4 @@ class Checkbox(FormComponent): Returns: The same `bool` value that is set as the status of the checkbox """ - return value + return bool(value) diff --git a/gradio/components/json_component.py b/gradio/components/json_component.py index 331306cdf6..a58f2c2789 100644 --- a/gradio/components/json_component.py +++ b/gradio/components/json_component.py @@ -5,6 +5,7 @@ from __future__ import annotations import json from typing import Any, Callable +import orjson from gradio_client.documentation import document from gradio.components.base import Component @@ -38,7 +39,7 @@ class JSON(Component): ): """ Parameters: - value: Default value. If callable, the function will be called whenever the app loads to set the initial value of the component. + value: Default value as a valid JSON `str` -- or a `list` or `dict` that can be serialized to a JSON string. If callable, the function will be called whenever the app loads to set the initial value of the component. label: The label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to. every: If `value` is a callable, run the function 'every' number of seconds while the client connection is open. Has no effect otherwise. The event can be accessed (e.g. to cancel it) via this component's .load_event attribute. show_label: if True, will display label. @@ -76,16 +77,25 @@ class JSON(Component): def postprocess(self, value: dict | list | str | None) -> dict | list | None: """ Parameters: - value: Expects a `str` filepath to a file containing valid JSON -- or a `list` or `dict` that is valid JSON + value: Expects a valid JSON `str` -- or a `list` or `dict` that can be serialized to a JSON string. The `list` or `dict` value can contain numpy arrays. Returns: Returns the JSON as a `list` or `dict`. """ if value is None: return None if isinstance(value, str): - return json.loads(value) + return orjson.loads(value) else: - return value + # Use orjson to convert NumPy arrays and datetime objects to JSON. + # This ensures a backward compatibility with the previous behavior. + # See https://github.com/gradio-app/gradio/pull/8041 + return orjson.loads( + orjson.dumps( + value, + option=orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_PASSTHROUGH_DATETIME, + default=str, + ) + ) def example_payload(self) -> Any: return {"foo": "bar"} diff --git a/test/components/test_json_component.py b/test/components/test_json_component.py new file mode 100644 index 0000000000..15b5a3fd9d --- /dev/null +++ b/test/components/test_json_component.py @@ -0,0 +1,25 @@ +import json + +import numpy as np +import pytest + +from gradio.components.json_component import JSON + + +class TestJSON: + @pytest.mark.parametrize( + "value, expected", + [ + (None, None), + (True, True), + ([1, 2, 3], [1, 2, 3]), + ([np.array([1, 2, 3])], [[1, 2, 3]]), + ({"foo": [1, 2, 3]}, {"foo": [1, 2, 3]}), + ({"foo": np.array([1, 2, 3])}, {"foo": [1, 2, 3]}), + ], + ) + def test_postprocess_returns_json_serializable_value(self, value, expected): + json_component = JSON() + postprocessed_value = json_component.postprocess(value) + assert postprocessed_value == expected + assert json.loads(json.dumps(postprocessed_value)) == expected