From 9cefd2e90a1d0cc4d3e4e953fc5b9b1a7afb68dd Mon Sep 17 00:00:00 2001 From: Abubakar Abid Date: Wed, 10 Jan 2024 16:35:25 -0800 Subject: [PATCH] Refactor examples so they accept data in the same format as is returned by function, rename `.as_example()` to `.process_example()` (#6933) * image-editor-examples * add changeset * add changeset * delete changeset * change to process_example() * add changeset * changes for all components until dataset.py * rename * fix checkboxgroup * format * changes * add changeset * changes * add changeset * radio * add changeset * changes * add changeset * changes * examples * remove print * fix * clean * add changeset * fix tests * fix tests * fix test * fix * add changeset * fix video example * add changeset --------- Co-authored-by: gradio-pr-bot --- .changeset/large-olives-unite.md | 11 ++ gradio/_simple_templates/simpledropdown.py | 2 +- gradio/components/audio.py | 10 +- gradio/components/base.py | 26 ++- gradio/components/checkboxgroup.py | 13 -- gradio/components/dataframe.py | 28 ++- gradio/components/dataset.py | 11 +- gradio/components/dropdown.py | 8 - gradio/components/file.py | 2 +- gradio/components/image.py | 6 - gradio/components/image_editor.py | 32 ---- gradio/components/markdown.py | 4 - gradio/components/model3d.py | 2 +- gradio/components/radio.py | 3 - gradio/components/video.py | 5 - gradio/processing_utils.py | 2 +- .../02_key-component-concepts.md | 5 +- guides/05_custom-components/04_backend.md | 8 +- .../06_frequently-asked-questions.md | 2 +- js/checkboxgroup/Example.svelte | 15 +- js/dropdown/Example.svelte | 18 +- js/image/Example.svelte | 5 +- js/imageeditor/Example.svelte | 5 +- js/radio/Example.svelte | 6 +- js/video/Example.svelte | 5 +- pnpm-lock.yaml | 162 ++++++------------ test/test_components.py | 55 +++--- test/test_helpers.py | 2 +- 28 files changed, 208 insertions(+), 245 deletions(-) create mode 100644 .changeset/large-olives-unite.md diff --git a/.changeset/large-olives-unite.md b/.changeset/large-olives-unite.md new file mode 100644 index 000000000000..075b6f0758ff --- /dev/null +++ b/.changeset/large-olives-unite.md @@ -0,0 +1,11 @@ +--- +"@gradio/checkboxgroup": minor +"@gradio/dropdown": minor +"@gradio/image": minor +"@gradio/imageeditor": minor +"@gradio/radio": minor +"@gradio/video": minor +"gradio": minor +--- + +fix:Refactor examples so they accept data in the same format as is returned by function, rename `.as_example()` to `.process_example()` diff --git a/gradio/_simple_templates/simpledropdown.py b/gradio/_simple_templates/simpledropdown.py index 5b5158842219..15d7631db6d7 100644 --- a/gradio/_simple_templates/simpledropdown.py +++ b/gradio/_simple_templates/simpledropdown.py @@ -103,5 +103,5 @@ def postprocess(self, y): self._warn_if_invalid_choice(y) return y - def as_example(self, input_data): + def process_example(self, input_data): return next((c[0] for c in self.choices if c[1] == input_data), None) diff --git a/gradio/components/audio.py b/gradio/components/audio.py index 608145229e63..46b4218b3446 100644 --- a/gradio/components/audio.py +++ b/gradio/components/audio.py @@ -292,8 +292,14 @@ def stream_output( binary_data = binary_data[44:] return binary_data, output_file - def as_example(self, input_data: str | None) -> str: - return Path(input_data).name if input_data else "" + def process_example( + self, value: tuple[int, np.ndarray] | str | Path | bytes | None + ) -> str: + if value is None: + return "" + elif isinstance(value, (str, Path)): + return Path(value).name + return "(audio)" def check_streamable(self): if ( diff --git a/gradio/components/base.py b/gradio/components/base.py index 35ebc55157a7..5966f52589e5 100644 --- a/gradio/components/base.py +++ b/gradio/components/base.py @@ -60,12 +60,12 @@ def postprocess(self, value): return value @abstractmethod - def as_example(self, value): + def process_example(self, value): """ - Return the input data in a way that can be displayed by the examples dataset component in the front-end. + Process the input data in a way that can be displayed by the examples dataset component in the front-end. For example, only return the name of a file as opposed to a full path. Or get the head of a dataframe. - Must be able to be converted to a string to put in the config. + The return value must be able to be json-serializable to put in the config. """ pass @@ -241,9 +241,23 @@ def attach_load_event(self, callable: Callable, every: float | None): """Add a load event that runs `callable`, optionally every `every` seconds.""" self.load_event_to_attach = (callable, every) - def as_example(self, input_data): - """Return the input data in a way that can be displayed by the examples dataset component in the front-end.""" - return input_data + def process_example(self, value): + """ + Process the input data in a way that can be displayed by the examples dataset component in the front-end. + By default, this calls the `.postprocess()` method of the component. However, if the `.postprocess()` method is + computationally intensive, or returns a large payload, a custom implementation may be appropriate. + + For example, the `process_example()` method of the `gr.Audio()` component only returns the name of the file, not + the processed audio file. The `.process_example()` method of the `gr.Dataframe()` returns the head of a dataframe + instead of the full dataframe. + + The return value of this method must be json-serializable to put in the config. + """ + return self.postprocess(value) + + def as_example(self, value): + """Deprecated and replaced by `process_example()`.""" + return self.process_example(value) def api_info(self) -> dict[str, Any]: """ diff --git a/gradio/components/checkboxgroup.py b/gradio/components/checkboxgroup.py index 6762f20a8d85..f46e25160749 100644 --- a/gradio/components/checkboxgroup.py +++ b/gradio/components/checkboxgroup.py @@ -124,16 +124,3 @@ def postprocess( if not isinstance(value, list): value = [value] return value - - def as_example(self, input_data): - if input_data is None: - return None - elif not isinstance(input_data, list): - input_data = [input_data] - for data in input_data: - if data not in [c[0] for c in self.choices]: - raise ValueError(f"Example {data} provided not a valid choice.") - return [ - next((c[0] for c in self.choices if c[1] == data), None) - for data in input_data - ] diff --git a/gradio/components/dataframe.py b/gradio/components/dataframe.py index 11f4a98b9b50..bcdd488959bf 100644 --- a/gradio/components/dataframe.py +++ b/gradio/components/dataframe.py @@ -187,11 +187,13 @@ def postprocess( | dict | str | None, - ) -> DataframeData | dict: + ) -> DataframeData: if value is None: return self.postprocess(self.empty_input) if isinstance(value, dict): - return value + return DataframeData( + headers=value.get("headers", []), data=value.get("data", [[]]) + ) if isinstance(value, (str, pd.DataFrame)): if isinstance(value, str): value = pd.read_csv(value) # type: ignore @@ -289,14 +291,22 @@ def __validate_headers(headers: list[str] | None, col_count: int): f"Check the values passed to `col_count` and `headers`." ) - def as_example(self, input_data: pd.DataFrame | np.ndarray | str | None): - if input_data is None: + def process_example( + self, + value: pd.DataFrame + | Styler + | np.ndarray + | list + | list[list] + | dict + | str + | None, + ): + if value is None: return "" - elif isinstance(input_data, pd.DataFrame): - return input_data.head(n=5).to_dict(orient="split")["data"] # type: ignore - elif isinstance(input_data, np.ndarray): - return input_data.tolist() - return input_data + value_df_data = self.postprocess(value) + value_df = pd.DataFrame(value_df_data.data, columns=value_df_data.headers) + return value_df.head(n=5).to_dict(orient="split")["data"] def example_inputs(self) -> Any: return {"headers": ["a", "b"], "data": [["foo", "bar"]]} diff --git a/gradio/components/dataset.py b/gradio/components/dataset.py index 01a671a0acf2..e778751c12ae 100644 --- a/gradio/components/dataset.py +++ b/gradio/components/dataset.py @@ -6,6 +6,7 @@ from gradio_client.documentation import document, set_documentation_group +from gradio import processing_utils from gradio.components.base import ( Component, get_component_instance, @@ -89,10 +90,16 @@ def __init__( self.samples = [[]] if samples is None else samples for example in self.samples: for i, (component, ex) in enumerate(zip(self._components, example)): + # If proxy_url is set, that means it is being loaded from an external Gradio app + # which means that the example has already been processed. if self.proxy_url is None: - # If proxy_url is set, that means it is being loaded from an external Gradio app - # which means that the example has already been processed. + # The `as_example()` method has been renamed to `process_example()` but we + # use the previous name to be backwards-compatible with previously-created + # custom components example[i] = component.as_example(ex) + example[i] = processing_utils.move_files_to_cache( + example[i], component + ) self.type = type self.label = label if headers is not None: diff --git a/gradio/components/dropdown.py b/gradio/components/dropdown.py index dcf48c2712a1..cba2e7185289 100644 --- a/gradio/components/dropdown.py +++ b/gradio/components/dropdown.py @@ -176,11 +176,3 @@ def postprocess( else: self._warn_if_invalid_choice(value) return value - - def as_example(self, input_data): - if self.multiselect: - return [ - next((c[0] for c in self.choices if c[1] == data), None) - for data in input_data - ] - return next((c[0] for c in self.choices if c[1] == input_data), None) diff --git a/gradio/components/file.py b/gradio/components/file.py index f84ce52688c2..eb0b35cd5a28 100644 --- a/gradio/components/file.py +++ b/gradio/components/file.py @@ -160,7 +160,7 @@ def postprocess(self, value: str | list[str] | None) -> ListFiles | FileData | N size=Path(value).stat().st_size, ) - def as_example(self, input_data: str | list | None) -> str: + def process_example(self, input_data: str | list | None) -> str: if input_data is None: return "" elif isinstance(input_data, list): diff --git a/gradio/components/image.py b/gradio/components/image.py index b8021516eb5b..bd03991ac197 100644 --- a/gradio/components/image.py +++ b/gradio/components/image.py @@ -188,7 +188,6 @@ def postprocess( ) -> FileData | None: if value is None: return None - if isinstance(value, str) and value.lower().endswith(".svg"): return FileData(path=value, orig_name=Path(value).name) saved = image_utils.save_image(value, self.GRADIO_CACHE) @@ -201,10 +200,5 @@ def check_streamable(self): "Image streaming only available if sources is ['webcam']. Streaming not supported with multiple sources." ) - def as_example(self, input_data: str | Path | None) -> str | None: - if input_data is None: - return None - return self.move_resource_to_block_cache(input_data) - def example_inputs(self) -> Any: return "https://raw.githubusercontent.com/gradio-app/gradio/main/test/test_files/bus.png" diff --git a/gradio/components/image_editor.py b/gradio/components/image_editor.py index 60b013db322c..a3bfe6396515 100644 --- a/gradio/components/image_editor.py +++ b/gradio/components/image_editor.py @@ -8,7 +8,6 @@ from typing import Any, Iterable, List, Literal, Optional, TypedDict, Union, cast import numpy as np -from gradio_client import utils as client_utils from gradio_client.documentation import document, set_documentation_group from PIL import Image as _Image # using _ to minimize namespace pollution @@ -310,37 +309,6 @@ def postprocess(self, value: EditorValue | ImageType | None) -> EditorData | Non else None, ) - def as_example( - self, input_data: EditorExampleValue | str | None - ) -> EditorExampleValue | None: - def resolve_path(file_or_url: str | None) -> str | None: - if file_or_url is None: - return None - input_data = str(file_or_url) - # If an externally hosted image or a URL, don't convert to absolute path - if self.proxy_url or client_utils.is_http_url_like(input_data): - return input_data - return self.move_resource_to_block_cache(input_data) - - if input_data is None: - return None - elif isinstance(input_data, str): - input_data = { - "background": input_data, - "layers": [], - "composite": input_data, - } - - input_data["background"] = resolve_path(input_data["background"]) - input_data["layers"] = ( - [resolve_path(f) for f in input_data["layers"]] - if input_data["layers"] - else [] - ) - input_data["composite"] = resolve_path(input_data["composite"]) - - return input_data - def example_inputs(self) -> Any: return { "background": "https://raw.githubusercontent.com/gradio-app/gradio/main/test/test_files/bus.png", diff --git a/gradio/components/markdown.py b/gradio/components/markdown.py index e638afccac4c..cdf1a45da4bf 100644 --- a/gradio/components/markdown.py +++ b/gradio/components/markdown.py @@ -84,10 +84,6 @@ def postprocess(self, value: str | None) -> str | None: unindented_y = inspect.cleandoc(value) return unindented_y - def as_example(self, input_data: str | None) -> str: - postprocessed = self.postprocess(input_data) - return postprocessed if postprocessed else "" - def preprocess(self, payload: str | None) -> str | None: return payload diff --git a/gradio/components/model3d.py b/gradio/components/model3d.py index 02e483724ac4..8d5011312521 100644 --- a/gradio/components/model3d.py +++ b/gradio/components/model3d.py @@ -106,7 +106,7 @@ def postprocess(self, value: str | Path | None) -> FileData | None: return value return FileData(path=str(value), orig_name=Path(value).name) - def as_example(self, input_data: str | None) -> str: + def process_example(self, input_data: str | Path | None) -> str: return Path(input_data).name if input_data else "" def example_inputs(self): diff --git a/gradio/components/radio.py b/gradio/components/radio.py index 3b57b64969fb..5ba9f45d5ff2 100644 --- a/gradio/components/radio.py +++ b/gradio/components/radio.py @@ -125,6 +125,3 @@ def api_info(self) -> dict[str, Any]: "title": "Radio", "type": "string", } - - def as_example(self, input_data): - return next((c[0] for c in self.choices if c[1] == input_data), None) diff --git a/gradio/components/video.py b/gradio/components/video.py index 9335260c122b..6c13fbd85ed0 100644 --- a/gradio/components/video.py +++ b/gradio/components/video.py @@ -338,8 +338,3 @@ def srt_to_vtt(srt_file_path, vtt_file_path): def example_inputs(self) -> Any: return "https://github.com/gradio-app/gradio/raw/main/demo/video_component/files/world.mp4" - - def as_example(self, input_data: str | Path | None) -> str | None: - if input_data is None: - return None - return self.move_resource_to_block_cache(input_data) diff --git a/gradio/processing_utils.py b/gradio/processing_utils.py index 35bdcd19f925..98b4af212bc9 100644 --- a/gradio/processing_utils.py +++ b/gradio/processing_utils.py @@ -237,7 +237,7 @@ def move_resource_to_block_cache( def move_files_to_cache(data: Any, block: Component, postprocess: bool = False): """Move files to cache and replace the file path with the cache path. - Runs after postprocess and before preprocess. + Runs after .postprocess(), after .process_example(), and before .preprocess(). Args: data: The input or output data for a component. Can be a dictionary or a dataclass diff --git a/guides/05_custom-components/02_key-component-concepts.md b/guides/05_custom-components/02_key-component-concepts.md index 271a573f7b43..098b6ea30dcf 100644 --- a/guides/05_custom-components/02_key-component-concepts.md +++ b/guides/05_custom-components/02_key-component-concepts.md @@ -110,10 +110,9 @@ To enable the example view, you must have the following two files in the top of * `Example.svelte`: this corresponds to the "example version" of your component * `Index.svelte`: this corresponds to the "regular version" -In the backend, you typically don't need to do anything unless you would like to modify the user-provided `value` of the examples to something else before it is sent to the frontend. -You can do this in the `as_example` method of the component. +In the backend, you typically don't need to do anything. The user-provided example `value` is processed using the same `.postprocess()` method described earlier. If you'd like to do process the data differently (for example, if the `.postprocess()` method is computationally expensive), then you can write your own `.process_example()` method for your custom component, which will be used instead. -The `Example.svelte` and `as_example` methods will be covered in greater depth in the dedicated [frontend](./frontend) and [backend](./backend) guides. +The `Example.svelte` file and `process_example()` method will be covered in greater depth in the dedicated [frontend](./frontend) and [backend](./backend) guides respectively. ### What you need to remember diff --git a/guides/05_custom-components/04_backend.md b/guides/05_custom-components/04_backend.md index 6f6f5adbdd17..4a61ab869846 100644 --- a/guides/05_custom-components/04_backend.md +++ b/guides/05_custom-components/04_backend.md @@ -55,13 +55,13 @@ They handle the conversion from the data sent by the frontend to the format expe return y ``` -### `as_example` +### `process_example` Takes in the original Python value and returns the modified value that should be displayed in the examples preview in the app. -Let's look at the following example from the `Radio` component. +If not provided, the `.postprocess()` method is used instead. Let's look at the following example from the `SimpleDropdown` component. ```python -def as_example(self, input_data): +def process_example(self, input_data): return next((c[0] for c in self.choices if c[1] == input_data), None) ``` @@ -69,7 +69,7 @@ Since `self.choices` is a list of tuples corresponding to (`display_name`, `valu ```python @abstractmethod -def as_example(self, y): +def process_example(self, y): pass ``` diff --git a/guides/05_custom-components/06_frequently-asked-questions.md b/guides/05_custom-components/06_frequently-asked-questions.md index 7e0a1395bd3d..d7b6c5d43bcf 100644 --- a/guides/05_custom-components/06_frequently-asked-questions.md +++ b/guides/05_custom-components/06_frequently-asked-questions.md @@ -22,7 +22,7 @@ If you would like to share your component with the gradio community, it is recom ## What methods are mandatory for implementing a custom component in Gradio? -You must implement the `preprocess`, `postprocess`, `as_example`, `api_info`, `example_inputs`, `flag`, and `read_from_flag` methods. Read more in the [backend guide](./backend). +You must implement the `preprocess`, `postprocess`, `api_info`, `example_inputs`, `flag`, and `read_from_flag` methods. Read more in the [backend guide](./backend). ## What is the purpose of a `data_model` in Gradio custom components? diff --git a/js/checkboxgroup/Example.svelte b/js/checkboxgroup/Example.svelte index 136a7e0dcbf5..03b00e16396e 100644 --- a/js/checkboxgroup/Example.svelte +++ b/js/checkboxgroup/Example.svelte @@ -2,6 +2,19 @@ export let value: string[]; export let type: "gallery" | "table"; export let selected = false; + export let choices: [string, string | number][]; + + let names = value + .map( + (val) => + ( + choices.find((pair) => pair[1] === val) as + | [string, string | number] + | undefined + )?.[0] + ) + .filter((name) => name !== undefined); + let names_string = names.join(", ");
- {#each value as check, i}{check.toLocaleString()}{#if i !== value.length - 1}, {/if}{/each} + {names_string}