state.change listener with deep hash check (#8446)

* changes

* changes

* add changeset

* changes

* changes

* changes

* changes

* Update guides/04_building-with-blocks/04_dynamic-apps-with-render-decorator.md

Co-authored-by: Abubakar Abid <abubakar@huggingface.co>

---------

Co-authored-by: Ali Abid <aliabid94@gmail.com>
Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
Co-authored-by: Abubakar Abid <abubakar@huggingface.co>
This commit is contained in:
aliabid94 2024-06-04 14:19:30 -07:00 committed by GitHub
parent 33c8081aa9
commit 4a55157ed9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 191 additions and 36 deletions

View File

@ -0,0 +1,5 @@
---
"gradio": minor
---
feat:state.change listener with deep hash check

View File

@ -1 +1 @@
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: audio_mixer"]}, {"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", "import numpy as np\n", "\n", "with gr.Blocks() as demo:\n", " track_count = gr.State(1)\n", " add_track_btn = gr.Button(\"Add Track\")\n", "\n", " add_track_btn.click(lambda count: count + 1, track_count, track_count)\n", "\n", " @gr.render(inputs=track_count)\n", " def render_tracks(count):\n", " audios = []\n", " volumes = []\n", " with gr.Row():\n", " for i in range(count):\n", " with gr.Column(variant=\"panel\", min_width=200):\n", " gr.Textbox(placeholder=\"Track Name\", key=f\"name-{i}\", show_label=False)\n", " track_audio = gr.Audio(label=f\"Track {i}\", key=f\"track-{i}\")\n", " track_volume = gr.Slider(0, 100, value=100, label=\"Volume\", key=f\"volume-{i}\")\n", " audios.append(track_audio)\n", " volumes.append(track_volume)\n", "\n", " def merge(data):\n", " sr, output = None, None\n", " for audio, volume in zip(audios, volumes):\n", " sr, audio_val = data[audio]\n", " volume_val = data[volume]\n", " final_track = audio_val * (volume_val / 100)\n", " if output is None:\n", " output = final_track\n", " else:\n", " min_shape = tuple(min(s1, s2) for s1, s2 in zip(output.shape, final_track.shape))\n", " trimmed_output = output[:min_shape[0], ...][:, :min_shape[1], ...] if output.ndim > 1 else output[:min_shape[0]]\n", " trimmed_final = final_track[:min_shape[0], ...][:, :final_track[1], ...] if final_track.ndim > 1 else final_track[:min_shape[0]]\n", " output += trimmed_output + trimmed_final\n", " return (sr, output)\n", " \n", " merge_btn.click(merge, set(audios + volumes), output_audio)\n", "\n", " merge_btn = gr.Button(\"Merge Tracks\")\n", " output_audio = gr.Audio(label=\"Output\")\n", " \n", "if __name__ == \"__main__\":\n", " demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: audio_mixer"]}, {"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", "import numpy as np\n", "\n", "with gr.Blocks() as demo:\n", " track_count = gr.State(1)\n", " add_track_btn = gr.Button(\"Add Track\")\n", "\n", " add_track_btn.click(lambda count: count + 1, track_count, track_count)\n", "\n", " @gr.render(inputs=track_count)\n", " def render_tracks(count):\n", " audios = []\n", " volumes = []\n", " with gr.Row():\n", " for i in range(count):\n", " with gr.Column(variant=\"panel\", min_width=200):\n", " gr.Textbox(placeholder=\"Track Name\", key=f\"name-{i}\", show_label=False)\n", " track_audio = gr.Audio(label=f\"Track {i}\", key=f\"track-{i}\")\n", " track_volume = gr.Slider(0, 100, value=100, label=\"Volume\", key=f\"volume-{i}\")\n", " audios.append(track_audio)\n", " volumes.append(track_volume)\n", "\n", " def merge(data):\n", " sr, output = None, None\n", " for audio, volume in zip(audios, volumes):\n", " sr, audio_val = data[audio]\n", " volume_val = data[volume]\n", " final_track = audio_val * (volume_val / 100)\n", " if output is None:\n", " output = final_track\n", " else:\n", " min_shape = tuple(min(s1, s2) for s1, s2 in zip(output.shape, final_track.shape))\n", " trimmed_output = output[:min_shape[0], ...][:, :min_shape[1], ...] if output.ndim > 1 else output[:min_shape[0]]\n", " trimmed_final = final_track[:min_shape[0], ...][:, :final_track[1], ...] if final_track.ndim > 1 else final_track[:min_shape[0]]\n", " output += trimmed_output + trimmed_final\n", " return (sr, output)\n", " \n", " merge_btn.click(merge, set(audios + volumes), output_audio)\n", "\n", " merge_btn = gr.Button(\"Merge Tracks\")\n", " output_audio = gr.Audio(label=\"Output\", interactive=False)\n", " \n", "if __name__ == \"__main__\":\n", " demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}

View File

@ -38,7 +38,7 @@ with gr.Blocks() as demo:
merge_btn.click(merge, set(audios + volumes), output_audio)
merge_btn = gr.Button("Merge Tracks")
output_audio = gr.Audio(label="Output")
output_audio = gr.Audio(label="Output", interactive=False)
if __name__ == "__main__":
demo.launch()

View File

@ -1 +1 @@
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: state_change"]}, {"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", "with gr.Blocks() as demo:\n", "\n", " with gr.Row():\n", " state_a = gr.State(0)\n", " btn_a = gr.Button(\"Increment A\")\n", " value_a = gr.Number(label=\"A\")\n", " btn_a.click(lambda x: x+1, state_a, state_a)\n", " state_a.change(lambda x: x, state_a, value_a)\n", " with gr.Row():\n", " state_b = gr.State(0)\n", " btn_b = gr.Button(\"Increment B\")\n", " value_b = gr.Number(label=\"num\")\n", " btn_b.click(lambda x: x+1, state_b, state_b)\n", "\n", " @gr.on(inputs=state_b, outputs=value_b)\n", " def identity(x):\n", " return x\n", "\n", " @gr.render(inputs=[state_a, state_b])\n", " def render(a, b):\n", " for x in range(a):\n", " with gr.Row():\n", " for y in range(b):\n", " gr.Button(f\"Button {x}, {y}\")\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: state_change"]}, {"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", "with gr.Blocks() as demo:\n", "\n", " with gr.Row():\n", " state_a = gr.State(0)\n", " btn_a = gr.Button(\"Increment A\")\n", " value_a = gr.Number(label=\"A\")\n", " btn_a.click(lambda x: x+1, state_a, state_a)\n", " state_a.change(lambda x: x, state_a, value_a)\n", " with gr.Row():\n", " state_b = gr.State(0)\n", " btn_b = gr.Button(\"Increment B\")\n", " value_b = gr.Number(label=\"num\")\n", " btn_b.click(lambda x: x+1, state_b, state_b)\n", "\n", " @gr.on(inputs=state_b, outputs=value_b)\n", " def identity(x):\n", " return x\n", "\n", " @gr.render(inputs=[state_a, state_b])\n", " def render(a, b):\n", " for x in range(a):\n", " with gr.Row():\n", " for y in range(b):\n", " gr.Button(f\"Button {x}, {y}\")\n", "\n", " list_state = gr.State([])\n", " dict_state = gr.State(dict())\n", " nested_list_state = gr.State([])\n", " set_state = gr.State(set())\n", "\n", " def transform_list(x):\n", " return {n: n for n in x}, [x[:] for _ in range(len(x))], set(x)\n", " \n", " list_state.change(\n", " transform_list,\n", " inputs=list_state,\n", " outputs=[dict_state, nested_list_state, set_state],\n", " )\n", "\n", " all_textbox = gr.Textbox(label=\"Output\")\n", " change_count = gr.Number(label=\"Changes\")\n", " gr.on(\n", " inputs=[change_count, dict_state, nested_list_state, set_state],\n", " triggers=[dict_state.change, nested_list_state.change, set_state.change],\n", " fn=lambda x, *args: (x+1, \"\\n\".join(str(arg) for arg in args)),\n", " outputs=[change_count, all_textbox],\n", " )\n", "\n", " count_to_3_btn = gr.Button(\"Count to 3\")\n", " count_to_3_btn.click(lambda: [1, 2, 3], outputs=list_state)\n", " zero_all_btn = gr.Button(\"Zero All\")\n", " zero_all_btn.click(\n", " lambda x: [0] * len(x), inputs=list_state, outputs=list_state\n", " )\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}

View File

@ -25,5 +25,35 @@ with gr.Blocks() as demo:
for y in range(b):
gr.Button(f"Button {x}, {y}")
list_state = gr.State([])
dict_state = gr.State(dict())
nested_list_state = gr.State([])
set_state = gr.State(set())
def transform_list(x):
return {n: n for n in x}, [x[:] for _ in range(len(x))], set(x)
list_state.change(
transform_list,
inputs=list_state,
outputs=[dict_state, nested_list_state, set_state],
)
all_textbox = gr.Textbox(label="Output")
change_count = gr.Number(label="Changes")
gr.on(
inputs=[change_count, dict_state, nested_list_state, set_state],
triggers=[dict_state.change, nested_list_state.change, set_state.change],
fn=lambda x, *args: (x+1, "\n".join(str(arg) for arg in args)),
outputs=[change_count, all_textbox],
)
count_to_3_btn = gr.Button("Count to 3")
count_to_3_btn.click(lambda: [1, 2, 3], outputs=list_state)
zero_all_btn = gr.Button("Zero All")
zero_all_btn.click(
lambda x: [0] * len(x), inputs=list_state, outputs=list_state
)
if __name__ == "__main__":
demo.launch()

View File

@ -1 +1 @@
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: todo_list"]}, {"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", "with gr.Blocks() as demo:\n", " \n", " tasks = gr.State([])\n", " new_task = gr.Textbox(label=\"Task Name\", autofocus=True)\n", "\n", " def add_task(tasks, new_task_name):\n", " return tasks + [{\"name\": new_task_name, \"complete\": False}], \"\"\n", "\n", " new_task.submit(add_task, [tasks, new_task], [tasks, new_task])\n", "\n", " @gr.render(inputs=tasks)\n", " def render_todos(task_list):\n", " complete = [task for task in task_list if task[\"complete\"]]\n", " incomplete = [task for task in task_list if not task[\"complete\"]]\n", " gr.Markdown(f\"### Incomplete Tasks ({len(incomplete)})\")\n", " for task in incomplete:\n", " with gr.Row():\n", " gr.Textbox(task['name'], show_label=False, container=False)\n", " done_btn = gr.Button(\"Done\", scale=0)\n", " def mark_done(task=task):\n", " _task_list = task_list[:]\n", " _task_list[task_list.index(task)] = {\"name\": task[\"name\"], \"complete\": True}\n", " return _task_list\n", " done_btn.click(mark_done, None, [tasks])\n", "\n", " delete_btn = gr.Button(\"Delete\", scale=0, variant=\"stop\")\n", " def delete(task=task):\n", " task_index = task_list.index(task)\n", " return task_list[:task_index] + task_list[task_index+1:]\n", " delete_btn.click(delete, None, [tasks])\n", "\n", " gr.Markdown(f\"### Complete Tasks ({len(complete)})\")\n", " for task in complete:\n", " gr.Textbox(task['name'], show_label=False, container=False)\n", "\n", "\n", "\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: todo_list"]}, {"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", "with gr.Blocks() as demo:\n", " \n", " tasks = gr.State([])\n", " new_task = gr.Textbox(label=\"Task Name\", autofocus=True)\n", "\n", " def add_task(tasks, new_task_name):\n", " return tasks + [{\"name\": new_task_name, \"complete\": False}], \"\"\n", "\n", " new_task.submit(add_task, [tasks, new_task], [tasks, new_task])\n", "\n", " @gr.render(inputs=tasks)\n", " def render_todos(task_list):\n", " complete = [task for task in task_list if task[\"complete\"]]\n", " incomplete = [task for task in task_list if not task[\"complete\"]]\n", " gr.Markdown(f\"### Incomplete Tasks ({len(incomplete)})\")\n", " for task in incomplete:\n", " with gr.Row():\n", " gr.Textbox(task['name'], show_label=False, container=False)\n", " done_btn = gr.Button(\"Done\", scale=0)\n", " def mark_done(task=task):\n", " task[\"complete\"] = True\n", " return task_list\n", " done_btn.click(mark_done, None, [tasks])\n", "\n", " delete_btn = gr.Button(\"Delete\", scale=0, variant=\"stop\")\n", " def delete(task=task):\n", " task_list.remove(task)\n", " return task_list\n", " delete_btn.click(delete, None, [tasks])\n", "\n", " gr.Markdown(f\"### Complete Tasks ({len(complete)})\")\n", " for task in complete:\n", " gr.Textbox(task['name'], show_label=False, container=False)\n", "\n", "\n", "\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}

View File

@ -20,15 +20,14 @@ with gr.Blocks() as demo:
gr.Textbox(task['name'], show_label=False, container=False)
done_btn = gr.Button("Done", scale=0)
def mark_done(task=task):
_task_list = task_list[:]
_task_list[task_list.index(task)] = {"name": task["name"], "complete": True}
return _task_list
task["complete"] = True
return task_list
done_btn.click(mark_done, None, [tasks])
delete_btn = gr.Button("Delete", scale=0, variant="stop")
def delete(task=task):
task_index = task_list.index(task)
return task_list[:task_index] + task_list[task_index+1:]
task_list.remove(task)
return task_list
delete_btn.click(delete, None, [tasks])
gr.Markdown(f"### Complete Tasks ({len(complete)})")

View File

@ -1718,7 +1718,6 @@ Received outputs:
self.validate_outputs(block_fn, predictions) # type: ignore
output = []
changed_state_ids = []
for i, block in enumerate(block_fn.outputs):
try:
if predictions[i] is components._Keywords.FINISHED_ITERATING:
@ -1732,16 +1731,6 @@ Received outputs:
if block.stateful:
if not utils.is_update(predictions[i]):
has_change_event = False
for dep in state.blocks_config.fns.values():
if block._id in [t[0] for t in dep.targets if t[1] == "change"]:
has_change_event = True
break
if has_change_event and (
block._id not in state
or not utils.deep_equal(state[block._id], predictions[i])
):
changed_state_ids.append(block._id)
state[block._id] = predictions[i]
output.append(None)
else:
@ -1785,7 +1774,7 @@ Received outputs:
)
output.append(outputs_cached)
return output, changed_state_ids
return output
async def handle_streaming_outputs(
self,
@ -1892,6 +1881,8 @@ Received outputs:
if isinstance(block_fn, int):
block_fn = self.fns[block_fn]
batch = block_fn.batch
state_ids_to_track, hashed_values = self.get_state_ids_to_track(block_fn, state)
changed_state_ids = []
if batch:
max_batch_size = block_fn.max_batch_size
@ -1924,11 +1915,10 @@ Received outputs:
state,
)
preds = result["prediction"]
data_and_changed_state_ids = [
data = [
await self.postprocess_data(block_fn, list(o), state)
for o in zip(*preds)
]
data, changed_state_ids = zip(*data_and_changed_state_ids)
if root_path is not None:
data = processing_utils.add_root_url(data, root_path, None)
data = list(zip(*data))
@ -1952,9 +1942,14 @@ Received outputs:
in_event_listener,
state,
)
data, changed_state_ids = await self.postprocess_data(
block_fn, result["prediction"], state
)
data = await self.postprocess_data(block_fn, result["prediction"], state)
if state:
changed_state_ids = [
state_id
for hash_value, state_id in zip(hashed_values, state_ids_to_track)
if hash_value != utils.deep_hash(state[state_id])
]
if root_path is not None:
data = processing_utils.add_root_url(data, root_path, None)
is_generating, iterator = result["is_generating"], result["iterator"]
@ -1995,6 +1990,22 @@ Received outputs:
return output
def get_state_ids_to_track(
self, block_fn: BlockFunction, state: SessionState | None
) -> tuple[list[int], list]:
if state is None:
return [], []
state_ids_to_track = []
hashed_values = []
for block in block_fn.outputs:
if block.stateful and any(
(block._id, "change") in fn.targets for fn in self.fns.values()
):
value = state[block._id]
state_ids_to_track.append(block._id)
hashed_values.append(utils.deep_hash(value))
return state_ids_to_track, hashed_values
def create_limiter(self):
self.limiter = (
None

View File

@ -326,9 +326,7 @@ class Examples:
[output.render() for output in self.outputs]
demo.load(self.fn, self.inputs, self.outputs)
demo.unrender()
return (await demo.postprocess_data(demo.default_config.fns[0], output, None))[
0
]
return await demo.postprocess_data(demo.default_config.fns[0], output, None)
def _get_cached_index_if_cached(self, example_index) -> int | None:
if Path(self.cached_indices_file).exists():

View File

@ -6,6 +6,7 @@ import ast
import asyncio
import copy
import functools
import hashlib
import importlib
import importlib.util
import inspect
@ -1402,3 +1403,25 @@ def connect_heartbeat(config: dict[str, Any], blocks) -> bool:
if any_unload:
break
return any_state or any_unload
def deep_hash(obj):
"""Compute a hash for a deeply nested data structure."""
hasher = hashlib.sha256()
if isinstance(obj, (int, float, str, bytes)):
items = obj
elif isinstance(obj, dict):
items = tuple(
[
(k, deep_hash(v))
for k, v in sorted(obj.items(), key=lambda x: hash(x[0]))
]
)
elif isinstance(obj, (list, tuple)):
items = tuple(deep_hash(x) for x in obj)
elif isinstance(obj, set):
items = tuple(deep_hash(x) for x in sorted(obj, key=hash))
else:
items = str(id(obj)).encode("utf-8")
hasher.update(repr(items).encode("utf-8"))
return hasher.hexdigest()

View File

@ -51,7 +51,7 @@ $demo_todo_list
Note that almost the entire app is inside a single `gr.render` that reacts to the tasks `gr.State` variable. This variable is a nested list, which presents some complexity. If you design a `gr.render` to react to a list or dict structure, ensure you do the following:
1. Any event listener that modifies the state variable in a manner that should trigger a re-render should return a new value, not simply modify the state variable directly. A `gr.State` change event listener will not trigger if the state is provided as input and then modified directly. A new variable must be returned that can be compared against. That's why in the `mark_done` method, we create a new list that we edit. Similarly, in `delete`, we create a new list instead of calling `task_list.remove(task)`, and in `add_task`, we don't use `task_list.append`.
1. Any event listener that modifies a state variable in a manner that should trigger a re-render must set the state variable as an output. This lets Gradio know to check if the variable has changed behind the scenes.
2. In a `gr.render`, if a variable in a loop is used inside an event listener function, that variable should be "frozen" via setting it to itself as a default argument in the function header. See how we have `task=task` in both `mark_done` and `delete`. This freezes the variable to its "loop-time" value.
Let's take a look at one last example that uses everything we learned. Below is an audio mixer. Provide multiple audio tracks and mix them together.

View File

@ -0,0 +1,39 @@
import { test, expect } from "@gradio/tootils";
test("test 2d state-based render", async ({ page }) => {
await page.getByRole("button", { name: "Increment A" }).click();
await expect(
page.locator("button").filter({ hasText: "Button" })
).toHaveCount(0);
await page.getByRole("button", { name: "Increment B" }).click();
await page.getByRole("button", { name: "Increment A" }).click();
await expect(
page.locator("button").filter({ hasText: "Button" })
).toHaveCount(2);
await page.getByRole("button", { name: "Increment A" }).click();
await page.getByRole("button", { name: "Increment B" }).click();
await page.getByRole("button", { name: "Increment A" }).click();
await expect(
page.locator("button").filter({ hasText: "Button" })
).toHaveCount(8);
});
test("test datastructure-based state changes", async ({ page }) => {
await page.getByRole("button", { name: "Count to" }).click();
await expect(page.getByLabel("Output")).toHaveValue(
`{1: 1, 2: 2, 3: 3}\n[[1, 2, 3], [1, 2, 3], [1, 2, 3]]\n{1, 2, 3}`
);
await expect(page.getByLabel("Changes")).toHaveValue("1");
await page.getByRole("button", { name: "Count to" }).click();
await page.getByRole("button", { name: "Count to" }).click();
await page.getByRole("button", { name: "Count to" }).click();
await expect(page.getByLabel("Output")).toHaveValue(
`{1: 1, 2: 2, 3: 3}\n[[1, 2, 3], [1, 2, 3], [1, 2, 3]]\n{1, 2, 3}`
);
await expect(page.getByLabel("Changes")).toHaveValue("1");
await page.getByRole("button", { name: "Zero All" }).click();
await expect(page.getByLabel("Output")).toHaveValue(
`{0: 0}\n[[0, 0, 0], [0, 0, 0], [0, 0, 0]]\n{0}`
);
await expect(page.getByLabel("Changes")).toHaveValue("2");
});

View File

@ -0,0 +1,50 @@
import { test, expect } from "@gradio/tootils";
test("clicking through tabs shows correct content", async ({ page }) => {
await expect(page.locator("body")).toContainText("Incomplete Tasks (0)");
await expect(page.locator("body")).toContainText("Complete Tasks (0)");
const input_text = page.getByLabel("Task Name");
await input_text.fill("eat");
await input_text.press("Enter");
await expect(page.locator("body")).not.toContainText("Incomplete Tasks (0)");
await expect(page.locator("body")).toContainText("Incomplete Tasks (1)");
await expect(page.locator("body")).toContainText("Complete Tasks (0)");
await expect(page.locator("textarea").nth(1)).toHaveValue("eat");
await input_text.fill("pray");
await input_text.press("Enter");
await expect(page.locator("body")).toContainText("Incomplete Tasks (2)");
await expect(page.locator("body")).toContainText("Complete Tasks (0)");
await expect(page.locator("textarea").nth(2)).toHaveValue("pray");
await input_text.fill("love");
await input_text.press("Enter");
await expect(page.locator("body")).toContainText("Incomplete Tasks (3)");
await expect(page.locator("body")).toContainText("Complete Tasks (0)");
await expect(page.locator("textarea").nth(1)).toHaveValue("eat");
await expect(page.locator("textarea").nth(2)).toHaveValue("pray");
await expect(page.locator("textarea").nth(3)).toHaveValue("love");
const done_btn_for_eat = page
.locator("button")
.filter({ hasText: "Done" })
.first();
await done_btn_for_eat.click();
await expect(page.locator("body")).toContainText("Incomplete Tasks (2)");
await expect(page.locator("body")).toContainText("Complete Tasks (1)");
const delete_btn_for_love = page
.locator("button")
.filter({ hasText: "Delete" })
.last();
await delete_btn_for_love.click();
await expect(page.locator("body")).toContainText("Incomplete Tasks (1)");
await expect(page.locator("body")).toContainText("Complete Tasks (1)");
});

View File

@ -525,7 +525,7 @@ class TestBlocksPostprocessing:
outputs=io_components,
)
output, _ = await demo.postprocess_data(
output = await demo.postprocess_data(
demo.fns[0], [gr.update(value=None) for _ in io_components], state=None
)
@ -550,7 +550,7 @@ class TestBlocksPostprocessing:
outputs=text,
)
output, _ = await demo.postprocess_data(
output = await demo.postprocess_data(
demo.fns[0], gr.update(value="NO_VALUE"), state=None
)
assert output[0]["value"] == "NO_VALUE"
@ -566,7 +566,7 @@ class TestBlocksPostprocessing:
checkbox = gr.Checkbox(value=True, label="Show image")
checkbox.change(change_visibility, inputs=checkbox, outputs=im_list)
output, _ = await demo.postprocess_data(
output = await demo.postprocess_data(
demo.fns[0], [gr.update(visible=False)] * 2, state=None
)
assert output == [
@ -586,12 +586,12 @@ class TestBlocksPostprocessing:
update.click(update_values, inputs=[num], outputs=[num2])
output, _ = await demo.postprocess_data(
output = await demo.postprocess_data(
demo.fns[0], {num2: gr.Number(value=42)}, state=None
)
assert output[0]["value"] == 42
output, _ = await demo.postprocess_data(demo.fns[0], {num2: 23}, state=None)
output = await demo.postprocess_data(demo.fns[0], {num2: 23}, state=None)
assert output[0] == 23
@pytest.mark.asyncio
@ -1695,7 +1695,7 @@ async def test_blocks_postprocessing_with_copies_of_component_instance():
fn=clear_func, outputs=[chatbot, chatbot2, chatbot3], api_name="clear"
)
output, _ = await demo.postprocess_data(
output = await demo.postprocess_data(
demo.fns[0], [gr.Chatbot(value=[])] * 3, None
)
assert output == [{"value": [], "__type__": "update"}] * 3