diff --git a/CHANGELOG.md b/CHANGELOG.md index 310cfb6f689f4..b1dae99ada1b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,10 @@ ## New Features: -No changes to highlight. +- Returning language agnostic types in the `/info` route by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4039](https://github.com/gradio-app/gradio/pull/4039) ## Bug Fixes: - - Fixed bug where type hints in functions caused the event handler to crash by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4068](https://github.com/gradio-app/gradio/pull/4068) - Fix dropdown default value not appearing by [@aliabid94](https://github.com/aliabid94) in [PR 4072](https://github.com/gradio-app/gradio/pull/4072). - Soft theme label color fix by [@aliabid94](https://github.com/aliabid94) in [PR 4070](https://github.com/gradio-app/gradio/pull/4070) diff --git a/client/python/gradio_client/client.py b/client/python/gradio_client/client.py index e44c0b75668f0..699eb2823f7da 100644 --- a/client/python/gradio_client/client.py +++ b/client/python/gradio_client/client.py @@ -30,7 +30,12 @@ from gradio_client import serializing, utils from gradio_client.documentation import document, set_documentation_group from gradio_client.serializing import Serializable -from gradio_client.utils import Communicator, JobStatus, Status, StatusUpdate +from gradio_client.utils import ( + Communicator, + JobStatus, + Status, + StatusUpdate, +) set_documentation_group("py-client") @@ -399,17 +404,17 @@ def view_api( api_info_url = urllib.parse.urljoin(self.src, utils.RAW_API_INFO_URL) r = requests.get(api_info_url, headers=self.headers) - # Versions of Gradio older than 3.26 returned format of the API info + # Versions of Gradio older than 3.28.3 returned format of the API info # from the /info endpoint if ( - version.parse(self.config.get("version", "2.0")) >= version.Version("3.26") + version.parse(self.config.get("version", "2.0")) > version.Version("3.28.3") and r.ok ): info = r.json() else: fetch = requests.post( utils.SPACE_FETCHER_URL, - json={"serialize": self.serialize, "config": json.dumps(self.config)}, + json={"config": json.dumps(self.config), "serialize": self.serialize}, ) if fetch.ok: info = fetch.json()["api"] @@ -449,7 +454,7 @@ def reset_session(self) -> None: def _render_endpoints_info( self, name_or_index: str | int, - endpoints_info: dict[str, list[dict[str, str]]], + endpoints_info: dict[str, list[dict[str, Any]]], ) -> str: parameter_names = [p["label"] for p in endpoints_info["parameters"]] parameter_names = [utils.sanitize_parameter_names(p) for p in parameter_names] @@ -473,13 +478,25 @@ def _render_endpoints_info( human_info += " Parameters:\n" if endpoints_info["parameters"]: for info in endpoints_info["parameters"]: - human_info += f" - [{info['component']}] {utils.sanitize_parameter_names(info['label'])}: {info['type_python']} ({info['type_description']})\n" + desc = ( + f" ({info['python_type']['description']})" + if info["python_type"].get("description") + else "" + ) + type_ = info["python_type"]["type"] + human_info += f" - [{info['component']}] {utils.sanitize_parameter_names(info['label'])}: {type_}{desc} \n" else: human_info += " - None\n" human_info += " Returns:\n" if endpoints_info["returns"]: for info in endpoints_info["returns"]: - human_info += f" - [{info['component']}] {utils.sanitize_parameter_names(info['label'])}: {info['type_python']} ({info['type_description']})\n" + desc = ( + f" ({info['python_type']['description']})" + if info["python_type"].get("description") + else "" + ) + type_ = info["python_type"]["type"] + human_info += f" - [{info['component']}] {utils.sanitize_parameter_names(info['label'])}: {type_}{desc} \n" else: human_info += " - None\n" diff --git a/client/python/gradio_client/serializing.py b/client/python/gradio_client/serializing.py index 02c9507355b89..9f496f3a5e947 100644 --- a/client/python/gradio_client/serializing.py +++ b/client/python/gradio_client/serializing.py @@ -9,8 +9,17 @@ from gradio_client import media_data, utils from gradio_client.data_classes import FileData +serializer_types = json.load(open(Path(__file__).parent / "types.json")) + class Serializable: + def serialized_info(self): + """ + The typing information for this component as a dictionary whose values are a list of 2 strings: [Python type, language-agnostic description]. + Keys of the dictionary are: raw_input, raw_output, serialized_input, serialized_output + """ + return self.api_info() + def api_info(self) -> dict[str, list[str]]: """ The typing information for this component as a dictionary whose values are a list of 2 strings: [Python type, language-agnostic description]. @@ -57,12 +66,10 @@ def deserialize( class SimpleSerializable(Serializable): """General class that does not perform any serialization or deserialization.""" - def api_info(self) -> dict[str, str | list[str]]: + def api_info(self) -> dict[str, bool | dict]: return { - "raw_input": ["Any", ""], - "raw_output": ["Any", ""], - "serialized_input": ["Any", ""], - "serialized_output": ["Any", ""], + "info": serializer_types["SimpleSerializable"], + "serialized_info": False, } def example_inputs(self) -> dict[str, Any]: @@ -75,12 +82,10 @@ def example_inputs(self) -> dict[str, Any]: class StringSerializable(Serializable): """Expects a string as input/output but performs no serialization.""" - def api_info(self) -> dict[str, list[str]]: + def api_info(self) -> dict[str, bool | dict]: return { - "raw_input": ["str", "string value"], - "raw_output": ["str", "string value"], - "serialized_input": ["str", "string value"], - "serialized_output": ["str", "string value"], + "info": serializer_types["StringSerializable"], + "serialized_info": False, } def example_inputs(self) -> dict[str, Any]: @@ -93,12 +98,10 @@ def example_inputs(self) -> dict[str, Any]: class ListStringSerializable(Serializable): """Expects a list of strings as input/output but performs no serialization.""" - def api_info(self) -> dict[str, list[str]]: + def api_info(self) -> dict[str, bool | dict]: return { - "raw_input": ["List[str]", "list of string values"], - "raw_output": ["List[str]", "list of string values"], - "serialized_input": ["List[str]", "list of string values"], - "serialized_output": ["List[str]", "list of string values"], + "info": serializer_types["ListStringSerializable"], + "serialized_info": False, } def example_inputs(self) -> dict[str, Any]: @@ -111,12 +114,10 @@ def example_inputs(self) -> dict[str, Any]: class BooleanSerializable(Serializable): """Expects a boolean as input/output but performs no serialization.""" - def api_info(self) -> dict[str, list[str]]: + def api_info(self) -> dict[str, bool | dict]: return { - "raw_input": ["bool", "boolean value"], - "raw_output": ["bool", "boolean value"], - "serialized_input": ["bool", "boolean value"], - "serialized_output": ["bool", "boolean value"], + "info": serializer_types["BooleanSerializable"], + "serialized_info": False, } def example_inputs(self) -> dict[str, Any]: @@ -129,12 +130,10 @@ def example_inputs(self) -> dict[str, Any]: class NumberSerializable(Serializable): """Expects a number (int/float) as input/output but performs no serialization.""" - def api_info(self) -> dict[str, list[str]]: + def api_info(self) -> dict[str, bool | dict]: return { - "raw_input": ["int | float", "numeric value"], - "raw_output": ["int | float", "numeric value"], - "serialized_input": ["int | float", "numeric value"], - "serialized_output": ["int | float", "numeric value"], + "info": serializer_types["NumberSerializable"], + "serialized_info": False, } def example_inputs(self) -> dict[str, Any]: @@ -147,13 +146,11 @@ def example_inputs(self) -> dict[str, Any]: class ImgSerializable(Serializable): """Expects a base64 string as input/output which is serialized to a filepath.""" - def api_info(self) -> dict[str, list[str]]: - return { - "raw_input": ["str", "base64 representation of image"], - "raw_output": ["str", "base64 representation of image"], - "serialized_input": ["str", "filepath or URL to image"], - "serialized_output": ["str", "filepath or URL to image"], - } + def serialized_info(self): + return {"type": "string", "description": "filepath or URL to image"} + + def api_info(self) -> dict[str, bool | dict]: + return {"info": serializer_types["ImgSerializable"], "serialized_info": True} def example_inputs(self) -> dict[str, Any]: return { @@ -204,20 +201,34 @@ def deserialize( class FileSerializable(Serializable): """Expects a dict with base64 representation of object as input/output which is serialized to a filepath.""" - def api_info(self) -> dict[str, list[str]]: + def serialized_info(self): + return self._single_file_serialized_info() + + def _single_file_api_info(self): + return { + "info": serializer_types["SingleFileSerializable"], + "serialized_info": True, + } + + def _single_file_serialized_info(self): + return {"type": "string", "description": "filepath or URL to file"} + + def _multiple_file_serialized_info(self): + return { + "type": "array", + "description": "List of filepath(s) or URL(s) to files", + "items": {"type": "string", "description": "filepath or URL to file"}, + } + + def _multiple_file_api_info(self): return { - "raw_input": [ - "str | Dict", - "base64 string representation of file; or a dictionary-like object, the keys should be either: is_file (False), data (base64 representation of file) or is_file (True), name (str filename)", - ], - "raw_output": [ - "Dict", - "dictionary-like object with keys: name (str filename), data (base64 representation of file), is_file (bool, set to False)", - ], - "serialized_input": ["str", "filepath or URL to file"], - "serialized_output": ["str", "filepath or URL to file"], + "info": serializer_types["MultipleFileSerializable"], + "serialized_info": True, } + def api_info(self) -> dict[str, dict | bool]: + return self._single_file_api_info() + def example_inputs(self) -> dict[str, Any]: return { "raw": {"is_file": False, "data": media_data.BASE64_FILE}, @@ -331,19 +342,11 @@ def deserialize( class VideoSerializable(FileSerializable): - def api_info(self) -> dict[str, list[str]]: - return { - "raw_input": [ - "str | Dict", - "base64 string representation of file; or a dictionary-like object, the keys should be either: is_file (False), data (base64 representation of file) or is_file (True), name (str filename)", - ], - "raw_output": [ - "Tuple[Dict, Dict]", - "a tuple of 2 dictionary-like object with keys: name (str filename), data (base64 representation of file), is_file (bool, set to False). First dictionary is for the video, second dictionary is for the subtitles.", - ], - "serialized_input": ["str", "filepath or URL to file"], - "serialized_output": ["str", "filepath or URL to file"], - } + def serialized_info(self): + return {"type": "string", "description": "filepath or URL to video file"} + + def api_info(self) -> dict[str, dict | bool]: + return {"info": serializer_types["FileSerializable"], "serialized_info": True} def example_inputs(self) -> dict[str, Any]: return { @@ -378,13 +381,11 @@ def deserialize( class JSONSerializable(Serializable): - def api_info(self) -> dict[str, list[str]]: - return { - "raw_input": ["str | Dict | List", "JSON-serializable object or a string"], - "raw_output": ["Dict | List", "dictionary- or list-like object"], - "serialized_input": ["str", "filepath to JSON file"], - "serialized_output": ["str", "filepath to JSON file"], - } + def serialized_info(self): + return {"type": "string", "description": "filepath to JSON file"} + + def api_info(self) -> dict[str, dict | bool]: + return {"info": serializer_types["JSONSerializable"], "serialized_info": True} def example_inputs(self) -> dict[str, Any]: return { @@ -430,24 +431,16 @@ def deserialize( class GallerySerializable(Serializable): - def api_info(self) -> dict[str, list[str]]: + def serialized_info(self): + return { + "type": "string", + "description": "path to directory with images and a file associating images with captions called captions.json", + } + + def api_info(self) -> dict[str, dict | bool]: return { - "raw_input": [ - "List[List[str | None]]", - "List of lists. The inner lists should contain two elements: a base64 file representation and an optional caption, the outer list should contain one such list for each image in the gallery.", - ], - "raw_output": [ - "List[List[str | None]]", - "List of lists. The inner lists should contain two elements: a base64 file representation and an optional caption, the outer list should contain one such list for each image in the gallery.", - ], - "serialized_input": [ - "str", - "path to directory with images and a file associating images with captions called captions.json", - ], - "serialized_output": [ - "str", - "path to directory with images and a file associating images with captions called captions.json", - ], + "info": serializer_types["GallerySerializable"], + "serialized_info": True, } def example_inputs(self) -> dict[str, Any]: diff --git a/client/python/gradio_client/types.json b/client/python/gradio_client/types.json new file mode 100644 index 0000000000000..8fe876126f600 --- /dev/null +++ b/client/python/gradio_client/types.json @@ -0,0 +1,199 @@ +{ + "SimpleSerializable": { + "type": {}, + "description": "any valid value" + }, + "StringSerializable": { + "type": "string" + }, + "ListStringSerializable": { + "type": "array", + "items": { + "type": "string" + } + }, + "BooleanSerializable": { + "type": "boolean" + }, + "NumberSerializable": { + "type": "number" + }, + "ImgSerializable": { + "type": "string", + "description": "base64 representation of an image" + }, + "FileSerializable": { + "oneOf": [ + { + "type": "string", + "description": "filepath or URL to file" + }, + { + "type": "object", + "properties": { + "name": { "type": "string", "description": "name of file" }, + "data": { + "type": "string", + "description": "base64 representation of file" + }, + "size": { + "type": "integer", + "description": "size of image in bytes" + }, + "is_file": { + "type": "boolean", + "description": "true if the file has been uploaded to the server" + }, + "orig_name": { + "type": "string", + "description": "original name of the file" + } + }, + "required": ["name", "data"] + }, + { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string", + "description": "filepath or URL to file" + }, + { + "type": "object", + "properties": { + "name": { "type": "string", "description": "name of file" }, + "data": { + "type": "string", + "description": "base64 representation of file" + }, + "size": { + "type": "integer", + "description": "size of image in bytes" + }, + "is_file": { + "type": "boolean", + "description": "true if the file has been uploaded to the server" + }, + "orig_name": { + "type": "string", + "description": "original name of the file" + } + }, + "required": ["name", "data"] + } + ] + } + } + ] + }, + "SingleFileSerializable": { + "oneOf": [ + { + "type": "string", + "description": "filepath or URL to file" + }, + { + "type": "object", + "properties": { + "name": { "type": "string", "description": "name of file" }, + "data": { + "type": "string", + "description": "base64 representation of file" + }, + "size": { + "type": "integer", + "description": "size of image in bytes" + }, + "is_file": { + "type": "boolean", + "description": "true if the file has been uploaded to the server" + }, + "orig_name": { + "type": "string", + "description": "original name of the file" + } + }, + "required": ["name", "data"] + } + ] + }, + "MultipleFileSerializable": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string", + "description": "filepath or URL to file" + }, + { + "type": "object", + "properties": { + "name": { "type": "string", "description": "name of file" }, + "data": { + "type": "string", + "description": "base64 representation of file" + }, + "size": { + "type": "integer", + "description": "size of image in bytes" + }, + "is_file": { + "type": "boolean", + "description": "true if the file has been uploaded to the server" + }, + "orig_name": { + "type": "string", + "description": "original name of the file" + } + }, + "required": ["name", "data"] + } + ] + } + }, + "JSONSerializable": { + "type": {}, + "description": "any valid json" + }, + "GallerySerializable": { + "type": "array", + "items": { + "type": "array", + "items": false, + "maxSize": 2, + "minSize": 2, + "prefixItems": [ + { + "type": "object", + "properties": { + "name": { "type": "string", "description": "name of file" }, + "data": { + "type": "string", + "description": "base64 representation of file" + }, + "size": { + "type": "integer", + "description": "size of image in bytes" + }, + "is_file": { + "type": "boolean", + "description": "true if the file has been uploaded to the server" + }, + "orig_name": { + "type": "string", + "description": "original name of the file" + } + }, + "required": ["name", "data"] + }, + { + "oneOf": [ + { "type": "string", "description": "caption of image" }, + { "type": "null" } + ] + } + ] + } + } +} diff --git a/client/python/gradio_client/utils.py b/client/python/gradio_client/utils.py index 7e4f4f62fe664..a9bcec285aa6e 100644 --- a/client/python/gradio_client/utils.py +++ b/client/python/gradio_client/utils.py @@ -29,7 +29,7 @@ CONFIG_URL = "/config" API_INFO_URL = "/info" RAW_API_INFO_URL = "/info?serialize=False" -SPACE_FETCHER_URL = "https://gradio-space-api-fetcher.hf.space/api" +SPACE_FETCHER_URL = "https://gradio-space-api-fetcher-v2.hf.space/api" RESET_URL = "/reset" SPACE_URL = "https://hf.space/{}" @@ -487,3 +487,61 @@ def synchronize_async(func: Callable, *args, **kwargs) -> Any: **kwargs: """ return fsspec.asyn.sync(fsspec.asyn.get_loop(), func, *args, **kwargs) # type: ignore + + +class APIInfoParseError(ValueError): + pass + + +def get_type(schema: dict): + if "type" in schema: + return schema["type"] + elif schema.get("oneOf"): + return "oneOf" + elif schema.get("anyOf"): + return "anyOf" + else: + raise APIInfoParseError(f"Cannot parse type for {schema}") + + +def json_schema_to_python_type(schema: Any) -> str: + """Convert the json schema into a python type hint""" + type_ = get_type(schema) + if type_ == {}: + if "json" in schema["description"]: + return "Dict[Any, Any]" + else: + return "Any" + elif type_ == "null": + return "None" + elif type_ == "integer": + return "int" + elif type_ == "string": + return "str" + elif type_ == "boolean": + return "bool" + elif type_ == "number": + return "int | float" + elif type_ == "array": + items = schema.get("items") + if "prefixItems" in items: + elements = ", ".join( + [json_schema_to_python_type(i) for i in items["prefixItems"]] + ) + return f"Tuple[{elements}]" + else: + elements = json_schema_to_python_type(items) + return f"List[{elements}]" + elif type_ == "object": + des = ", ".join( + [ + f"{n}: {json_schema_to_python_type(v)} ({v.get('description')})" + for n, v in schema["properties"].items() + ] + ) + return f"Dict({des})" + elif type_ in ["oneOf", "anyOf"]: + desc = " | ".join([json_schema_to_python_type(i) for i in schema[type_]]) + return desc + else: + raise APIInfoParseError(f"Cannot parse schema {schema}") diff --git a/client/python/gradio_client/version.txt b/client/python/gradio_client/version.txt index 845639eef26c0..0ea3a944b399d 100644 --- a/client/python/gradio_client/version.txt +++ b/client/python/gradio_client/version.txt @@ -1 +1 @@ -0.1.4 +0.2.0 diff --git a/client/python/test/conftest.py b/client/python/test/conftest.py index 8049ad9d5d24e..ddcf4a1a8b7a4 100644 --- a/client/python/test/conftest.py +++ b/client/python/test/conftest.py @@ -157,3 +157,14 @@ def show(n): list_btn.click(show, num, out) return demo.queue() + + +@pytest.fixture +def file_io_demo(): + demo = gr.Interface( + lambda x: print("foox"), + [gr.File(file_count="multiple"), "file"], + [gr.File(file_count="multiple"), "file"], + ) + + return demo diff --git a/client/python/test/test_client.py b/client/python/test/test_client.py index c4fa25c48cbb5..495bbfa38073d 100644 --- a/client/python/test/test_client.py +++ b/client/python/test/test_client.py @@ -14,7 +14,7 @@ from huggingface_hub.utils import RepositoryNotFoundError from gradio_client import Client -from gradio_client.serializing import SimpleSerializable +from gradio_client.serializing import Serializable from gradio_client.utils import Communicator, ProgressUnit, Status, StatusUpdate os.environ["HF_HUB_DISABLE_TELEMETRY"] = "1" @@ -490,32 +490,45 @@ def test_numerical_to_label_space(self): "parameters": [ { "label": "Sex", - "type_python": "str", - "type_description": "string value", + "type": {"type": "string"}, + "python_type": {"type": "str", "description": ""}, "component": "Radio", "example_input": "Howdy!", + "serializer": "StringSerializable", }, { "label": "Age", - "type_python": "int | float", - "type_description": "numeric value", + "type": {"type": "number"}, + "python_type": { + "type": "int | float", + "description": "", + }, "component": "Slider", "example_input": 5, + "serializer": "NumberSerializable", }, { "label": "Fare (british pounds)", - "type_python": "int | float", - "type_description": "numeric value", + "type": {"type": "number"}, + "python_type": { + "type": "int | float", + "description": "", + }, "component": "Slider", "example_input": 5, + "serializer": "NumberSerializable", }, ], "returns": [ { "label": "output", - "type_python": "str", - "type_description": "filepath to JSON file", + "type": {"type": {}, "description": "any valid json"}, + "python_type": { + "type": "str", + "description": "filepath to JSON file", + }, "component": "Label", + "serializer": "JSONSerializable", } ], }, @@ -523,32 +536,45 @@ def test_numerical_to_label_space(self): "parameters": [ { "label": "Sex", - "type_python": "str", - "type_description": "string value", + "type": {"type": "string"}, + "python_type": {"type": "str", "description": ""}, "component": "Radio", "example_input": "Howdy!", + "serializer": "StringSerializable", }, { "label": "Age", - "type_python": "int | float", - "type_description": "numeric value", + "type": {"type": "number"}, + "python_type": { + "type": "int | float", + "description": "", + }, "component": "Slider", "example_input": 5, + "serializer": "NumberSerializable", }, { "label": "Fare (british pounds)", - "type_python": "int | float", - "type_description": "numeric value", + "type": {"type": "number"}, + "python_type": { + "type": "int | float", + "description": "", + }, "component": "Slider", "example_input": 5, + "serializer": "NumberSerializable", }, ], "returns": [ { "label": "output", - "type_python": "str", - "type_description": "filepath to JSON file", + "type": {"type": {}, "description": "any valid json"}, + "python_type": { + "type": "str", + "description": "filepath to JSON file", + }, "component": "Label", + "serializer": "JSONSerializable", } ], }, @@ -556,32 +582,45 @@ def test_numerical_to_label_space(self): "parameters": [ { "label": "Sex", - "type_python": "str", - "type_description": "string value", + "type": {"type": "string"}, + "python_type": {"type": "str", "description": ""}, "component": "Radio", "example_input": "Howdy!", + "serializer": "StringSerializable", }, { "label": "Age", - "type_python": "int | float", - "type_description": "numeric value", + "type": {"type": "number"}, + "python_type": { + "type": "int | float", + "description": "", + }, "component": "Slider", "example_input": 5, + "serializer": "NumberSerializable", }, { "label": "Fare (british pounds)", - "type_python": "int | float", - "type_description": "numeric value", + "type": {"type": "number"}, + "python_type": { + "type": "int | float", + "description": "", + }, "component": "Slider", "example_input": 5, + "serializer": "NumberSerializable", }, ], "returns": [ { "label": "output", - "type_python": "str", - "type_description": "filepath to JSON file", + "type": {"type": {}, "description": "any valid json"}, + "python_type": { + "type": "str", + "description": "filepath to JSON file", + }, "component": "Label", + "serializer": "JSONSerializable", } ], }, @@ -593,8 +632,7 @@ def test_numerical_to_label_space(self): def test_serializable_in_mapping(self, calculator_demo): with connect(calculator_demo) as client: assert all( - isinstance(c, SimpleSerializable) - for c in client.endpoints[0].serializers + isinstance(c, Serializable) for c in client.endpoints[0].serializers ) @pytest.mark.flaky @@ -609,18 +647,20 @@ def test_private_space(self): "parameters": [ { "label": "x", - "type_python": "str", - "type_description": "string value", + "type": {"type": "string"}, + "python_type": {"type": "str", "description": ""}, "component": "Textbox", "example_input": "Howdy!", + "serializer": "StringSerializable", } ], "returns": [ { "label": "output", - "type_python": "str", - "type_description": "string value", + "type": {"type": "string"}, + "python_type": {"type": "str", "description": ""}, "component": "Textbox", + "serializer": "StringSerializable", } ], } @@ -636,32 +676,45 @@ def test_fetch_old_version_space(self): "parameters": [ { "label": "num1", - "type_python": "int | float", - "type_description": "numeric value", + "type": {"type": "number"}, + "python_type": { + "type": "int | float", + "description": "", + }, "component": "Number", "example_input": 5, + "serializer": "NumberSerializable", }, { "label": "operation", - "type_python": "str", - "type_description": "string value", + "type": {"type": "string"}, + "python_type": {"type": "str", "description": ""}, "component": "Radio", "example_input": "Howdy!", + "serializer": "StringSerializable", }, { "label": "num2", - "type_python": "int | float", - "type_description": "numeric value", + "type": {"type": "number"}, + "python_type": { + "type": "int | float", + "description": "", + }, "component": "Number", "example_input": 5, + "serializer": "NumberSerializable", }, ], "returns": [ { "label": "output", - "type_python": "int | float", - "type_description": "numeric value", + "type": {"type": "number"}, + "python_type": { + "type": "int | float", + "description": "", + }, "component": "Number", + "serializer": "NumberSerializable", } ], } @@ -676,6 +729,28 @@ def test_unnamed_endpoints_use_fn_index(self, count_generator_demo): assert "fn_index=0" in info assert "api_name" not in info + def test_file_io(self, file_io_demo): + with connect(file_io_demo) as client: + info = client.view_api(return_format="dict") + inputs = info["named_endpoints"]["/predict"]["parameters"] + outputs = info["named_endpoints"]["/predict"]["returns"] + assert inputs[0]["python_type"] == { + "type": "List[str]", + "description": "List of filepath(s) or URL(s) to files", + } + assert inputs[1]["python_type"] == { + "type": "str", + "description": "filepath or URL to file", + } + assert outputs[0]["python_type"] == { + "type": "List[str]", + "description": "List of filepath(s) or URL(s) to files", + } + assert outputs[1]["python_type"] == { + "type": "str", + "description": "filepath or URL to file", + } + class TestEndpoints: def test_upload(self): diff --git a/client/python/test/test_serializing.py b/client/python/test/test_serializing.py index 5fe804e99041b..4576b3076572d 100644 --- a/client/python/test/test_serializing.py +++ b/client/python/test/test_serializing.py @@ -1,12 +1,24 @@ import os import tempfile +import pytest from gradio import components -from gradio_client.serializing import COMPONENT_MAPPING, FileSerializable +from gradio_client.serializing import COMPONENT_MAPPING, FileSerializable, Serializable from gradio_client.utils import encode_url_or_file_to_base64 +@pytest.mark.parametrize("serializer_class", Serializable.__subclasses__()) +def test_duplicate(serializer_class): + if "gradio_client" not in serializer_class.__module__: + pytest.skip(f"{serializer_class} not defined in gradio_client") + serializer = serializer_class() + info = serializer.api_info() + assert "info" in info and "serialized_info" in info + if "serialized_info" in info: + assert serializer.serialized_info() + + def test_check_component_fallback_serializers(): for component_name, class_type in COMPONENT_MAPPING.items(): if component_name == "dataset": # cannot be instantiated without parameters diff --git a/client/python/test/test_utils.py b/client/python/test/test_utils.py index f3b5f09bf9bbb..b6a7717bc362b 100644 --- a/client/python/test/test_utils.py +++ b/client/python/test/test_utils.py @@ -1,3 +1,4 @@ +import importlib.resources import json import tempfile from copy import deepcopy @@ -9,6 +10,13 @@ from gradio_client import media_data, utils +types = json.loads(importlib.resources.read_text("gradio_client", "types.json")) +types["MultipleFile"] = { + "type": "array", + "items": {"type": "string", "description": "filepath or URL to file"}, +} +types["SingleFile"] = {"type": "string", "description": "filepath or URL to file"} + def test_encode_url_or_file_to_base64(): output_base64 = utils.encode_url_or_file_to_base64( @@ -120,3 +128,36 @@ def test_sleep_successful(mock_post): def test_sleep_unsuccessful(mock_post): with pytest.raises(utils.SpaceDuplicationError): utils.set_space_timeout("gradio/calculator") + + +@pytest.mark.parametrize("schema", types) +def test_json_schema_to_python_type(schema): + if schema == "SimpleSerializable": + answer = "Any" + elif schema == "StringSerializable": + answer = "str" + elif schema == "ListStringSerializable": + answer = "List[str]" + elif schema == "BooleanSerializable": + answer = "bool" + elif schema == "NumberSerializable": + answer = "int | float" + elif schema == "ImgSerializable": + answer = "str" + elif schema == "FileSerializable": + answer = "str | Dict(name: str (name of file), data: str (base64 representation of file), size: int (size of image in bytes), is_file: bool (true if the file has been uploaded to the server), orig_name: str (original name of the file)) | List[str | Dict(name: str (name of file), data: str (base64 representation of file), size: int (size of image in bytes), is_file: bool (true if the file has been uploaded to the server), orig_name: str (original name of the file))]" + elif schema == "JSONSerializable": + answer = "Dict[Any, Any]" + elif schema == "GallerySerializable": + answer = "Tuple[Dict(name: str (name of file), data: str (base64 representation of file), size: int (size of image in bytes), is_file: bool (true if the file has been uploaded to the server), orig_name: str (original name of the file)), str | None]" + elif schema == "SingleFileSerializable": + answer = "str | Dict(name: str (name of file), data: str (base64 representation of file), size: int (size of image in bytes), is_file: bool (true if the file has been uploaded to the server), orig_name: str (original name of the file))" + elif schema == "MultipleFileSerializable": + answer = "List[str | Dict(name: str (name of file), data: str (base64 representation of file), size: int (size of image in bytes), is_file: bool (true if the file has been uploaded to the server), orig_name: str (original name of the file))]" + elif schema == "SingleFile": + answer = "str" + elif schema == "MultipleFile": + answer = "List[str]" + else: + raise ValueError(f"This test has not been modified to check {schema}") + assert utils.json_schema_to_python_type(types[schema]) == answer diff --git a/gradio/blocks.py b/gradio/blocks.py index cf74b2c65cb2f..1d717e19e1ff9 100644 --- a/gradio/blocks.py +++ b/gradio/blocks.py @@ -20,6 +20,7 @@ from gradio_client import serializing from gradio_client import utils as client_utils from gradio_client.documentation import document, set_documentation_group +from packaging import version from typing_extensions import Literal from gradio import ( @@ -468,6 +469,9 @@ def get_api_info(config: dict, serialize: bool = True): """ api_info = {"named_endpoints": {}, "unnamed_endpoints": {}} mode = config.get("mode", None) + after_new_format = version.parse(config.get("version", "2.0")) > version.Version( + "3.28.3" + ) for d, dependency in enumerate(config["dependencies"]): dependency_info = {"parameters": [], "returns": []} @@ -494,29 +498,36 @@ def get_api_info(config: dict, serialize: bool = True): # The config has the most specific API info (taking into account the parameters # of the component), so we use that if it exists. Otherwise, we fallback to the # Serializer's API info. - if component.get("api_info"): - if serialize: - info = component["api_info"]["serialized_input"] - example = component["example_inputs"]["serialized"] - else: - info = component["api_info"]["raw_input"] - example = component["example_inputs"]["raw"] + serializer = serializing.COMPONENT_MAPPING[type]() + if component.get("api_info") and after_new_format: + info = component["api_info"] + example = component["example_inputs"]["serialized"] else: - serializer = serializing.COMPONENT_MAPPING[type]() assert isinstance(serializer, serializing.Serializable) - if serialize: - info = serializer.api_info()["serialized_input"] - example = serializer.example_inputs()["serialized"] - else: - info = serializer.api_info()["raw_input"] - example = serializer.example_inputs()["raw"] + info = serializer.api_info() + example = serializer.example_inputs()["raw"] + python_info = info["info"] + if serialize and info["serialized_info"]: + python_info = serializer.serialized_info() + if ( + isinstance(serializer, serializing.FileSerializable) + and component["props"].get("file_count", "single") != "single" + ): + python_info = serializer._multiple_file_serialized_info() + + python_type = client_utils.json_schema_to_python_type(python_info) + serializer_name = serializing.COMPONENT_MAPPING[type].__name__ dependency_info["parameters"].append( { "label": label, - "type_python": info[0], - "type_description": info[1], + "type": info["info"], + "python_type": { + "type": python_type, + "description": python_info.get("description", ""), + }, "component": type.capitalize(), "example_input": example, + "serializer": serializer_name, } ) @@ -540,16 +551,27 @@ def get_api_info(config: dict, serialize: bool = True): label = component["props"].get("label", f"value_{o}") serializer = serializing.COMPONENT_MAPPING[type]() assert isinstance(serializer, serializing.Serializable) - if serialize: - info = serializer.api_info()["serialized_output"] - else: - info = serializer.api_info()["raw_output"] + info = serializer.api_info() + python_info = info["info"] + if serialize and info["serialized_info"]: + python_info = serializer.serialized_info() + if ( + isinstance(serializer, serializing.FileSerializable) + and component["props"].get("file_count", "single") != "single" + ): + python_info = serializer._multiple_file_serialized_info() + python_type = client_utils.json_schema_to_python_type(python_info) + serializer_name = serializing.COMPONENT_MAPPING[type].__name__ dependency_info["returns"].append( { "label": label, - "type_python": info[0], - "type_description": info[1], + "type": info["info"], + "python_type": { + "type": python_type, + "description": python_info.get("description", ""), + }, "component": type.capitalize(), + "serializer": serializer_name, } ) diff --git a/gradio/components.py b/gradio/components.py index f557ff6e857bf..bc45926cd2172 100644 --- a/gradio/components.py +++ b/gradio/components.py @@ -845,13 +845,13 @@ def __init__( NeighborInterpretable.__init__(self) self.cleared_value = self.value - def api_info(self) -> dict[str, tuple[str, str]]: - description = f"numeric value between {self.minimum} and {self.maximum}" + def api_info(self) -> dict[str, dict | bool]: return { - "raw_input": ("int | float", description), - "raw_output": ("int | float", description), - "serialized_input": ("int | float", description), - "serialized_output": ("int | float", description), + "info": { + "type": "number", + "description": f"numeric value between {self.minimum} and {self.maximum}", + }, + "serialized_info": False, } def example_inputs(self) -> dict[str, Any]: @@ -1483,19 +1483,16 @@ def __init__( self.cleared_value = self.value or ([] if multiselect else "") - def api_info(self) -> dict[str, tuple[str, str]]: + def api_info(self) -> dict[str, dict | bool]: if self.multiselect: - type = "List[str]" - description = f"List of options from: {self.choices}" + type = { + "type": "array", + "items": {"type": "string"}, + "description": f"List of options from: {self.choices}", + } else: - type = "str" - description = f"Option from: {self.choices}" - return { - "raw_input": (type, description), - "raw_output": (type, description), - "serialized_input": (type, description), - "serialized_output": (type, description), - } + type = {"type": "string", "description": f"Option from: {self.choices}"} + return {"info": type, "serialized_info": False} def example_inputs(self) -> dict[str, Any]: if self.multiselect: @@ -2788,6 +2785,18 @@ def as_example(self, input_data: str | list | None) -> str: else: return Path(input_data).name + def api_info(self) -> dict[str, dict | bool]: + if self.file_count == "single": + return self._single_file_api_info() + else: + return self._multiple_file_api_info() + + def serialized_info(self): + if self.file_count == "single": + return self._single_file_serialized_info() + else: + return self._multiple_file_serialized_info() + @document("style") class Dataframe(Changeable, Selectable, IOComponent, JSONSerializable): diff --git a/gradio/test_data/blocks_configs.py b/gradio/test_data/blocks_configs.py index 1cdf72b1fbaea..c468d0dd7ac6c 100644 --- a/gradio/test_data/blocks_configs.py +++ b/gradio/test_data/blocks_configs.py @@ -13,13 +13,8 @@ "visible": True, "style": {}, }, - "serializer": "Serializable", - "api_info": { - "raw_input": ["str", "string value"], - "raw_output": ["str", "string value"], - "serialized_input": ["str", "string value"], - "serialized_output": ["str", "string value"], - }, + "serializer": "StringSerializable", + "api_info": {"info": {"type": "string"}, "serialized_info": False}, "example_inputs": {"raw": "Howdy!", "serialized": "Howdy!"}, }, { @@ -34,12 +29,10 @@ "visible": True, "style": {}, }, - "serializer": "Serializable", + "serializer": "ListStringSerializable", "api_info": { - "raw_input": ["List[str]", "list of string values"], - "raw_output": ["List[str]", "list of string values"], - "serialized_input": ["List[str]", "list of string values"], - "serialized_output": ["List[str]", "list of string values"], + "info": {"type": "array", "items": {"type": "string"}}, + "serialized_info": False, }, "example_inputs": {"raw": "Covid", "serialized": "Covid"}, }, @@ -76,10 +69,11 @@ }, "serializer": "ImgSerializable", "api_info": { - "raw_input": ["str", "base64 representation of image"], - "raw_output": ["str", "base64 representation of image"], - "serialized_input": ["str", "filepath or URL to image"], - "serialized_output": ["str", "filepath or URL to image"], + "info": { + "type": "string", + "description": "base64 representation of an image", + }, + "serialized_info": True, }, "example_inputs": { "raw": "data:image/png;base64,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", @@ -92,13 +86,8 @@ "props": {"show_label": True, "name": "json", "visible": True, "style": {}}, "serializer": "JSONSerializable", "api_info": { - "raw_input": [ - "str | Dict | List", - "JSON-serializable object or a string", - ], - "raw_output": ["Dict | List", "dictionary- or list-like object"], - "serialized_input": ["str", "filepath to JSON file"], - "serialized_output": ["str", "filepath to JSON file"], + "info": {"type": {}, "description": "any valid json"}, + "serialized_info": True, }, "example_inputs": {"raw": {"a": 1, "b": 2}, "serialized": None}, }, @@ -113,13 +102,8 @@ "visible": True, "style": {}, }, - "serializer": "Serializable", - "api_info": { - "raw_input": ["str", "string value"], - "raw_output": ["str", "string value"], - "serialized_input": ["str", "string value"], - "serialized_output": ["str", "string value"], - }, + "serializer": "StringSerializable", + "api_info": {"info": {"type": "string"}, "serialized_info": False}, "example_inputs": {"raw": "Howdy!", "serialized": "Howdy!"}, }, { @@ -154,10 +138,11 @@ }, "serializer": "ImgSerializable", "api_info": { - "raw_input": ["str", "base64 representation of image"], - "raw_output": ["str", "base64 representation of image"], - "serialized_input": ["str", "filepath or URL to image"], - "serialized_output": ["str", "filepath or URL to image"], + "info": { + "type": "string", + "description": "base64 representation of an image", + }, + "serialized_info": True, }, "example_inputs": { "raw": "data:image/png;base64,R0lGODlhPQBEAPeoAJosM//AwO/AwHVYZ/z595kzAP/s7P+goOXMv8+fhw/v739/f+8PD98fH/8mJl+fn/9ZWb8/PzWlwv///6wWGbImAPgTEMImIN9gUFCEm/gDALULDN8PAD6atYdCTX9gUNKlj8wZAKUsAOzZz+UMAOsJAP/Z2ccMDA8PD/95eX5NWvsJCOVNQPtfX/8zM8+QePLl38MGBr8JCP+zs9myn/8GBqwpAP/GxgwJCPny78lzYLgjAJ8vAP9fX/+MjMUcAN8zM/9wcM8ZGcATEL+QePdZWf/29uc/P9cmJu9MTDImIN+/r7+/vz8/P8VNQGNugV8AAF9fX8swMNgTAFlDOICAgPNSUnNWSMQ5MBAQEJE3QPIGAM9AQMqGcG9vb6MhJsEdGM8vLx8fH98AANIWAMuQeL8fABkTEPPQ0OM5OSYdGFl5jo+Pj/+pqcsTE78wMFNGQLYmID4dGPvd3UBAQJmTkP+8vH9QUK+vr8ZWSHpzcJMmILdwcLOGcHRQUHxwcK9PT9DQ0O/v70w5MLypoG8wKOuwsP/g4P/Q0IcwKEswKMl8aJ9fX2xjdOtGRs/Pz+Dg4GImIP8gIH0sKEAwKKmTiKZ8aB/f39Wsl+LFt8dgUE9PT5x5aHBwcP+AgP+WltdgYMyZfyywz78AAAAAAAD///8AAP9mZv///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAKgALAAAAAA9AEQAAAj/AFEJHEiwoMGDCBMqXMiwocAbBww4nEhxoYkUpzJGrMixogkfGUNqlNixJEIDB0SqHGmyJSojM1bKZOmyop0gM3Oe2liTISKMOoPy7GnwY9CjIYcSRYm0aVKSLmE6nfq05QycVLPuhDrxBlCtYJUqNAq2bNWEBj6ZXRuyxZyDRtqwnXvkhACDV+euTeJm1Ki7A73qNWtFiF+/gA95Gly2CJLDhwEHMOUAAuOpLYDEgBxZ4GRTlC1fDnpkM+fOqD6DDj1aZpITp0dtGCDhr+fVuCu3zlg49ijaokTZTo27uG7Gjn2P+hI8+PDPERoUB318bWbfAJ5sUNFcuGRTYUqV/3ogfXp1rWlMc6awJjiAAd2fm4ogXjz56aypOoIde4OE5u/F9x199dlXnnGiHZWEYbGpsAEA3QXYnHwEFliKAgswgJ8LPeiUXGwedCAKABACCN+EA1pYIIYaFlcDhytd51sGAJbo3onOpajiihlO92KHGaUXGwWjUBChjSPiWJuOO/LYIm4v1tXfE6J4gCSJEZ7YgRYUNrkji9P55sF/ogxw5ZkSqIDaZBV6aSGYq/lGZplndkckZ98xoICbTcIJGQAZcNmdmUc210hs35nCyJ58fgmIKX5RQGOZowxaZwYA+JaoKQwswGijBV4C6SiTUmpphMspJx9unX4KaimjDv9aaXOEBteBqmuuxgEHoLX6Kqx+yXqqBANsgCtit4FWQAEkrNbpq7HSOmtwag5w57GrmlJBASEU18ADjUYb3ADTinIttsgSB1oJFfA63bduimuqKB1keqwUhoCSK374wbujvOSu4QG6UvxBRydcpKsav++Ca6G8A6Pr1x2kVMyHwsVxUALDq/krnrhPSOzXG1lUTIoffqGR7Goi2MAxbv6O2kEG56I7CSlRsEFKFVyovDJoIRTg7sugNRDGqCJzJgcKE0ywc0ELm6KBCCJo8DIPFeCWNGcyqNFE06ToAfV0HBRgxsvLThHn1oddQMrXj5DyAQgjEHSAJMWZwS3HPxT/QMbabI/iBCliMLEJKX2EEkomBAUCxRi42VDADxyTYDVogV+wSChqmKxEKCDAYFDFj4OmwbY7bDGdBhtrnTQYOigeChUmc1K3QTnAUfEgGFgAWt88hKA6aCRIXhxnQ1yg3BCayK44EWdkUQcBByEQChFXfCB776aQsG0BIlQgQgE8qO26X1h8cEUep8ngRBnOy74E9QgRgEAC8SvOfQkh7FDBDmS43PmGoIiKUUEGkMEC/PJHgxw0xH74yx/3XnaYRJgMB8obxQW6kL9QYEJ0FIFgByfIL7/IQAlvQwEpnAC7DtLNJCKUoO/w45c44GwCXiAFB/OXAATQryUxdN4LfFiwgjCNYg+kYMIEFkCKDs6PKAIJouyGWMS1FSKJOMRB/BoIxYJIUXFUxNwoIkEKPAgCBZSQHQ1A2EWDfDEUVLyADj5AChSIQW6gu10bE/JG2VnCZGfo4R4d0sdQoBAHhPjhIB94v/wRoRKQWGRHgrhGSQJxCS+0pCZbEhAAOw==", @@ -170,13 +155,8 @@ "props": {"show_label": True, "name": "json", "visible": True, "style": {}}, "serializer": "JSONSerializable", "api_info": { - "raw_input": [ - "str | Dict | List", - "JSON-serializable object or a string", - ], - "raw_output": ["Dict | List", "dictionary- or list-like object"], - "serialized_input": ["str", "filepath to JSON file"], - "serialized_output": ["str", "filepath to JSON file"], + "info": {"type": {}, "description": "any valid json"}, + "serialized_info": True, }, "example_inputs": {"raw": {"a": 1, "b": 2}, "serialized": None}, }, @@ -191,13 +171,8 @@ "visible": True, "style": {}, }, - "serializer": "Serializable", - "api_info": { - "raw_input": ["str", "string value"], - "raw_output": ["str", "string value"], - "serialized_input": ["str", "string value"], - "serialized_output": ["str", "string value"], - }, + "serializer": "StringSerializable", + "api_info": {"info": {"type": "string"}, "serialized_info": False}, "example_inputs": {"raw": "Howdy!", "serialized": "Howdy!"}, }, { @@ -213,13 +188,8 @@ "visible": True, "style": {}, }, - "serializer": "Serializable", - "api_info": { - "raw_input": ["str", "string value"], - "raw_output": ["str", "string value"], - "serialized_input": ["str", "string value"], - "serialized_output": ["str", "string value"], - }, + "serializer": "StringSerializable", + "api_info": {"info": {"type": "string"}, "serialized_info": False}, "example_inputs": {"raw": "Howdy!", "serialized": "Howdy!"}, }, { @@ -353,13 +323,8 @@ "visible": True, "style": {}, }, - "serializer": "Serializable", - "api_info": { - "raw_input": ["str", "string value"], - "raw_output": ["str", "string value"], - "serialized_input": ["str", "string value"], - "serialized_output": ["str", "string value"], - }, + "serializer": "StringSerializable", + "api_info": {"info": {"type": "string"}, "serialized_info": False}, "example_inputs": {"raw": "Howdy!", "serialized": "Howdy!"}, }, { @@ -374,12 +339,10 @@ "visible": True, "style": {}, }, - "serializer": "Serializable", + "serializer": "ListStringSerializable", "api_info": { - "raw_input": ["List[str]", "list of string values"], - "raw_output": ["List[str]", "list of string values"], - "serialized_input": ["List[str]", "list of string values"], - "serialized_output": ["List[str]", "list of string values"], + "info": {"type": "array", "items": {"type": "string"}}, + "serialized_info": False, }, "example_inputs": {"raw": "Covid", "serialized": "Covid"}, }, @@ -416,10 +379,11 @@ }, "serializer": "ImgSerializable", "api_info": { - "raw_input": ["str", "base64 representation of image"], - "raw_output": ["str", "base64 representation of image"], - "serialized_input": ["str", "filepath or URL to image"], - "serialized_output": ["str", "filepath or URL to image"], + "info": { + "type": "string", + "description": "base64 representation of an image", + }, + "serialized_info": True, }, "example_inputs": { "raw": "data:image/png;base64,R0lGODlhPQBEAPeoAJosM//AwO/AwHVYZ/z595kzAP/s7P+goOXMv8+fhw/v739/f+8PD98fH/8mJl+fn/9ZWb8/PzWlwv///6wWGbImAPgTEMImIN9gUFCEm/gDALULDN8PAD6atYdCTX9gUNKlj8wZAKUsAOzZz+UMAOsJAP/Z2ccMDA8PD/95eX5NWvsJCOVNQPtfX/8zM8+QePLl38MGBr8JCP+zs9myn/8GBqwpAP/GxgwJCPny78lzYLgjAJ8vAP9fX/+MjMUcAN8zM/9wcM8ZGcATEL+QePdZWf/29uc/P9cmJu9MTDImIN+/r7+/vz8/P8VNQGNugV8AAF9fX8swMNgTAFlDOICAgPNSUnNWSMQ5MBAQEJE3QPIGAM9AQMqGcG9vb6MhJsEdGM8vLx8fH98AANIWAMuQeL8fABkTEPPQ0OM5OSYdGFl5jo+Pj/+pqcsTE78wMFNGQLYmID4dGPvd3UBAQJmTkP+8vH9QUK+vr8ZWSHpzcJMmILdwcLOGcHRQUHxwcK9PT9DQ0O/v70w5MLypoG8wKOuwsP/g4P/Q0IcwKEswKMl8aJ9fX2xjdOtGRs/Pz+Dg4GImIP8gIH0sKEAwKKmTiKZ8aB/f39Wsl+LFt8dgUE9PT5x5aHBwcP+AgP+WltdgYMyZfyywz78AAAAAAAD///8AAP9mZv///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAKgALAAAAAA9AEQAAAj/AFEJHEiwoMGDCBMqXMiwocAbBww4nEhxoYkUpzJGrMixogkfGUNqlNixJEIDB0SqHGmyJSojM1bKZOmyop0gM3Oe2liTISKMOoPy7GnwY9CjIYcSRYm0aVKSLmE6nfq05QycVLPuhDrxBlCtYJUqNAq2bNWEBj6ZXRuyxZyDRtqwnXvkhACDV+euTeJm1Ki7A73qNWtFiF+/gA95Gly2CJLDhwEHMOUAAuOpLYDEgBxZ4GRTlC1fDnpkM+fOqD6DDj1aZpITp0dtGCDhr+fVuCu3zlg49ijaokTZTo27uG7Gjn2P+hI8+PDPERoUB318bWbfAJ5sUNFcuGRTYUqV/3ogfXp1rWlMc6awJjiAAd2fm4ogXjz56aypOoIde4OE5u/F9x199dlXnnGiHZWEYbGpsAEA3QXYnHwEFliKAgswgJ8LPeiUXGwedCAKABACCN+EA1pYIIYaFlcDhytd51sGAJbo3onOpajiihlO92KHGaUXGwWjUBChjSPiWJuOO/LYIm4v1tXfE6J4gCSJEZ7YgRYUNrkji9P55sF/ogxw5ZkSqIDaZBV6aSGYq/lGZplndkckZ98xoICbTcIJGQAZcNmdmUc210hs35nCyJ58fgmIKX5RQGOZowxaZwYA+JaoKQwswGijBV4C6SiTUmpphMspJx9unX4KaimjDv9aaXOEBteBqmuuxgEHoLX6Kqx+yXqqBANsgCtit4FWQAEkrNbpq7HSOmtwag5w57GrmlJBASEU18ADjUYb3ADTinIttsgSB1oJFfA63bduimuqKB1keqwUhoCSK374wbujvOSu4QG6UvxBRydcpKsav++Ca6G8A6Pr1x2kVMyHwsVxUALDq/krnrhPSOzXG1lUTIoffqGR7Goi2MAxbv6O2kEG56I7CSlRsEFKFVyovDJoIRTg7sugNRDGqCJzJgcKE0ywc0ELm6KBCCJo8DIPFeCWNGcyqNFE06ToAfV0HBRgxsvLThHn1oddQMrXj5DyAQgjEHSAJMWZwS3HPxT/QMbabI/iBCliMLEJKX2EEkomBAUCxRi42VDADxyTYDVogV+wSChqmKxEKCDAYFDFj4OmwbY7bDGdBhtrnTQYOigeChUmc1K3QTnAUfEgGFgAWt88hKA6aCRIXhxnQ1yg3BCayK44EWdkUQcBByEQChFXfCB776aQsG0BIlQgQgE8qO26X1h8cEUep8ngRBnOy74E9QgRgEAC8SvOfQkh7FDBDmS43PmGoIiKUUEGkMEC/PJHgxw0xH74yx/3XnaYRJgMB8obxQW6kL9QYEJ0FIFgByfIL7/IQAlvQwEpnAC7DtLNJCKUoO/w45c44GwCXiAFB/OXAATQryUxdN4LfFiwgjCNYg+kYMIEFkCKDs6PKAIJouyGWMS1FSKJOMRB/BoIxYJIUXFUxNwoIkEKPAgCBZSQHQ1A2EWDfDEUVLyADj5AChSIQW6gu10bE/JG2VnCZGfo4R4d0sdQoBAHhPjhIB94v/wRoRKQWGRHgrhGSQJxCS+0pCZbEhAAOw==", @@ -432,13 +396,8 @@ "props": {"show_label": True, "name": "json", "visible": True, "style": {}}, "serializer": "JSONSerializable", "api_info": { - "raw_input": [ - "str | Dict | List", - "JSON-serializable object or a string", - ], - "raw_output": ["Dict | List", "dictionary- or list-like object"], - "serialized_input": ["str", "filepath to JSON file"], - "serialized_output": ["str", "filepath to JSON file"], + "info": {"type": {}, "description": "any valid json"}, + "serialized_info": True, }, "example_inputs": {"raw": {"a": 1, "b": 2}, "serialized": None}, }, @@ -453,13 +412,8 @@ "visible": True, "style": {}, }, - "serializer": "Serializable", - "api_info": { - "raw_input": ["str", "string value"], - "raw_output": ["str", "string value"], - "serialized_input": ["str", "string value"], - "serialized_output": ["str", "string value"], - }, + "serializer": "StringSerializable", + "api_info": {"info": {"type": "string"}, "serialized_info": False}, "example_inputs": {"raw": "Howdy!", "serialized": "Howdy!"}, }, { @@ -494,10 +448,11 @@ }, "serializer": "ImgSerializable", "api_info": { - "raw_input": ["str", "base64 representation of image"], - "raw_output": ["str", "base64 representation of image"], - "serialized_input": ["str", "filepath or URL to image"], - "serialized_output": ["str", "filepath or URL to image"], + "info": { + "type": "string", + "description": "base64 representation of an image", + }, + "serialized_info": True, }, "example_inputs": { "raw": "data:image/png;base64,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", @@ -510,13 +465,8 @@ "props": {"show_label": True, "name": "json", "visible": True, "style": {}}, "serializer": "JSONSerializable", "api_info": { - "raw_input": [ - "str | Dict | List", - "JSON-serializable object or a string", - ], - "raw_output": ["Dict | List", "dictionary- or list-like object"], - "serialized_input": ["str", "filepath to JSON file"], - "serialized_output": ["str", "filepath to JSON file"], + "info": {"type": {}, "description": "any valid json"}, + "serialized_info": True, }, "example_inputs": {"raw": {"a": 1, "b": 2}, "serialized": None}, }, @@ -531,13 +481,8 @@ "visible": True, "style": {}, }, - "serializer": "Serializable", - "api_info": { - "raw_input": ["str", "string value"], - "raw_output": ["str", "string value"], - "serialized_input": ["str", "string value"], - "serialized_output": ["str", "string value"], - }, + "serializer": "StringSerializable", + "api_info": {"info": {"type": "string"}, "serialized_info": False}, "example_inputs": {"raw": "Howdy!", "serialized": "Howdy!"}, }, { @@ -553,13 +498,8 @@ "visible": True, "style": {}, }, - "serializer": "Serializable", - "api_info": { - "raw_input": ["str", "string value"], - "raw_output": ["str", "string value"], - "serialized_input": ["str", "string value"], - "serialized_output": ["str", "string value"], - }, + "serializer": "StringSerializable", + "api_info": {"info": {"type": "string"}, "serialized_info": False}, "example_inputs": {"raw": "Howdy!", "serialized": "Howdy!"}, }, { diff --git a/gradio/utils.py b/gradio/utils.py index 9a3a9a3fe924d..cbaf049257783 100644 --- a/gradio/utils.py +++ b/gradio/utils.py @@ -36,6 +36,7 @@ import httpx import matplotlib import requests +from gradio_client.serializing import Serializable from markdown_it import MarkdownIt from mdit_py_plugins.dollarmath.index import dollarmath_plugin from mdit_py_plugins.footnote.index import footnote_plugin @@ -990,6 +991,9 @@ def get_class_that_defined_method(meth: Callable): and getattr(meth.__self__, "__class__", None) ): for cls in inspect.getmro(meth.__self__.__class__): + # Find the first serializer defined in gradio_client that + if issubclass(cls, Serializable) and "gradio_client" in cls.__module__: + return cls if meth.__name__ in cls.__dict__: return cls meth = getattr(meth, "__func__", meth) # fallback to __qualname__ parsing diff --git a/requirements.txt b/requirements.txt index 4f1439c402d22..0788c693f7243 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ aiohttp altair>=4.2.0 fastapi ffmpy -gradio_client>=0.1.3 +gradio_client>=0.2.0 httpx huggingface_hub>=0.13.0 Jinja2 diff --git a/test/test_components.py b/test/test_components.py index a1b472e482032..bea6d2fbcc7e9 100644 --- a/test/test_components.py +++ b/test/test_components.py @@ -955,7 +955,7 @@ def test_component_functions(self): x_file["is_example"] = True assert file_input.preprocess(x_file) is not None - zero_size_file = {"name": "document.txt", "size": 0, "data": "data:"} + zero_size_file = {"name": "document.txt", "size": 0, "data": ""} temp_file = file_input.preprocess(zero_size_file) assert os.stat(temp_file.name).st_size == 0