Skip to content

Instantly share code, notes, and snippets.

@Gerschel
Last active May 6, 2023 01:31
Show Gist options
  • Save Gerschel/51a0e2a9089795ff399f9e346ae68085 to your computer and use it in GitHub Desktop.
Save Gerschel/51a0e2a9089795ff399f9e346ae68085 to your computer and use it in GitHub Desktop.
Gradio dropdown example with x number of dropdowns; fix for loop and list comprehension concerning scope with components
"""
Example program for moving forward through dropdowns, each one being dependent
on the prior one.
Ideally, you'd want to disable interactive, so they can't pick a choice higher up
that was selected lower. #SOLVED: ~~I haven't solved the interactive issue yet~~. Interaction issue solved.
gr.Dropdown.update interactive didn't modify when using this style of dependent dropdowns
The fix was to use gr.update, which, in other testing, didn't work when developing this script,
but does, when you have the helper functions.
List of lists no longer needed, after applying the fixes for the list comprehension, cleaning the code
up and testing it, showed it still worked.
"""
import gradio as gr
from string import ascii_uppercase
#import to make type hinting shut-up when passing functions, such as `fn` parameter, and returning lambda
import typing
with gr.Blocks(analytics_enabled=False) as demo:
# helper functions
def configure_choices(i: int, x: str) -> list[str]:
dropdown_container[i+1].choices = [y for y in dropdown_container[i].choices if y != x]
return dropdown_container[i+1].choices
def closure_fun(i: int) -> typing.Callable[[str], list[dict[str, str | bool]]]:
def func(x: str) -> list[dict[str, str | bool]]:
return [
#func: typing.Callable[[str], list[dict]] = lambda x: [
gr.update(interactive=False),
gr.update(
choices = configure_choices(i, x),
visible=True,
interactive=True
)
]
return func
# Initial choices
dropdown_choice: list[str] = [ascii_uppercase[x:x+3] for x in range(0,15,3)]
# Component container, prior iteration of code required it as a list of lists
dropdown_container: list[gr.Dropdown] = []
for x in range(len(dropdown_choice)):
# Create element for the same quantity of choices, in case all choices will be used
dropdown_container.append(gr.Dropdown(choices=None if x else dropdown_choice,
interactive= not x,
visible= not x
)
)
# Set all other choices as you go forward through the dropdowns
for i, e in enumerate(dropdown_container):
if i+1 < len(dropdown_container):
e.change(
fn=closure_fun(i),
inputs = e,
#NOTE: Can't use `i` in inputs and outputs, after we fall off the `for`, it isn't retained in the `.change` method\
# but, `e` is
outputs= [dropdown_container[dropdown_container.index(e)], dropdown_container[dropdown_container.index(e) + 1]]
)
else:
e.change(
#dummy function needed to work for this example
fn=lambda x: print(x),
inputs = e,
)
if __name__ == "__main__":
demo.launch()
@Gerschel
Copy link
Author

Gerschel commented Dec 8, 2022

Update: This code is a little more condense and straight to the point than the prior version. It also solves issues I had with updating visibility and interaction.

Demo: Note the list pops selected element out and passes on the rest to the next dropdown.
ui_test4_demo

I had worked this out while in a discussion, you could get some rough details there. https://github.com/gradio-app/gradio/discussions/2769
But in essence, I was creating a script for stable-diffusion-webui, and came across a barrier, a few times.

I was writing scripts that were dense, that could handle x number of variables, y number of times. I wanted the scripts to be dynamic, where, if something changed in a file higher up the chain, the code would handle those new changes, barring a few things like name changes.

An example, I want to include presets for my scripts, but all presets could include items from the paste_fields, samplers and model checkpoints. Obviously the app and user can add more to their install. So I feel it would be stupid to not code for that as a variable.

To achieve this, I was using list comprehensions and loops to build the components.

This resulted in things not working.

In my journey to discover what was going on, I put together a minimal dropdown app, where I could play around with and interact in many iterations.

Initially I had list comprehensions as my fn parameter. I found that the comprehensions were holding on to old scopes of empty choices from the component, even if I updated the choices manually for that component.
Another issue, the code technically ran off the end of the for loop, but for some reason inputs and outputs kept pointing at the correct view of the component (while the list comprehension was not), and if I enumerated the list, the indice would always be equal to length of list.

So I had to use closure in the fn parameter to retain the indice.

I had to relocate the lambda.

With gradio running as a backend, you are not suppose to be able to get this tricky without using js (noted for reason to move the lambda or function call). Mentioned to me in the discussion linked above.

The solution was a list, also crammed into the same list, that could be written to, then read from to write the next values, for chaining.
I needed something that could be read from any child scope.
I think this has to do with the with statement, locking in scope in the class gr.blocks (the thing that broke the lambda).

So the resolution was a list, which they can be mutated out of scope.

Which brings us to:
list to serve as container for a lists, the inner lists contains the component on the left, and mutable choices on the right, not a copy or pointer to the components choices attribute, that's stale, wont update once we leave the with block.

To solve the interactive option, you might need to do the same thing. I would propose a named tuple, with elements that are lists, for proper mutablility out of sccope, versus using the namedtuple replace method, which might work.
I'm certain I'm going to continue down this topic and would create an updated gist to cover it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment