Skip to content

Commit

Permalink
Prevent invalid values from being submitted to dropdown, etc. (#8810)
Browse files Browse the repository at this point in the history
* prevent invalid values

* error

* add changeset

* component

* add tests

* fix tests

* spec ts

* format

---------

Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
  • Loading branch information
abidlabs and gradio-pr-bot authored Jul 19, 2024
1 parent 21f1b89 commit 4cf8af9
Show file tree
Hide file tree
Showing 11 changed files with 143 additions and 95 deletions.
5 changes: 5 additions & 0 deletions .changeset/dark-cougars-fold.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"gradio": minor
---

feat:Prevent invalid values from being submitted to dropdown, etc.
144 changes: 72 additions & 72 deletions .changeset/pre.json
Original file line number Diff line number Diff line change
@@ -1,74 +1,74 @@
{
"mode": "pre",
"tag": "beta",
"initialVersions": {
"@gradio/client": "1.3.0",
"gradio_client": "1.1.0",
"gradio": "4.38.1",
"@gradio/cdn-test": "0.0.1",
"@gradio/spaces-test": "0.0.1",
"website": "0.34.0",
"@gradio/accordion": "0.3.18",
"@gradio/annotatedimage": "0.6.13",
"@gradio/app": "1.38.1",
"@gradio/atoms": "0.7.6",
"@gradio/audio": "0.12.2",
"@gradio/box": "0.1.20",
"@gradio/button": "0.2.46",
"@gradio/chatbot": "0.12.1",
"@gradio/checkbox": "0.3.8",
"@gradio/checkboxgroup": "0.5.8",
"@gradio/code": "0.7.0",
"@gradio/colorpicker": "0.3.8",
"@gradio/column": "0.1.2",
"@gradio/dataframe": "0.8.13",
"@gradio/dataset": "0.2.0",
"@gradio/datetime": "0.0.2",
"@gradio/downloadbutton": "0.1.23",
"@gradio/dropdown": "0.7.8",
"@gradio/fallback": "0.3.8",
"@gradio/file": "0.8.5",
"@gradio/fileexplorer": "0.4.14",
"@gradio/form": "0.1.20",
"@gradio/gallery": "0.11.2",
"@gradio/group": "0.1.1",
"@gradio/highlightedtext": "0.7.2",
"@gradio/html": "0.3.1",
"@gradio/icons": "0.6.0",
"@gradio/image": "0.12.2",
"@gradio/imageeditor": "0.7.13",
"@gradio/json": "0.2.8",
"@gradio/label": "0.3.8",
"@gradio/lite": "4.38.1",
"@gradio/markdown": "0.8.1",
"@gradio/model3d": "0.11.0",
"@gradio/multimodaltextbox": "0.5.2",
"@gradio/number": "0.4.8",
"@gradio/paramviewer": "0.4.17",
"@gradio/plot": "0.6.0",
"@gradio/preview": "0.10.1",
"gradio_test": "0.5.0",
"@gradio/radio": "0.5.8",
"@gradio/row": "0.1.3",
"@gradio/simpledropdown": "0.2.8",
"@gradio/simpleimage": "0.6.2",
"@gradio/simpletextbox": "0.2.8",
"@gradio/slider": "0.4.8",
"@gradio/state": "0.1.0",
"@gradio/statustracker": "0.7.1",
"@gradio/storybook": "0.6.0",
"@gradio/tabitem": "0.2.12",
"@gradio/tabs": "0.2.11",
"@gradio/textbox": "0.6.7",
"@gradio/theme": "0.2.3",
"@gradio/timer": "0.3.0",
"@gradio/tooltip": "0.1.0",
"@gradio/tootils": "0.6.0",
"@gradio/upload": "0.11.5",
"@gradio/uploadbutton": "0.6.14",
"@gradio/utils": "0.5.1",
"@gradio/video": "0.9.2",
"@gradio/wasm": "0.11.0"
},
"changesets": []
"mode": "pre",
"tag": "beta",
"initialVersions": {
"@gradio/client": "1.3.0",
"gradio_client": "1.1.0",
"gradio": "4.38.1",
"@gradio/cdn-test": "0.0.1",
"@gradio/spaces-test": "0.0.1",
"website": "0.34.0",
"@gradio/accordion": "0.3.18",
"@gradio/annotatedimage": "0.6.13",
"@gradio/app": "1.38.1",
"@gradio/atoms": "0.7.6",
"@gradio/audio": "0.12.2",
"@gradio/box": "0.1.20",
"@gradio/button": "0.2.46",
"@gradio/chatbot": "0.12.1",
"@gradio/checkbox": "0.3.8",
"@gradio/checkboxgroup": "0.5.8",
"@gradio/code": "0.7.0",
"@gradio/colorpicker": "0.3.8",
"@gradio/column": "0.1.2",
"@gradio/dataframe": "0.8.13",
"@gradio/dataset": "0.2.0",
"@gradio/datetime": "0.0.2",
"@gradio/downloadbutton": "0.1.23",
"@gradio/dropdown": "0.7.8",
"@gradio/fallback": "0.3.8",
"@gradio/file": "0.8.5",
"@gradio/fileexplorer": "0.4.14",
"@gradio/form": "0.1.20",
"@gradio/gallery": "0.11.2",
"@gradio/group": "0.1.1",
"@gradio/highlightedtext": "0.7.2",
"@gradio/html": "0.3.1",
"@gradio/icons": "0.6.0",
"@gradio/image": "0.12.2",
"@gradio/imageeditor": "0.7.13",
"@gradio/json": "0.2.8",
"@gradio/label": "0.3.8",
"@gradio/lite": "4.38.1",
"@gradio/markdown": "0.8.1",
"@gradio/model3d": "0.11.0",
"@gradio/multimodaltextbox": "0.5.2",
"@gradio/number": "0.4.8",
"@gradio/paramviewer": "0.4.17",
"@gradio/plot": "0.6.0",
"@gradio/preview": "0.10.1",
"gradio_test": "0.5.0",
"@gradio/radio": "0.5.8",
"@gradio/row": "0.1.3",
"@gradio/simpledropdown": "0.2.8",
"@gradio/simpleimage": "0.6.2",
"@gradio/simpletextbox": "0.2.8",
"@gradio/slider": "0.4.8",
"@gradio/state": "0.1.0",
"@gradio/statustracker": "0.7.1",
"@gradio/storybook": "0.6.0",
"@gradio/tabitem": "0.2.12",
"@gradio/tabs": "0.2.11",
"@gradio/textbox": "0.6.7",
"@gradio/theme": "0.2.3",
"@gradio/timer": "0.3.0",
"@gradio/tooltip": "0.1.0",
"@gradio/tootils": "0.6.0",
"@gradio/upload": "0.11.5",
"@gradio/uploadbutton": "0.6.14",
"@gradio/utils": "0.5.1",
"@gradio/video": "0.9.2",
"@gradio/wasm": "0.11.0"
},
"changesets": []
}
2 changes: 1 addition & 1 deletion demo/blocks_essay/run.ipynb
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: blocks_essay"]}, {"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", "countries_cities_dict = {\n", " \"USA\": [\"New York\", \"Los Angeles\", \"Chicago\"],\n", " \"Canada\": [\"Toronto\", \"Montreal\", \"Vancouver\"],\n", " \"Pakistan\": [\"Karachi\", \"Lahore\", \"Islamabad\"],\n", "}\n", "\n", "\n", "def change_textbox(choice):\n", " if choice == \"short\":\n", " return gr.Textbox(lines=2, visible=True), gr.Button(interactive=True)\n", " elif choice == \"long\":\n", " return gr.Textbox(lines=8, visible=True, value=\"Lorem ipsum dolor sit amet\"), gr.Button(interactive=True)\n", " else:\n", " return gr.Textbox(visible=False), gr.Button(interactive=False)\n", "\n", "\n", "with gr.Blocks() as demo:\n", " radio = gr.Radio(\n", " [\"short\", \"long\", \"none\"], label=\"What kind of essay would you like to write?\"\n", " )\n", " text = gr.Textbox(lines=2, interactive=True, show_copy_button=True)\n", "\n", " with gr.Row():\n", " num = gr.Number(minimum=0, maximum=100, label=\"input\")\n", " out = gr.Number(label=\"output\")\n", " minimum_slider = gr.Slider(0, 100, 0, label=\"min\")\n", " maximum_slider = gr.Slider(0, 100, 100, label=\"max\")\n", " submit_btn = gr.Button(\"Submit\", variant=\"primary\")\n", "\n", " with gr.Row():\n", " country = gr.Dropdown(list(countries_cities_dict.keys()), label=\"Country\")\n", " cities = gr.Dropdown([], label=\"Cities\")\n", " \n", " @country.change(inputs=country, outputs=cities)\n", " def update_cities(country):\n", " cities = list(countries_cities_dict[country])\n", " return gr.Dropdown(choices=cities, value=cities[0], interactive=True)\n", "\n", " def reset_bounds(minimum, maximum):\n", " return gr.Number(minimum=minimum, maximum=maximum)\n", "\n", " radio.change(fn=change_textbox, inputs=radio, outputs=[text, submit_btn])\n", " gr.on(\n", " [minimum_slider.change, maximum_slider.change],\n", " reset_bounds,\n", " [minimum_slider, maximum_slider],\n", " outputs=num,\n", " )\n", " num.submit(lambda x: x, num, out)\n", "\n", "\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: blocks_essay"]}, {"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", "countries_cities_dict = {\n", " \"USA\": [\"New York\", \"Los Angeles\", \"Chicago\"],\n", " \"Canada\": [\"Toronto\", \"Montreal\", \"Vancouver\"],\n", " \"Pakistan\": [\"Karachi\", \"Lahore\", \"Islamabad\"],\n", "}\n", "\n", "\n", "def change_textbox(choice):\n", " if choice == \"short\":\n", " return gr.Textbox(lines=2, visible=True), gr.Button(interactive=True)\n", " elif choice == \"long\":\n", " return gr.Textbox(lines=8, visible=True, value=\"Lorem ipsum dolor sit amet\"), gr.Button(interactive=True)\n", " else:\n", " return gr.Textbox(visible=False), gr.Button(interactive=False)\n", "\n", "\n", "with gr.Blocks() as demo:\n", " radio = gr.Radio(\n", " [\"short\", \"long\", \"none\"], label=\"What kind of essay would you like to write?\"\n", " )\n", " text = gr.Textbox(lines=2, interactive=True, show_copy_button=True)\n", "\n", " with gr.Row():\n", " num = gr.Number(minimum=0, maximum=100, label=\"input\")\n", " out = gr.Number(label=\"output\")\n", " minimum_slider = gr.Slider(0, 100, 0, label=\"min\")\n", " maximum_slider = gr.Slider(0, 100, 100, label=\"max\")\n", " submit_btn = gr.Button(\"Submit\", variant=\"primary\")\n", "\n", " with gr.Row():\n", " country = gr.Dropdown(list(countries_cities_dict.keys()), label=\"Country\")\n", " cities = gr.Dropdown([], label=\"Cities\")\n", " first_letter = gr.Textbox(label=\"First Letter\")\n", " \n", " @country.change(inputs=country, outputs=cities)\n", " def update_cities(country):\n", " cities = list(countries_cities_dict[country])\n", " return gr.Dropdown(choices=cities, value=cities[0], interactive=True)\n", "\n", " def reset_bounds(minimum, maximum):\n", " return gr.Number(minimum=minimum, maximum=maximum)\n", "\n", " radio.change(fn=change_textbox, inputs=radio, outputs=[text, submit_btn])\n", " gr.on(\n", " [minimum_slider.change, maximum_slider.change],\n", " reset_bounds,\n", " [minimum_slider, maximum_slider],\n", " outputs=num,\n", " )\n", " num.submit(lambda x: x, num, out)\n", "\n", " cities.change(\n", " lambda x: x[0], cities, first_letter\n", " )\n", "\n", "\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
5 changes: 5 additions & 0 deletions demo/blocks_essay/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def change_textbox(choice):
with gr.Row():
country = gr.Dropdown(list(countries_cities_dict.keys()), label="Country")
cities = gr.Dropdown([], label="Cities")
first_letter = gr.Textbox(label="First Letter")

@country.change(inputs=country, outputs=cities)
def update_cities(country):
Expand All @@ -50,6 +51,10 @@ def reset_bounds(minimum, maximum):
)
num.submit(lambda x: x, num, out)

