Support gr.load()-ing Gradio apps with Blocks.load() events (#10324)

* changes

* add changeset

* changes

* changes

* add changeset

* changes

---------

Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
This commit is contained in:
Abubakar Abid 2025-01-10 10:14:58 -08:00 committed by GitHub
parent 354341826a
commit 343503d62e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 79 additions and 26 deletions

View File

@ -0,0 +1,5 @@
---
"gradio": patch
---
fix:Support `gr.load()`-ing Gradio apps with `Blocks.load()` events

View File

@ -40,7 +40,7 @@ from gradio import (
utils,
wasm_utils,
)
from gradio.blocks_events import BlocksEvents, BlocksMeta
from gradio.blocks_events import BLOCKS_EVENTS, BlocksEvents, BlocksMeta
from gradio.context import (
Context,
LocalContext,
@ -1315,21 +1315,14 @@ class Blocks(BlockContext, BlocksEvents, metaclass=BlocksMeta):
)
dependency["no_target"] = True
else:
targets = [
getattr(
original_mapping[
target if isinstance(target, int) else target[0]
],
trigger if isinstance(target, int) else target[1],
)
for target in _targets
]
targets = [
EventListenerMethod(
t.__self__ if t.has_trigger else None,
t.event_name, # type: ignore
)
for t in targets
for t in Blocks.get_event_targets(
original_mapping, _targets, trigger
)
]
dependency = root_block.default_config.set_event_trigger(
targets=targets, fn=fn, **dependency
@ -1433,6 +1426,11 @@ class Blocks(BlockContext, BlocksEvents, metaclass=BlocksMeta):
]
for dependency in self.fns.values():
dependency._id += dependency_offset
# Any event -- e.g. Blocks.load() -- that is triggered by this Blocks
# should now be triggered by the root Blocks instead.
for target in dependency.targets:
if target[0] == self._id:
target = (Context.root_block._id, target[1])
api_name = dependency.api_name
if isinstance(api_name, str):
api_name_ = utils.append_unique_suffix(
@ -3006,3 +3004,29 @@ Received inputs:
api_info["named_endpoints"][f"/{fn.api_name}"] = dependency_info
return api_info
@staticmethod
def get_event_targets(
original_mapping: dict[int, Block], _targets: list, trigger: str
) -> list:
target_events = []
for target in _targets:
# If target is just an integer (old format), use it directly with the trigger
# Otherwise target is a tuple and we use its components
target_id = target if isinstance(target, int) else target[0]
event_name = trigger if isinstance(target, int) else target[1]
block = original_mapping.get(target_id)
# Blocks events are a special case because they are not stored in the blocks list in the config
if block is None:
if event_name in [
event.event_name if isinstance(event, EventListener) else event
for event in BLOCKS_EVENTS
]:
block = Context.root_block
else:
raise ValueError(
f"Cannot find Block with id: {target_id} but is present as a target in the config"
)
event = getattr(block, event_name)
target_events.append(event)
return target_events

View File

@ -518,7 +518,7 @@ def safe_deepcopy(obj: Any) -> Any:
def assert_configs_are_equivalent_besides_ids(
config1: dict, config2: dict, root_keys: tuple = ("mode",)
config1: BlocksConfigDict, config2: BlocksConfigDict, root_keys: tuple = ("mode",)
):
"""Allows you to test if two different Blocks configs produce the same demo.
@ -563,20 +563,31 @@ def assert_configs_are_equivalent_besides_ids(
if "children" in child1 or "children" in child2:
same_children_recursive(child1["children"], child2["children"])
children1 = config1["layout"]["children"]
children2 = config2["layout"]["children"]
same_children_recursive(children1, children2)
if "layout" in config1:
if "layout" not in config2:
raise ValueError(
"The first config has a layout key, but the second does not"
)
children1 = config1["layout"]["children"]
children2 = config2["layout"]["children"]
same_children_recursive(children1, children2)
for d1, d2 in zip(config1["dependencies"], config2["dependencies"], strict=False):
for t1, t2 in zip(d1.pop("targets"), d2.pop("targets"), strict=False):
assert_same_components(t1[0], t2[0])
for i1, i2 in zip(d1.pop("inputs"), d2.pop("inputs"), strict=False):
assert_same_components(i1, i2)
for o1, o2 in zip(d1.pop("outputs"), d2.pop("outputs"), strict=False):
assert_same_components(o1, o2)
if d1 != d2:
raise ValueError(f"{d1} does not match {d2}")
if "dependencies" in config1:
if "dependencies" not in config2:
raise ValueError(
"The first config has a dependencies key, but the second does not"
)
for d1, d2 in zip(
config1["dependencies"], config2["dependencies"], strict=False
):
for t1, t2 in zip(d1.pop("targets"), d2.pop("targets"), strict=False):
assert_same_components(t1[0], t2[0])
for i1, i2 in zip(d1.pop("inputs"), d2.pop("inputs"), strict=False):
assert_same_components(i1, i2)
for o1, o2 in zip(d1.pop("outputs"), d2.pop("outputs"), strict=False):
assert_same_components(o1, o2)
if d1 != d2:
raise ValueError(f"{d1} does not match {d2}")
return True

View File

@ -92,7 +92,20 @@ class TestBlocksMethods:
for component in config1["components"]:
component["props"]["proxy_url"] = f"{fake_url}/"
config2 = demo2.get_config_file()
assert assert_configs_are_equivalent_besides_ids(config1, config2) # type: ignore
assert assert_configs_are_equivalent_besides_ids(config1, config2)
def test_load_from_config_with_blocks_events(self):
fake_url = "https://fake.hf.space"
def fn():
return "Hello"
with gr.Blocks() as demo:
t = gr.Textbox()
demo.load(fn, None, t)
config = demo.get_config_file()
gr.Blocks.from_config(config, [fn], fake_url) # Should not raise
def test_partial_fn_in_config(self):
def greet(name, formatter):