from __future__ import annotations import copy import hashlib import inspect import json import os import random import secrets import string import sys import threading import time import warnings import webbrowser from collections import defaultdict from pathlib import Path from types import ModuleType from typing import TYPE_CHECKING, Any, AsyncIterator, Callable, Literal, Sequence, cast from urllib.parse import urlparse, urlunparse import anyio import fastapi import httpx from anyio import CapacityLimiter from gradio_client import utils as client_utils from gradio_client.documentation import document from gradio import ( analytics, components, networking, processing_utils, queueing, routes, strings, themes, utils, wasm_utils, ) from gradio.blocks_events import BlocksEvents, BlocksMeta from gradio.context import ( Context, get_blocks_context, get_render_context, set_render_context, ) from gradio.data_classes import FileData, GradioModel, GradioRootModel from gradio.events import ( EventData, EventListener, EventListenerMethod, ) from gradio.exceptions import ( DuplicateBlockError, InvalidApiNameError, InvalidComponentError, ) from gradio.helpers import create_tracker, skip, special_args from gradio.state_holder import SessionState from gradio.themes import Default as DefaultTheme from gradio.themes import ThemeClass as Theme from gradio.tunneling import ( BINARY_FILENAME, BINARY_FOLDER, BINARY_PATH, BINARY_URL, CURRENT_TUNNELS, ) from gradio.utils import ( TupleNoPrint, check_function_inputs_match, component_or_layout_class, get_cancel_function, get_continuous_fn, get_package_version, get_upload_folder, ) try: import spaces # type: ignore except Exception: spaces = None if TYPE_CHECKING: # Only import for type checking (is False at runtime). from fastapi.applications import FastAPI from gradio.components.base import Component from gradio.renderable import Renderable BUILT_IN_THEMES: dict[str, Theme] = { t.name: t for t in [ themes.Base(), themes.Default(), themes.Monochrome(), themes.Soft(), themes.Glass(), ] } class Block: def __init__( self, *, elem_id: str | None = None, elem_classes: list[str] | str | None = None, render: bool = True, key: int | str | None = None, visible: bool = True, proxy_url: str | None = None, ): self._id = Context.id Context.id += 1 self.visible = visible self.elem_id = elem_id self.elem_classes = ( [elem_classes] if isinstance(elem_classes, str) else elem_classes ) self.proxy_url = proxy_url self.share_token = secrets.token_urlsafe(32) self.parent: BlockContext | None = None self.is_rendered: bool = False self._constructor_args: list[dict] self.state_session_capacity = 10000 self.temp_files: set[str] = set() self.GRADIO_CACHE = get_upload_folder() self.key = key # Keep tracks of files that should not be deleted when the delete_cache parmaeter is set # These files are the default value of the component and files that are used in examples self.keep_in_cache = set() if render: self.render() @property def stateful(self): return False @property def skip_api(self): return False @property def constructor_args(self) -> dict[str, Any]: """Get the arguments passed to the component's initializer. Only set classes whose metaclass is ComponentMeta """ # the _constructor_args list is appended based on the mro of the class # so the first entry is for the bottom of the hierarchy return self._constructor_args[0] if self._constructor_args else {} @property def events( self, ) -> list[EventListener]: return getattr(self, "EVENTS", []) def render(self): """ Adds self into appropriate BlockContext """ root_context = get_blocks_context() render_context = get_render_context() if root_context is not None and self._id in root_context.blocks: raise DuplicateBlockError( f"A block with id: {self._id} has already been rendered in the current Blocks." ) if render_context is not None: render_context.add(self) if root_context is not None: root_context.blocks[self._id] = self self.is_rendered = True if isinstance(self, components.Component): root_context.root_block.temp_file_sets.append(self.temp_files) return self def unrender(self): """ Removes self from BlockContext if it has been rendered (otherwise does nothing). Removes self from the layout and collection of blocks, but does not delete any event triggers. """ root_context = get_blocks_context() render_context = get_render_context() if render_context is not None: try: render_context.children.remove(self) except ValueError: pass if root_context is not None: try: del root_context.blocks[self._id] self.is_rendered = False except KeyError: pass return self def get_block_name(self) -> str: """ Gets block's class name. If it is template component it gets the parent's class name. This is used to identify the Svelte file to use in the frontend. Override this method if a component should use a different Svelte file than the default naming convention. """ return ( self.__class__.__base__.__name__.lower() # type: ignore if hasattr(self, "is_template") else self.__class__.__name__.lower() ) def get_block_class(self) -> str: """ Gets block's class name. If it is template component it gets the parent's class name. Very similar to the get_block_name method, but this method is used to reconstruct a Gradio app that is loaded from a Space using gr.load(). This should generally NOT be overridden. """ return ( self.__class__.__base__.__name__.lower() # type: ignore if hasattr(self, "is_template") else self.__class__.__name__.lower() ) def get_expected_parent(self) -> type[BlockContext] | None: return None def get_config(self): config = {} signature = inspect.signature(self.__class__.__init__) for parameter in signature.parameters.values(): if hasattr(self, parameter.name): value = getattr(self, parameter.name) config[parameter.name] = utils.convert_to_dict_if_dataclass(value) for e in self.events: to_add = e.config_data() if to_add: config = {**to_add, **config} config.pop("render", None) config = {**config, "proxy_url": self.proxy_url, "name": self.get_block_class()} if (_selectable := getattr(self, "_selectable", None)) is not None: config["_selectable"] = _selectable return config @classmethod def recover_kwargs( cls, props: dict[str, Any], additional_keys: list[str] | None = None ): """ Recovers kwargs from a dict of props. """ additional_keys = additional_keys or [] signature = inspect.signature(cls.__init__) kwargs = {} for parameter in signature.parameters.values(): if parameter.name in props and parameter.name not in additional_keys: kwargs[parameter.name] = props[parameter.name] return kwargs async def async_move_resource_to_block_cache( self, url_or_file_path: str | Path | None ) -> str | None: """Moves a file or downloads a file from a url to a block's cache directory, adds to to the block's temp_files, and returns the path to the file in cache. This ensures that the file is accessible to the Block and can be served to users. This async version of the function is used when this is being called within a FastAPI route, as this is not blocking. """ if url_or_file_path is None: return None if isinstance(url_or_file_path, Path): url_or_file_path = str(url_or_file_path) if client_utils.is_http_url_like(url_or_file_path): temp_file_path = await processing_utils.async_save_url_to_cache( url_or_file_path, cache_dir=self.GRADIO_CACHE ) self.temp_files.add(temp_file_path) else: url_or_file_path = str(utils.abspath(url_or_file_path)) if not utils.is_in_or_equal(url_or_file_path, self.GRADIO_CACHE): try: temp_file_path = processing_utils.save_file_to_cache( url_or_file_path, cache_dir=self.GRADIO_CACHE ) except FileNotFoundError: # This can happen if when using gr.load() and the file is on a remote Space # but the file is not the `value` of the component. For example, if the file # is the `avatar_image` of the `Chatbot` component. In this case, we skip # copying the file to the cache and just use the remote file path. return url_or_file_path else: temp_file_path = url_or_file_path self.temp_files.add(temp_file_path) return temp_file_path def move_resource_to_block_cache( self, url_or_file_path: str | Path | None ) -> str | None: """Moves a file or downloads a file from a url to a block's cache directory, adds to to the block's temp_files, and returns the path to the file in cache. This ensures that the file is accessible to the Block and can be served to users. This sync version of the function is used when this is being called outside of a FastAPI route, e.g. when examples are being cached. """ if url_or_file_path is None: return None if isinstance(url_or_file_path, Path): url_or_file_path = str(url_or_file_path) if client_utils.is_http_url_like(url_or_file_path): temp_file_path = processing_utils.save_url_to_cache( url_or_file_path, cache_dir=self.GRADIO_CACHE ) self.temp_files.add(temp_file_path) else: url_or_file_path = str(utils.abspath(url_or_file_path)) if not utils.is_in_or_equal(url_or_file_path, self.GRADIO_CACHE): try: temp_file_path = processing_utils.save_file_to_cache( url_or_file_path, cache_dir=self.GRADIO_CACHE ) except FileNotFoundError: # This can happen if when using gr.load() and the file is on a remote Space # but the file is not the `value` of the component. For example, if the file # is the `avatar_image` of the `Chatbot` component. In this case, we skip # copying the file to the cache and just use the remote file path. return url_or_file_path else: temp_file_path = url_or_file_path self.temp_files.add(temp_file_path) return temp_file_path def serve_static_file( self, url_or_file_path: str | Path | dict | None ) -> dict | None: """If a file is a local file, moves it to the block's cache directory and returns a FileData-type dictionary corresponding to the file. If the file is a URL, returns a FileData-type dictionary corresponding to the URL. This ensures that the file is accessible in the frontend and can be served to users. Examples: >>> block.serve_static_file("https://gradio.app/logo.png") -> {"path": "https://gradio.app/logo.png", "url": "https://gradio.app/logo.png"} >>> block.serve_static_file("logo.png") -> {"path": "logo.png", "url": "/file=logo.png"} >>> block.serve_static_file({"path": "logo.png", "url": "/file=logo.png"}) -> {"path": "logo.png", "url": "/file=logo.png"} """ if url_or_file_path is None: return None if isinstance(url_or_file_path, dict): return url_or_file_path if isinstance(url_or_file_path, Path): url_or_file_path = str(url_or_file_path) if client_utils.is_http_url_like(url_or_file_path): return FileData(path=url_or_file_path, url=url_or_file_path).model_dump() else: data = {"path": url_or_file_path} try: return client_utils.synchronize_async( processing_utils.async_move_files_to_cache, data, self ) except AttributeError: # Can be raised if this function is called before the Block is fully initialized. return data class BlockContext(Block): def __init__( self, elem_id: str | None = None, elem_classes: list[str] | str | None = None, visible: bool = True, render: bool = True, ): """ Parameters: elem_id: An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles. elem_classes: An optional string or list of strings that are assigned as the class of this component in the HTML DOM. Can be used for targeting CSS styles. visible: If False, this will be hidden but included in the Blocks config file (its visibility can later be updated). render: If False, this will not be included in the Blocks config file at all. """ self.children: list[Block] = [] Block.__init__( self, elem_id=elem_id, elem_classes=elem_classes, visible=visible, render=render, ) TEMPLATE_DIR = "./templates/" FRONTEND_DIR = "../../frontend/" @property def skip_api(self): return True @classmethod def get_component_class_id(cls) -> str: module_name = cls.__module__ module_path = sys.modules[module_name].__file__ module_hash = hashlib.md5(f"{cls.__name__}_{module_path}".encode()).hexdigest() return module_hash @property def component_class_id(self): return self.get_component_class_id() def add_child(self, child: Block): self.children.append(child) def __enter__(self): render_context = get_render_context() self.parent = render_context set_render_context(self) return self def add(self, child: Block): child.parent = self self.children.append(child) def fill_expected_parents(self): root_context = get_blocks_context() children = [] pseudo_parent = None for child in self.children: expected_parent = child.get_expected_parent() if not expected_parent or isinstance(self, expected_parent): pseudo_parent = None children.append(child) else: if pseudo_parent is not None and isinstance( pseudo_parent, expected_parent ): pseudo_parent.add_child(child) else: pseudo_parent = expected_parent(render=False) pseudo_parent.parent = self children.append(pseudo_parent) pseudo_parent.add_child(child) if root_context: root_context.blocks[pseudo_parent._id] = pseudo_parent child.parent = pseudo_parent self.children = children def __exit__(self, exc_type: type[BaseException] | None = None, *args): set_render_context(self.parent) if exc_type is not None: return if getattr(self, "allow_expected_parents", True): self.fill_expected_parents() def postprocess(self, y): """ Any postprocessing needed to be performed on a block context. """ return y class BlockFunction: def __init__( self, fn: Callable | None, inputs: list[Component], outputs: list[Component], preprocess: bool, postprocess: bool, inputs_as_dict: bool, targets: list[tuple[int | None, str]], batch: bool = False, max_batch_size: int = 4, concurrency_limit: int | None | Literal["default"] = "default", concurrency_id: str | None = None, tracks_progress: bool = False, api_name: str | Literal[False] = False, js: str | None = None, show_progress: Literal["full", "minimal", "hidden"] = "full", every: float | None = None, cancels: list[int] | None = None, collects_event_data: bool = False, trigger_after: int | None = None, trigger_only_on_success: bool = False, trigger_mode: Literal["always_last", "once", "multiple"] = "once", queue: bool | None = None, scroll_to_output: bool = False, show_api: bool = True, renderable: Renderable | None = None, is_cancel_function: bool = False, ): self.fn = fn self.inputs = inputs self.outputs = outputs self.preprocess = preprocess self.postprocess = postprocess self.tracks_progress = tracks_progress self.concurrency_limit: int | None | Literal["default"] = concurrency_limit self.concurrency_id = concurrency_id or str(id(fn)) self.batch = batch self.max_batch_size = max_batch_size self.total_runtime = 0 self.total_runs = 0 self.inputs_as_dict = inputs_as_dict self.targets = targets self.name = getattr(fn, "__name__", "fn") if fn is not None else None self.api_name = api_name self.js = js self.show_progress = show_progress self.every = every self.cancels = cancels or [] self.collects_event_data = collects_event_data self.trigger_after = trigger_after self.trigger_only_on_success = trigger_only_on_success self.trigger_mode = trigger_mode self.queue = False if fn is None else queue self.scroll_to_output = False if utils.get_space() else scroll_to_output self.show_api = show_api self.zero_gpu = hasattr(self.fn, "zerogpu") self.types_continuous = bool(self.every) self.types_generator = ( inspect.isgeneratorfunction(self.fn) or inspect.isasyncgenfunction(self.fn) or bool(self.every) ) self.renderable = renderable # We need to keep track of which events are cancel events # in two places: # 1. So that we can skip postprocessing for cancel events. # They return event_ids that have been cancelled but there # are no output components # 2. So that we can place the ProcessCompletedMessage in the # event stream so that clients can close the stream when necessary self.is_cancel_function = is_cancel_function self.spaces_auto_wrap() def spaces_auto_wrap(self): if spaces is None: return if utils.get_space() is None: return self.fn = spaces.gradio_auto_wrap(self.fn) def __str__(self): return str( { "fn": self.name, "preprocess": self.preprocess, "postprocess": self.postprocess, } ) def __repr__(self): return str(self) def get_config(self): return { "targets": self.targets, "inputs": [block._id for block in self.inputs], "outputs": [block._id for block in self.outputs], "backend_fn": self.fn is not None, "js": self.js, "queue": self.queue, "api_name": self.api_name, "scroll_to_output": self.scroll_to_output, "show_progress": self.show_progress, "every": self.every, "batch": self.batch, "max_batch_size": self.max_batch_size, "cancels": self.cancels, "types": { "continuous": self.types_continuous, "generator": self.types_generator, }, "collects_event_data": self.collects_event_data, "trigger_after": self.trigger_after, "trigger_only_on_success": self.trigger_only_on_success, "trigger_mode": self.trigger_mode, "show_api": self.show_api, "zerogpu": self.zero_gpu, } 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 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. """ value = update_dict.pop("value", components._Keywords.NO_VALUE) update_dict = {k: getattr(block, k) for k in update_dict if hasattr(block, k)} if value is not components._Keywords.NO_VALUE: if postprocess: update_dict["value"] = block.postprocess(value) if isinstance(update_dict["value"], (GradioModel, GradioRootModel)): update_dict["value"] = update_dict["value"].model_dump() else: update_dict["value"] = value update_dict["__type__"] = "update" return update_dict def convert_component_dict_to_list( outputs_ids: list[int], predictions: dict ) -> list | dict: """ Converts a dictionary of component updates into a list of updates in the order of the outputs_ids and including every output component. Leaves other types of dictionaries unchanged. E.g. {"textbox": "hello", "number": {"__type__": "generic_update", "value": "2"}} Into -> ["hello", {"__type__": "generic_update"}, {"__type__": "generic_update", "value": "2"}] """ keys_are_blocks = [isinstance(key, Block) for key in predictions] if all(keys_are_blocks): reordered_predictions = [skip() for _ in outputs_ids] for component, value in predictions.items(): if component._id not in outputs_ids: raise ValueError( f"Returned component {component} not specified as output of function." ) output_index = outputs_ids.index(component._id) reordered_predictions[output_index] = value predictions = utils.resolve_singleton(reordered_predictions) elif any(keys_are_blocks): raise ValueError( "Returned dictionary included some keys as Components. Either all keys must be Components to assign Component values, or return a List of values to assign output values in order." ) return predictions class BlocksConfig: def __init__(self, root_block: Blocks): self._id: int = 0 self.root_block = root_block self.blocks: dict[int, Component | Block] = {} self.fns: list[BlockFunction] = [] def get_config(self, renderable: Renderable | None = None): config = {} rendered_ids = [] def get_layout(block: Block): rendered_ids.append(block._id) if not isinstance(block, BlockContext): return {"id": block._id} children_layout = [] for child in block.children: children_layout.append(get_layout(child)) return {"id": block._id, "children": children_layout} if renderable: root_block = self.blocks[renderable.column_id] else: root_block = self.root_block config["layout"] = get_layout(root_block) config["components"] = [] for _id, block in self.blocks.items(): if renderable: if _id not in rendered_ids: continue if block.key: block.key = f"{renderable._id}-{block.key}" props = block.get_config() if hasattr(block, "get_config") else {} block_config = { "id": _id, "type": block.get_block_name(), "props": utils.delete_none(props), "skip_api": block.skip_api, "component_class_id": getattr(block, "component_class_id", None), "key": block.key, } if renderable: block_config["renderable"] = renderable._id if not block.skip_api: block_config["api_info"] = block.api_info() # type: ignore # .example_inputs() has been renamed .example_payload() but # we use the old name for backwards compatibility with custom components # created on Gradio 4.20.0 or earlier block_config["example_inputs"] = block.example_inputs() # type: ignore config["components"].append(block_config) config["dependencies"] = [fn.get_config() for fn in self.fns] return config def __copy__(self): new = BlocksConfig(self.root_block) new.blocks = copy.copy(self.blocks) new.fns = copy.copy(self.fns) return new @document("launch", "queue", "integrate", "load", "unload") class Blocks(BlockContext, BlocksEvents, metaclass=BlocksMeta): """ Blocks is Gradio's low-level API that allows you to create more custom web applications and demos than Interfaces (yet still entirely in Python). Compared to the Interface class, Blocks offers more flexibility and control over: (1) the layout of components (2) the events that trigger the execution of functions (3) data flows (e.g. inputs can trigger outputs, which can trigger the next level of outputs). Blocks also offers ways to group together related demos such as with tabs. The basic usage of Blocks is as follows: create a Blocks object, then use it as a context (with the "with" statement), and then define layouts, components, or events within the Blocks context. Finally, call the launch() method to launch the demo. Example: import gradio as gr def update(name): return f"Welcome to Gradio, {name}!" with gr.Blocks() as demo: gr.Markdown("Start typing below and then click **Run** to see the output.") with gr.Row(): inp = gr.Textbox(placeholder="What is your name?") out = gr.Textbox() btn = gr.Button("Run") btn.click(fn=update, inputs=inp, outputs=out) demo.launch() Demos: blocks_hello, blocks_flipper, blocks_kinematics Guides: blocks-and-event-listeners, controlling-layout, state-in-blocks, custom-CSS-and-JS, using-blocks-like-functions """ def __init__( self, theme: Theme | str | None = None, analytics_enabled: bool | None = None, mode: str = "blocks", title: str = "Gradio", css: str | None = None, js: str | None = None, head: str | None = None, fill_height: bool = False, delete_cache: tuple[int, int] | None = None, **kwargs, ): """ Parameters: theme: A Theme object or a string representing a theme. If a string, will look for a built-in theme with that name (e.g. "soft" or "default"), or will attempt to load a theme from the Hugging Face Hub (e.g. "gradio/monochrome"). If None, will use the Default theme. analytics_enabled: Whether to allow basic telemetry. If None, will use GRADIO_ANALYTICS_ENABLED environment variable or default to True. mode: A human-friendly name for the kind of Blocks or Interface being created. Used internally for analytics. title: The tab title to display when this is opened in a browser window. css: Custom css as a string or path to a css file. This css will be included in the demo webpage. js: Custom js as a string or path to a js file. The custom js should be in the form of a single js function. This function will automatically be executed when the page loads. For more flexibility, use the head parameter to insert js inside