gradio icon indicating copy to clipboard operation
gradio copied to clipboard

A change handler for `gr.State`

Open ankrgyl opened this issue 2 years ago • 6 comments

  • [x] I have searched to see if a similar issue already exists.

Is your feature request related to a problem? Please describe.

I have 3 different input components that affect a variable -- essentially in my app, you can upload a document one of 3 ways (file upload, url, or an example). Each upload updates a document variable, which has the bytes of the upload + some pre-computed metadata. I'd like any of these three methods of upload to update some state that depends on the value of the document.

Describe the solution you'd like

It'd be very convenient to write something like

document.change(process_document, document, output_component)

so that each of the 3 upload components simply needs to update the variable, and then the rest of the pipeline runs.

Additional context
N/A but happy to provide more details if helpful.

ankrgyl avatar Aug 28 '22 23:08 ankrgyl

Thanks for creating the issue @ankrgyl. Would you be able to provide a more complete example here of what you'd like to do so we can use it to reproduce and test the fix?

abidlabs avatar Aug 28 '22 23:08 abidlabs

Absolutely! Here is the program I'd like to be able to write:

import gradio as gr
import requests
import os


def process_url(url):
    return requests.get(url, stream=True).content.read()


def process_file(file):
    return open(file.name, "rb").read()


with gr.Blocks() as demo:
    file_bytes = gr.Variable()
    with gr.Row():
        url = gr.Textbox(label="URL", interactive=True)
        file = gr.File(label="Upload", interactive=True)

    byte_len = gr.JSON(label="File length")

    file.change(process_file, inputs=[file], outputs=[file_bytes])
    url.change(process_url, inputs=[file], outputs=[file_bytes])
    file_bytes.change(lambda b: len(b), inputs=[file_bytes], outputs=[byte_len])

if __name__ == "__main__":
    demo.launch()

and the one I have to write:

import gradio as gr
import requests
import os


def process_url(url):
    b = requests.get(url, stream=True).content.read()
    return b, len(b)


def process_file(file):
    b = open(file.name, "rb").read()
    return b, len(b)


with gr.Blocks() as demo:
    file_bytes = gr.Variable()
    with gr.Row():
        url = gr.Textbox(label="URL", interactive=True)
        file = gr.File(label="Upload", interactive=True)

    byte_len = gr.JSON(label="File length")

    file.change(process_file, inputs=[file], outputs=[file_bytes, byte_len])
    url.change(process_url, inputs=[file], outputs=[file_bytes, byte_len])

if __name__ == "__main__":
    demo.launch()

(I omitted some details, like other uses of the variable, but imagine there's another control in the app, like a radio button which selects which "metric" you want to display about the file).

Hope this helps, and apologize if I missed something in the docs or elsewhere.

ankrgyl avatar Aug 28 '22 23:08 ankrgyl

A big +1 to this request. In my case I keep a per-session list of items in a gr.State and in my UI (across several tabs) populate several gr.Dropdown instances with the contents of that list. When the list changes (e.g. I edit an item or create a new one) I need all those dropdowns to refresh & reflect the updated state.

I've used the workaround @ankrgyl mentions above, where (in my example case) anything that modifies the gr.State would also be responsible for with generating updates to the gr.Dropdown instances. It works, and wasn't a big deal when my app was toy-sized, but UI code can quickly become quite hard to read, and arranging the flow of change events sensibly is important in keeping the code logically legible.

I ended up with an even more arcane workaround for now, but I'd dearly love to toss it.

zellski avatar Mar 08 '23 15:03 zellski

It might make sense for us to think of this as part of #2570 @aliabid94 instead of creating a specific gr.State.change() event

abidlabs avatar Mar 18 '23 14:03 abidlabs

This issue proposes a State.change -> IOComponent.update flow, which is the general solution to state reactivity and effectively fixes the IOComponent.change -> IOComponent.update flow expectation that is being violated.

I believe #2570 is syntactic sugar for the special State.change -> State.update flow and can be deferred until the underlying functionality proposed in this issue is implemented.

deckar01 avatar Sep 02 '23 16:09 deckar01

I would love this feature too. Being able to decouple components via a state variable is really powerful and allows you to simplify the code. It's currently really hard to have some kind of status / state trigger other components without explicitly adding them. It becomes even more complicated when you want something to update itself based on some state because then change handlers on that component will end up firing multiple times or end up in an infinite loop.

JohnDuncanScott avatar Mar 20 '24 08:03 JohnDuncanScott