Skip to content

Commit

Permalink
Refactor examples so they accept data in the same format as is return…
Browse files Browse the repository at this point in the history
…ed 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 <gradio-pr-bot@users.noreply.github.com>
  • Loading branch information
abidlabs and gradio-pr-bot committed Jan 11, 2024
1 parent 523b6bc commit 9cefd2e
Show file tree
Hide file tree
Showing 28 changed files with 208 additions and 245 deletions.
11 changes: 11 additions & 0 deletions .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()`
2 changes: 1 addition & 1 deletion gradio/_simple_templates/simpledropdown.py
Expand Up @@ -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)
10 changes: 8 additions & 2 deletions gradio/components/audio.py
Expand Up @@ -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 (
Expand Down
26 changes: 20 additions & 6 deletions gradio/components/base.py
Expand Up @@ -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

Expand Down Expand Up @@ -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]:
"""
Expand Down
13 changes: 0 additions & 13 deletions gradio/components/checkboxgroup.py
Expand Up @@ -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
]
28 changes: 19 additions & 9 deletions gradio/components/dataframe.py
Expand Up @@ -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
Expand Down Expand Up @@ -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"]]}
11 changes: 9 additions & 2 deletions gradio/components/dataset.py
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down
8 changes: 0 additions & 8 deletions gradio/components/dropdown.py
Expand Up @@ -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)
2 changes: 1 addition & 1 deletion gradio/components/file.py
Expand Up @@ -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):
Expand Down
6 changes: 0 additions & 6 deletions gradio/components/image.py
Expand Up @@ -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)
Expand All @@ -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"
32 changes: 0 additions & 32 deletions gradio/components/image_editor.py
Expand Up @@ -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

Expand Down Expand Up @@ -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",
Expand Down
4 changes: 0 additions & 4 deletions gradio/components/markdown.py
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion gradio/components/model3d.py
Expand Up @@ -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):
Expand Down
3 changes: 0 additions & 3 deletions gradio/components/radio.py
Expand Up @@ -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)
5 changes: 0 additions & 5 deletions gradio/components/video.py
Expand Up @@ -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)
2 changes: 1 addition & 1 deletion gradio/processing_utils.py
Expand Up @@ -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
Expand Down
5 changes: 2 additions & 3 deletions guides/05_custom-components/02_key-component-concepts.md
Expand Up @@ -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

Expand Down
8 changes: 4 additions & 4 deletions guides/05_custom-components/04_backend.md
Expand Up @@ -55,21 +55,21 @@ 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)
```

Since `self.choices` is a list of tuples corresponding to (`display_name`, `value`), this converts the value that a user provides to the display value (or if the value is not present in `self.choices`, it is converted to `None`).

```python
@abstractmethod
def as_example(self, y):
def process_example(self, y):
pass
```

Expand Down
Expand Up @@ -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?

Expand Down
15 changes: 14 additions & 1 deletion js/checkboxgroup/Example.svelte
Expand Up @@ -2,14 +2,27 @@
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(", ");
</script>

<div
class:table={type === "table"}
class:gallery={type === "gallery"}
class:selected
>
{#each value as check, i}{check.toLocaleString()}{#if i !== value.length - 1},&nbsp;{/if}{/each}
{names_string}
</div>

<style>
Expand Down

0 comments on commit 9cefd2e

Please sign in to comment.