from __future__ import annotations import dataclasses import inspect import json import re import shutil import textwrap from pathlib import Path from typing import Literal import gradio def _in_test_dir(): """Check if the current working directory ends with gradio/js/preview/test.""" return Path.cwd().parts[-4:] == ("gradio", "js", "preview", "test") default_demo_code = """ example = {name}().example_value() demo = gr.Interface( lambda x:x, {name}(), # interactive version of your component {name}(), # static version of your component # examples=[[example]], # uncomment this line to view the "example version" of your component ) """ static_only_demo_code = """ example = {name}().example_value() with gr.Blocks() as demo: with gr.Row(): {name}(label="Blank"), # blank component {name}(value=example, label="Populated"), # populated component """ layout_demo_code = """ with gr.Blocks() as demo: with {name}(): gr.Textbox(value="foo", interactive=True) gr.Number(value=10, interactive=True) """ fallback_code = """ with gr.Blocks() as demo: gr.Markdown("# Change the value (keep it JSON) and the front-end will update automatically.") {name}(value={{"message": "Hello from Gradio!"}}, label="Static") """ PATTERN_RE = r"gradio-template-\w+" PATTERN = "gradio-template-{template}" @dataclasses.dataclass class ComponentFiles: template: str demo_code: str = default_demo_code python_file_name: str = "" js_dir: str = "" def __post_init__(self): self.js_dir = self.js_dir or self.template.lower() self.python_file_name = self.python_file_name or f"{self.template.lower()}.py" OVERRIDES = { "AnnotatedImage": ComponentFiles( template="AnnotatedImage", python_file_name="annotated_image.py", demo_code=static_only_demo_code, ), "HighlightedText": ComponentFiles( template="HighlightedText", python_file_name="highlighted_text.py", demo_code=static_only_demo_code, ), "Chatbot": ComponentFiles(template="Chatbot", demo_code=static_only_demo_code), "Gallery": ComponentFiles(template="Gallery", demo_code=static_only_demo_code), "HTML": ComponentFiles(template="HTML", demo_code=static_only_demo_code), "Label": ComponentFiles(template="Label", demo_code=static_only_demo_code), "Markdown": ComponentFiles(template="Markdown", demo_code=static_only_demo_code), "Fallback": ComponentFiles(template="Fallback", demo_code=fallback_code), "Plot": ComponentFiles(template="Plot", demo_code=static_only_demo_code), "BarPlot": ComponentFiles( template="BarPlot", python_file_name="native_plot.py", js_dir="plot", demo_code=static_only_demo_code, ), "ClearButton": ComponentFiles( template="ClearButton", python_file_name="clear_button.py", js_dir="button", demo_code=static_only_demo_code, ), "ColorPicker": ComponentFiles( template="ColorPicker", python_file_name="color_picker.py" ), "DuplicateButton": ComponentFiles( template="DuplicateButton", python_file_name="duplicate_button.py", js_dir="button", demo_code=static_only_demo_code, ), "FileExplorer": ComponentFiles( template="FileExplorer", python_file_name="file_explorer.py", js_dir="fileexplorer", demo_code=textwrap.dedent( """ import os with gr.Blocks() as demo: {name}(value=os.path.dirname(__file__).split(os.sep)) """ ), ), "LinePlot": ComponentFiles( template="LinePlot", python_file_name="native_plot.py", js_dir="plot", demo_code=static_only_demo_code, ), "LogoutButton": ComponentFiles( template="LogoutButton", python_file_name="logout_button.py", js_dir="button", demo_code=static_only_demo_code, ), "LoginButton": ComponentFiles( template="LoginButton", python_file_name="login_button.py", js_dir="button", demo_code=static_only_demo_code, ), "ScatterPlot": ComponentFiles( template="ScatterPlot", python_file_name="native_plot.py", js_dir="plot", demo_code=static_only_demo_code, ), "UploadButton": ComponentFiles( template="UploadButton", python_file_name="upload_button.py", demo_code=static_only_demo_code, ), "JSON": ComponentFiles( template="JSON", python_file_name="json_component.py", demo_code=static_only_demo_code, ), "Row": ComponentFiles( template="Row", demo_code=layout_demo_code, ), "Column": ComponentFiles( template="Column", demo_code=layout_demo_code, ), "Tabs": ComponentFiles( template="Tabs", demo_code=textwrap.dedent( """ with gr.Blocks() as demo: with {name}(): with gr.Tab("Tab 1"): gr.Textbox(value="foo", interactive=True) with gr.Tab("Tab 2"): gr.Number(value=10, interactive=True) """ ), ), "Group": ComponentFiles( template="Group", demo_code=layout_demo_code, ), "Accordion": ComponentFiles( template="Accordion", demo_code=textwrap.dedent( """ with gr.Blocks() as demo: with {name}(label="Accordion"): gr.Textbox(value="foo", interactive=True) gr.Number(value=10, interactive=True) """ ), ), "Model3D": ComponentFiles( template="Model3D", js_dir="model3D", demo_code=textwrap.dedent( """ with gr.Blocks() as demo: {name}() """ ), ), "ImageEditor": ComponentFiles( template="ImageEditor", python_file_name="image_editor.py", js_dir="imageeditor", ), "MultimodalTextbox": ComponentFiles( template="MultimodalTextbox", python_file_name="multimodal_textbox.py", js_dir="multimodaltextbox", ), "DownloadButton": ComponentFiles( template="DownloadButton", python_file_name="download_button.py", js_dir="downloadbutton", ), } def _get_component_code(template: str | None) -> ComponentFiles: template = template or "Fallback" if template in OVERRIDES: return OVERRIDES[template] else: return ComponentFiles( python_file_name=f"{template.lower()}.py", js_dir=template.lower(), template=template, ) def _get_js_dependency_version(name: str, local_js_dir: Path) -> str: package_json = json.loads( Path(local_js_dir / name.split("/")[1] / "package.json").read_text() ) return package_json["version"] def _modify_js_deps( package_json: dict, key: Literal["dependencies", "devDependencies"], gradio_dir: Path, ): for dep in package_json.get(key, []): # if curent working directory is the gradio repo, use the local version of the dependency' if not _in_test_dir() and dep.startswith("@gradio/"): package_json[key][dep] = _get_js_dependency_version( dep, gradio_dir / "_frontend_code" ) return package_json def delete_contents(directory: str | Path) -> None: """Delete all contents of a directory, but not the directory itself.""" path = Path(directory) for child in path.glob("*"): if child.is_file(): child.unlink() elif child.is_dir(): shutil.rmtree(child) def _create_frontend( name: str, # noqa: ARG001 component: ComponentFiles, directory: Path, package_name: str, ): frontend = directory / "frontend" frontend.mkdir(exist_ok=True) p = Path(inspect.getfile(gradio)).parent def ignore(_src, names): ignored = [] for n in names: if ( n.startswith("CHANGELOG") or n.startswith("README.md") or ".test." in n or ".stories." in n or ".spec." in n ): ignored.append(n) return ignored shutil.copytree( str(p / "_frontend_code" / component.js_dir), frontend, dirs_exist_ok=True, ignore=ignore, ) source_package_json = json.loads(Path(frontend / "package.json").read_text()) source_package_json["name"] = package_name source_package_json = _modify_js_deps(source_package_json, "dependencies", p) source_package_json = _modify_js_deps(source_package_json, "devDependencies", p) (frontend / "package.json").write_text(json.dumps(source_package_json, indent=2)) shutil.copy( str(Path(__file__).parent / "files" / "gradio.config.js"), frontend / "gradio.config.js", ) def _replace_old_class_name(old_class_name: str, new_class_name: str, content: str): pattern = rf"(?<=\b)(?>", package_name).replace( "<