adf0d17497
publish / version_or_publish (push) Has been cancelled
storybook-build / changes (push) Has been cancelled
storybook-build / :storybook-build (push) Has been cancelled
Sync Gradio Skills to Hugging Face / sync-skills (push) Has been cancelled
functional / changes (push) Has been cancelled
functional / build-frontend (push) Has been cancelled
functional / functional-test-SSR=false (push) Has been cancelled
functional / functional-reload (push) Has been cancelled
js / changes (push) Has been cancelled
js / js-test (push) Has been cancelled
docs-build / changes (push) Has been cancelled
docs-build / docs-build (push) Has been cancelled
docs-build / website-build (push) Has been cancelled
functional / functional-test-SSR=true (push) Has been cancelled
hygiene / hygiene-test (push) Has been cancelled
python / changes (push) Has been cancelled
python / build (push) Has been cancelled
python / test-ubuntu-latest-flaky (push) Has been cancelled
python / test-ubuntu-latest-not-flaky (push) Has been cancelled
python / test-windows-latest-flaky (push) Has been cancelled
python / test-windows-latest-not-flaky (push) Has been cancelled
484 lines
14 KiB
Python
484 lines
14 KiB
Python
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
import inspect
|
|
import json
|
|
import re
|
|
import shutil
|
|
import textwrap
|
|
from pathlib import Path
|
|
from typing import Literal
|
|
|
|
from huggingface_hub import snapshot_download
|
|
|
|
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",
|
|
),
|
|
"Walkthrough": ComponentFiles(
|
|
template="Walkthrough",
|
|
js_dir="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)
|
|
"""
|
|
),
|
|
),
|
|
"Step": ComponentFiles(
|
|
template="Step",
|
|
js_dir="tabitem",
|
|
python_file_name="walkthrough.py",
|
|
demo_code=textwrap.dedent(
|
|
"""
|
|
with gr.Blocks() as demo:
|
|
with {name}():
|
|
gr.Textbox(value="foo", interactive=True)
|
|
"""
|
|
),
|
|
),
|
|
}
|
|
|
|
|
|
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 copy_svelte_to_deps(package_json: dict):
|
|
svelte_version = package_json.get("peerDependencies", {}).get("svelte", "latest")
|
|
package_json["dependencies"]["svelte"] = svelte_version
|
|
return package_json
|
|
|
|
|
|
def _modify_js_deps(
|
|
package_json: dict,
|
|
key: Literal["dependencies", "devDependencies"],
|
|
gradio_dir: Path,
|
|
):
|
|
for dep in package_json.get(key, []):
|
|
# if current 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" / gradio.__version__
|
|
)
|
|
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 _download_from_hub(destination: Path):
|
|
version = gradio.__version__
|
|
|
|
snapshot_download(
|
|
repo_id="gradio/frontend",
|
|
allow_patterns=f"{version}/**",
|
|
local_dir=destination,
|
|
repo_type="dataset",
|
|
)
|
|
|
|
|
|
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
|
|
|
|
component_source = p / "_frontend_code"
|
|
if not component_source.exists():
|
|
component_source.mkdir(exist_ok=True)
|
|
_download_from_hub(component_source)
|
|
|
|
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
|
|
|
|
# Replace once we figure out bug with svelte-package
|
|
shutil.copytree(
|
|
str(p / "_frontend_code" / gradio.__version__ / 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)
|
|
source_package_json = copy_svelte_to_deps(source_package_json)
|
|
|
|
(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",
|
|
)
|
|
shutil.copy(
|
|
str(Path(__file__).parent / "files" / "tsconfig.json"),
|
|
frontend / "tsconfig.json",
|
|
)
|
|
|
|
|
|
def _replace_old_class_name(old_class_name: str, new_class_name: str, content: str):
|
|
pattern = rf"(?<=\b)(?<!\bimport\s)(?<!\.){re.escape(old_class_name)}(?=\b)"
|
|
return re.sub(pattern, new_class_name, content)
|
|
|
|
|
|
def _strip_document_lines(content: str):
|
|
return "\n".join(
|
|
[line for line in content.split("\n") if not line.startswith("@document(")]
|
|
)
|
|
|
|
|
|
def _create_backend(
|
|
name: str, component: ComponentFiles, directory: Path, package_name: str
|
|
):
|
|
def find_template_in_list(template, list_to_search):
|
|
for item in list_to_search:
|
|
if template.lower() == item.lower():
|
|
return item
|
|
return None
|
|
|
|
lists_to_search = [
|
|
(gradio.components.__all__, "components"),
|
|
(gradio.layouts.__all__, "layouts"),
|
|
(gradio._simple_templates.__all__, "_simple_templates"), # type: ignore
|
|
]
|
|
|
|
correct_cased_template = None
|
|
module = None
|
|
|
|
for list_, module_name in lists_to_search:
|
|
correct_cased_template = find_template_in_list(component.template, list_)
|
|
if correct_cased_template:
|
|
module = module_name
|
|
break
|
|
|
|
if not correct_cased_template:
|
|
raise ValueError(
|
|
f"Cannot find {component.template} in gradio.components, gradio.layouts, or gradio._simple_templates. "
|
|
"Please pass in a valid component name via the --template option. It must match the name of the python class."
|
|
)
|
|
|
|
if not module:
|
|
raise ValueError("Module not found")
|
|
|
|
# These README contents are used to install the component but they are overwritten later
|
|
readme_contents = textwrap.dedent(
|
|
"""
|
|
# {package_name}
|
|
A Custom Gradio component.
|
|
|
|
## Example usage
|
|
|
|
```python
|
|
import gradio as gr
|
|
from {package_name} import {name}
|
|
```
|
|
"""
|
|
).format(package_name=package_name, name=name)
|
|
(directory / "README.md").write_text(readme_contents)
|
|
|
|
backend = directory / "backend" / package_name
|
|
backend.mkdir(exist_ok=True, parents=True)
|
|
|
|
gitignore = Path(__file__).parent / "files" / "gitignore"
|
|
gitignore_contents = gitignore.read_text()
|
|
gitignore_dest = directory / ".gitignore"
|
|
gitignore_dest.write_text(gitignore_contents)
|
|
|
|
pyproject = Path(__file__).parent / "files" / "pyproject_.toml"
|
|
pyproject_contents = pyproject.read_text()
|
|
pyproject_dest = directory / "pyproject.toml"
|
|
pyproject_contents = pyproject_contents.replace("<<name>>", package_name).replace(
|
|
"<<template>>", PATTERN.format(template=correct_cased_template)
|
|
)
|
|
pyproject_dest.write_text(pyproject_contents)
|
|
|
|
demo_dir = directory / "demo"
|
|
demo_dir.mkdir(exist_ok=True, parents=True)
|
|
|
|
(demo_dir / "app.py").write_text(
|
|
f"""
|
|
import gradio as gr
|
|
from {package_name} import {name}
|
|
|
|
{component.demo_code.format(name=name)}
|
|
|
|
if __name__ == "__main__":
|
|
demo.launch()
|
|
"""
|
|
)
|
|
(demo_dir / "__init__.py").touch()
|
|
|
|
init = backend / "__init__.py"
|
|
init.write_text(
|
|
f"""
|
|
from .{name.lower()} import {name}
|
|
|
|
__all__ = ['{name}']
|
|
"""
|
|
)
|
|
|
|
p = Path(inspect.getfile(gradio)).parent
|
|
python_file = backend / f"{name.lower()}.py"
|
|
|
|
shutil.copy(
|
|
str(p / module / component.python_file_name),
|
|
str(python_file),
|
|
)
|
|
|
|
source_pyi_file = p / module / component.python_file_name.replace(".py", ".pyi")
|
|
pyi_file = backend / f"{name.lower()}.pyi"
|
|
if source_pyi_file.exists():
|
|
shutil.copy(str(source_pyi_file), str(pyi_file))
|
|
|
|
content = python_file.read_text()
|
|
content = _replace_old_class_name(correct_cased_template, name, content)
|
|
content = _strip_document_lines(content)
|
|
python_file.write_text(content)
|
|
if pyi_file.exists():
|
|
pyi_content = pyi_file.read_text()
|
|
pyi_content = _replace_old_class_name(correct_cased_template, name, content)
|
|
pyi_content = _strip_document_lines(pyi_content)
|
|
pyi_file.write_text(pyi_content)
|