Skip to content

Commit

Permalink
Fix updating interactive prop (#6266)
Browse files Browse the repository at this point in the history
* interactive

* added functional test

* notebook

* format

* add changeset

* fix

* simplify backend

* simplify backend

* simplify backend

* fix tests

* add changeset

---------

Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
  • Loading branch information
abidlabs and gradio-pr-bot committed Nov 2, 2023
1 parent 8bbeca0 commit e32bac8
Show file tree
Hide file tree
Showing 8 changed files with 51 additions and 35 deletions.
6 changes: 6 additions & 0 deletions .changeset/better-parents-bow.md
@@ -0,0 +1,6 @@
---
"@gradio/app": patch
"gradio": patch
---

fix:Fix updating interactive prop
2 changes: 1 addition & 1 deletion demo/blocks_essay/run.ipynb
@@ -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", "\n", "def change_textbox(choice):\n", " if choice == \"short\":\n", " return gr.Textbox(lines=2, visible=True)\n", " elif choice == \"long\":\n", " return gr.Textbox(lines=8, visible=True, value=\"Lorem ipsum dolor sit amet\")\n", " else:\n", " return gr.Textbox(visible=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", " radio.change(fn=change_textbox, inputs=radio, outputs=text)\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", "\n", " def reset_bounds(minimum, maximum):\n", " return gr.Number(minimum=minimum, maximum=maximum)\n", "\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", "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", "\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", " 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}
10 changes: 6 additions & 4 deletions demo/blocks_essay/run.py
Expand Up @@ -3,29 +3,30 @@

def change_textbox(choice):
if choice == "short":
return gr.Textbox(lines=2, visible=True)
return gr.Textbox(lines=2, visible=True), gr.Button(interactive=True)
elif choice == "long":
return gr.Textbox(lines=8, visible=True, value="Lorem ipsum dolor sit amet")
return gr.Textbox(lines=8, visible=True, value="Lorem ipsum dolor sit amet"), gr.Button(interactive=True)
else:
return gr.Textbox(visible=False)
return gr.Textbox(visible=False), gr.Button(interactive=False)


with gr.Blocks() as demo:
radio = gr.Radio(
["short", "long", "none"], label="What kind of essay would you like to write?"
)
text = gr.Textbox(lines=2, interactive=True, show_copy_button=True)
radio.change(fn=change_textbox, inputs=radio, outputs=text)

with gr.Row():
num = gr.Number(minimum=0, maximum=100, label="input")
out = gr.Number(label="output")
minimum_slider = gr.Slider(0, 100, 0, label="min")
maximum_slider = gr.Slider(0, 100, 100, label="max")
submit_btn = gr.Button("Submit", variant="primary")

def reset_bounds(minimum, maximum):
return gr.Number(minimum=minimum, maximum=maximum)

radio.change(fn=change_textbox, inputs=radio, outputs=[text, submit_btn])
gr.on(
[minimum_slider.change, maximum_slider.change],
reset_bounds,
Expand All @@ -35,5 +36,6 @@ def reset_bounds(minimum, maximum):
num.submit(lambda x: x, num, out)



if __name__ == "__main__":
demo.launch()
33 changes: 10 additions & 23 deletions gradio/blocks.py
Expand Up @@ -374,36 +374,23 @@ def postprocess_update_dict(
block: Component | BlockContext, update_dict: dict, postprocess: bool = True
):
"""
Converts a dictionary of updates into a format that can be sent to the frontend.
E.g. {"__type__": "update", "value": "2", "interactive": False}
Into -> {"__type__": "update", "value": 2.0, "mode": "static"}
Converts a dictionary of updates into a format that can be sent to the frontend to update the component.
E.g. {"value": "2", "visible": True, "invalid_arg": "hello"}
Into -> {"__type__": "update", "value": 2.0, "visible": True}
Parameters:
block: The Block that is being updated with this update dictionary.
update_dict: The original update dictionary
postprocess: Whether to postprocess the "value" key of the update dictionary.
"""
update_dict = {k: update_dict[k] for k in update_dict if hasattr(block, k)}
if update_dict.get("value") is components._Keywords.NO_VALUE:
update_dict.pop("value")
interactive = update_dict.pop("interactive", None)
if interactive is not None:
update_dict["mode"] = "dynamic" if interactive else "static"
attr_dict = {
k: getattr(block, k) if hasattr(block, k) else v for k, v in update_dict.items()
}
attr_dict["__type__"] = "update"
attr_dict.pop("value", None)
if "value" in update_dict:
if not isinstance(block, components.Component):
raise InvalidComponentError(
f"Component {block.__class__} does not support value"
)
if postprocess:
attr_dict["value"] = block.postprocess(update_dict["value"])
if isinstance(attr_dict["value"], (GradioModel, GradioRootModel)):
attr_dict["value"] = attr_dict["value"].model_dump()
else:
attr_dict["value"] = update_dict["value"]
return attr_dict
elif "value" in update_dict and postprocess:
update_dict["value"] = block.postprocess(update_dict["value"])
if isinstance(update_dict["value"], (GradioModel, GradioRootModel)):
update_dict["value"] = update_dict["value"].model_dump()
update_dict["__type__"] = "update"
return update_dict


def convert_component_dict_to_list(
Expand Down
17 changes: 13 additions & 4 deletions js/app/src/Blocks.svelte
Expand Up @@ -283,7 +283,11 @@
});
}
async function handle_update(data: any, fn_index: number): Promise<void> {
async function handle_update(
data: any,
fn_index: number,
outputs_set_to_non_interactive: number[]
): Promise<void> {
const outputs = dependencies[fn_index].outputs;
data.forEach((value: any, i: number) => {
Expand All @@ -305,6 +309,9 @@
continue;
} else {
output.props[update_key] = update_value;
if (update_key == "interactive" && !update_value) {
outputs_set_to_non_interactive.push(outputs[i]);
}
}
}
} else {
Expand Down Expand Up @@ -405,7 +412,7 @@
payload.data = v;
make_prediction(payload);
} else {
handle_update(v, dep_index);
handle_update(v, dep_index, []);
}
});
} else {
Expand All @@ -426,6 +433,7 @@
function make_prediction(payload: Payload): void {
const pending_outputs: number[] = [];
let outputs_set_to_non_interactive: number[] = [];
const submission = app
.submit(payload.fn_index, payload.data as unknown[], payload.event_data)
.on("data", ({ data, fn_index }) => {
Expand All @@ -434,7 +442,7 @@
make_prediction(dep.final_event);
}
dep.pending_request = false;
handle_update(data, fn_index);
handle_update(data, fn_index, outputs_set_to_non_interactive);
})
.on("status", ({ fn_index, ...status }) => {
tick().then(() => {
Expand All @@ -448,7 +456,8 @@
instance_map[id].props.interactive = false;
} else if (
status.stage === "complete" &&
pending_outputs.includes(id)
pending_outputs.includes(id) &&
!outputs_set_to_non_interactive.includes(id)
) {
instance_map[id].props.interactive = true;
}
Expand Down
12 changes: 12 additions & 0 deletions js/app/test/blocks_essay.spec.ts
Expand Up @@ -20,6 +20,18 @@ test("updates frontend correctly", async ({ page }) => {
await expect(textbox).toBeHidden();
});

test("updates interactivity correctly", async ({ page }) => {
const short_btn = await page.getByLabel("short");
const hidden_btn = await page.getByLabel("none");
const submit_tn = await page.locator("button.primary").first();

await hidden_btn.check();
await expect(submit_tn).toHaveAttribute("disabled");

await short_btn.check();
await expect(submit_tn).not.toHaveAttribute("disabled");
});

test("updates backend correctly", async ({ page }) => {
const min_slider = await page.getByLabel("number input for min");
const num = await page.getByLabel("input").first();
Expand Down
4 changes: 2 additions & 2 deletions test/test_blocks.py
Expand Up @@ -624,9 +624,9 @@ def generic_update():
output = await demo.process_api(fn_index, [], state=None)
assert output["data"][0] == {
"__type__": "update",
"mode": "dynamic",
"interactive": True,
}
assert output["data"][1] == {"__type__": "update", "mode": "dynamic"}
assert output["data"][1] == {"__type__": "update", "interactive": True}

def test_error_raised_if_num_outputs_mismatch(self):
with gr.Blocks() as demo:
Expand Down
2 changes: 1 addition & 1 deletion test/test_helpers.py
Expand Up @@ -294,7 +294,7 @@ def test_caching_with_dict(self):
)
prediction = io.examples_handler.load_from_cache(0)
assert prediction == [
{"lines": 4, "__type__": "update", "mode": "static"},
{"lines": 4, "__type__": "update", "interactive": False},
gr.Label.data_model(**{"label": "lion", "confidences": None}),
]

Expand Down

0 comments on commit e32bac8

Please sign in to comment.