cities.change(
lambda x: x[0], cities, first_letter
)



if __name__ == "__main__":
Expand Down
8 changes: 7 additions & 1 deletion gradio/components/checkboxgroup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from gradio.components.base import Component, FormComponent
from gradio.events import Events
from gradio.exceptions import Error

if TYPE_CHECKING:
from gradio.components import Timer
Expand Down Expand Up @@ -116,10 +117,15 @@ def preprocess(
Returns:
Passes the list of checked checkboxes as a `list[str | int | float]` or their indices as a `list[int]` into the function, depending on `type`.
"""
choice_values = [value for _, value in self.choices]
for value in payload:
if value not in choice_values:
raise Error(
f"Value: {value} is not in the list of choices: {choice_values}"
)
if self.type == "value":
return payload
elif self.type == "index":
choice_values = [value for _, value in self.choices]
return [
choice_values.index(choice) if choice in choice_values else None
for choice in payload
Expand Down
24 changes: 18 additions & 6 deletions gradio/components/dropdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from gradio.components.base import Component, FormComponent
from gradio.events import Events
from gradio.exceptions import Error

if TYPE_CHECKING:
from gradio.components import Timer
Expand Down Expand Up @@ -159,15 +160,26 @@ def preprocess(
Returns:
Passes the value of the selected dropdown choice as a `str | int | float` or its index as an `int` into the function, depending on `type`. Or, if `multiselect` is True, passes the values of the selected dropdown choices as a list of correspoding values/indices instead.
"""
if payload is None:
return None

choice_values = [value for _, value in self.choices]
if not self.allow_custom_value:
if isinstance(payload, list):
for value in payload:
if value not in choice_values:
raise Error(
f"Value: {value} is not in the list of choices: {choice_values}"
)
elif payload not in choice_values:
raise Error(
f"Value: {payload} is not in the list of choices: {choice_values}"
)

if self.type == "value":
return payload
elif self.type == "index":
choice_values = [value for _, value in self.choices]
if payload is None:
return None
elif self.multiselect:
if not isinstance(payload, list):
raise TypeError("Multiselect dropdown payload must be a list")
if isinstance(payload, list):
return [
choice_values.index(choice) if choice in choice_values else None
for choice in payload
Expand Down
18 changes: 11 additions & 7 deletions gradio/components/radio.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from gradio.components.base import Component, FormComponent
from gradio.events import Events
from gradio.exceptions import Error

if TYPE_CHECKING:
from gradio.components import Timer
Expand Down Expand Up @@ -108,16 +109,19 @@ def preprocess(self, payload: str | int | float | None) -> str | int | float | N
Returns:
Passes the value of the selected radio button as a `str | int | float`, or its index as an `int` into the function, depending on `type`.
"""
if payload is None:
return None

choice_values = [value for _, value in self.choices]
if payload not in choice_values:
raise Error(
f"Value: {payload} is not in the list of choices: {choice_values}"
)

if self.type == "value":
return payload
elif self.type == "index":
if payload is None:
return None
else:
choice_values = [value for _, value in self.choices]
return (
choice_values.index(payload) if payload in choice_values else None
)
return choice_values.index(payload)
else:
raise ValueError(
f"Unknown type: {self.type}. Please choose from: 'value', 'index'."
Expand Down
3 changes: 3 additions & 0 deletions js/app/test/blocks_essay.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,15 @@ test("updates backend correctly", async ({ page }) => {
test("updates dropdown choices correctly", async ({ page }) => {
const country = await page.getByLabel("Country").first();
const city = await page.getByLabel("Cities").first();
const first_letter = await page.getByLabel("First Letter").first();

await country.fill("Canada");
await country.press("Enter");
await expect(city).toHaveValue("Toronto");
await expect(first_letter).toHaveValue("T");

await country.fill("Pakistan");
await country.press("Enter");
await expect(city).toHaveValue("Karachi");
await expect(first_letter).toHaveValue("K");
});
5 changes: 4 additions & 1 deletion test/components/test_checkbox_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ def test_component_functions(self):
checkboxes_input = gr.CheckboxGroup(["a", "b", "c"])
assert checkboxes_input.preprocess(["a", "c"]) == ["a", "c"]
assert checkboxes_input.postprocess(["a", "c"]) == ["a", "c"]
with pytest.raises(gr.Error):
checkboxes_input.preprocess(["d"])

checkboxes_input = gr.CheckboxGroup(["a", "b"], type="index")
assert checkboxes_input.preprocess(["a"]) == [0]
assert checkboxes_input.preprocess(["a", "b"]) == [0, 1]
assert checkboxes_input.preprocess(["a", "b", "c"]) == [0, 1, None]
with pytest.raises(gr.Error):
checkboxes_input.preprocess(["a", "b", "c"])

# When a Gradio app is loaded with gr.load, the tuples are converted to lists,
# so we need to test that case as well
Expand Down
21 changes: 15 additions & 6 deletions test/components/test_dropdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,29 @@ def test_component_functions(self):
assert dropdown_input.preprocess("c full") == "c full"
assert dropdown_input.postprocess("c full") == ["c full"]

# When a Gradio app is loaded with gr.load, the tuples are converted to lists,
# so we need to test that case as well
dropdown_input = gr.Dropdown(["a", "b", ["c", "c full"]]) # type: ignore
assert dropdown_input.choices == [("a", "a"), ("b", "b"), ("c", "c full")]
# When an external Gradio app is loaded with gr.load, the tuples are converted to lists,
# so we test that case as well
dropdown = gr.Dropdown(["a", "b", ["c", "c full"]]) # type: ignore
assert dropdown.choices == [("a", "a"), ("b", "b"), ("c", "c full")]

dropdown = gr.Dropdown(choices=["a", "b"], type="index")
assert dropdown.preprocess("a") == 0
assert dropdown.preprocess("b") == 1
assert dropdown.preprocess("c") is None
with pytest.raises(gr.Error):
dropdown.preprocess("c")

dropdown = gr.Dropdown(choices=["a", "b"], type="index", multiselect=True)
assert dropdown.preprocess(["a"]) == [0]
assert dropdown.preprocess(["a", "b"]) == [0, 1]
assert dropdown.preprocess(["a", "b", "c"]) == [0, 1, None]
with pytest.raises(gr.Error):
dropdown.preprocess(["a", "b", "c"])

dropdown = gr.Dropdown(["a", "b"], allow_custom_value=True)
assert dropdown.preprocess("a") == "a"
assert dropdown.preprocess("c") == "c"
dropdown = gr.Dropdown(["a", "b"], allow_custom_value=True, type="index")
assert dropdown.preprocess("a") == 0
assert dropdown.preprocess("c") is None

dropdown_input_multiselect = gr.Dropdown(["a", "b", ("c", "c full")])
assert dropdown_input_multiselect.preprocess(["a", "c full"]) == ["a", "c full"]
Expand Down
3 changes: 2 additions & 1 deletion test/components/test_radio.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ def test_component_functions(self):
radio = gr.Radio(choices=["a", "b"], type="index")
assert radio.preprocess("a") == 0
assert radio.preprocess("b") == 1
assert radio.preprocess("c") is None
with pytest.raises(gr.Error):
radio.preprocess("c")

# When a Gradio app is loaded with gr.load, the tuples are converted to lists,
# so we need to test that case as well
Expand Down

0 comments on commit 4cf8af9

Please sign in to comment.