Sharing themes (#3428)

* Rebase

* Remove build hooks

* Working implementation

* Add semver + unit tests

* CHANGELOG

* Add to docs

* Rename push_to_hub and fix typos

* Fix gallery

* Fix typo

* Address comments + tests

* Update gradio/themes/app.py

Co-authored-by: Abubakar Abid <abubakar@huggingface.co>

* Import Base as Theme. Use DefaultTheme() as fallback

* Fix types

* Make version and token truly optional

* Add version dropdown + tests

* trigger

* Support private themes and org_names

* Fix org_name typo

* Update wheel

* Fix font loading and dumping

* fixing tests

* fix tests

* formatting

* version

* remove requirements

* remove requirements

* formatting

* fix tests

---------

Co-authored-by: Abubakar Abid <abubakar@huggingface.co>
This commit is contained in:
Freddy Boulton 2023-03-18 23:15:02 -04:00 committed by GitHub
parent 27be008d58
commit 8ec2b0b98a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1129 additions and 12 deletions

12
.vscode/settings.json vendored
View File

@ -9,5 +9,15 @@
// Use Pollen's inbuilt variable ordering
"cssvar.disableSort": true,
// Add support for autocomplete in other file types
"cssvar.extensions": ["js", "css", "html", "jsx", "tsx", "svelte"]
"cssvar.extensions": [
"js",
"css",
"html",
"jsx",
"tsx",
"svelte"
],
"python.analysis.extraPaths": [
"./gradio/themes/utils"
]
}

View File

@ -111,6 +111,74 @@ No changes to highlight.
## New Features:
### Theme Sharing 🎨 🤝
You can now share your gradio themes with the world!
After creating a theme, you can upload it to the HuggingFace Hub to let others view it, use it, and build off of it!
### Uploading
There are two ways to upload a theme, via the theme class instance or the command line.
1. Via the class instance
```python
my_theme.push_to_hub(repo_name="my_theme",
version="0.2.0",
hf_token="...")
```
2. Via the command line
First save the theme to disk
```python
my_theme.dump(filename="my_theme.json")
```
Then use the `upload_theme` command:
```bash
upload_theme\
"my_theme.json"\
"my_theme"\
"0.2.0"\
"<hf-token>"
```
The `version` must be a valid [semantic version](https://www.geeksforgeeks.org/introduction-semantic-versioning/) string.
This creates a space on the huggingface hub to host the theme files and show potential users a preview of your theme.
An example theme space is here: https://huggingface.co/spaces/freddyaboulton/dracula_revamped
### Downloading
To use a theme from the hub, use the `from_hub` method on the `ThemeClass` and pass it to your app:
```python
my_theme = gr.Theme.from_hub("freddyaboulton/my_theme")
with gr.Blocks(theme=my_theme) as demo:
....
```
You can also pass the theme string directly to `Blocks` or `Interface` (`gr.Blocks(theme="freddyaboulton/my_theme")`)
You can pin your app to an upstream theme version by using semantic versioning expressions.
For example, the following would ensure the theme we load from the `my_theme` repo was between versions `0.1.0` and `0.2.0`:
```python
with gr.Blocks(theme="freddyaboulton/my_theme@>=0.1.0,<0.2.0") as demo:
....
```
by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3428](https://github.com/gradio-app/gradio/pull/3428)
### Code component 🦾
New code component allows you to enter, edit and display code with full syntax highlighting by [@pngwn](https://github.com/pngwn) in [PR 3421](https://github.com/gradio-app/gradio/pull/3421)
### The `Chatbot` component now supports audio, video, and images
The `Chatbot` component now supports audio, video, and images with a simple syntax: simply

View File

@ -85,6 +85,7 @@ from gradio.templates import (
TextArea,
Webcam,
)
from gradio.themes import Base as Theme
current_pkg_version = (
(pkgutil.get_data(__name__, "version.txt") or b"").decode("ascii").strip()

View File

@ -477,7 +477,7 @@ class Blocks(BlockContext):
def __init__(
self,
theme: Theme | None = None,
theme: Theme | str | None = None,
analytics_enabled: bool | None = None,
mode: str = "blocks",
title: str = "Gradio",
@ -496,7 +496,13 @@ class Blocks(BlockContext):
self.save_to = None
if theme is None:
theme = DefaultTheme()
elif not isinstance(theme, Theme):
elif isinstance(theme, str):
try:
theme = Theme.from_hub(theme)
except Exception as e:
warnings.warn(f"Cannot load {theme}. Caught Exception: {str(e)}")
theme = DefaultTheme()
if not isinstance(theme, Theme):
warnings.warn("Theme should be a class loaded from gradio.themes")
theme = DefaultTheme()
self.theme = theme

View File

@ -3705,7 +3705,7 @@ class JSON(Changeable, IOComponent, JSONSerializable):
def __init__(
self,
value: str | Callable | None = None,
value: str | Dict | List | Callable | None = None,
*,
label: str | None = None,
every: float | None = None,
@ -3866,7 +3866,7 @@ class Gallery(IOComponent, TempFileManager, FileSerializable, Selectable):
def __init__(
self,
value: List[np.ndarray | _Image.Image | str] | Callable | None = None,
value: List[np.ndarray | _Image.Image | str | Tuple] | Callable | None = None,
*,
label: str | None = None,
every: float | None = None,

147
gradio/themes/app.py Normal file
View File

@ -0,0 +1,147 @@
import time
from theme_dropdown import create_theme_dropdown # noqa: F401
import gradio as gr
dropdown, js = create_theme_dropdown()
with gr.Blocks(theme=gr.themes.Default()) as demo:
with gr.Row().style(equal_height=True):
with gr.Column(scale=10):
gr.Markdown(
"""
# Theme preview: `{THEME}`
To use this theme, set `theme='{AUTHOR}/{SPACE_NAME}'` in `gr.Blocks()` or `gr.Interface()`.
You can append an `@` and a semantic version expression, e.g. @>=1.0.0,<2.0.0 to pin to a given version
of this theme.
"""
)
with gr.Column(scale=3):
with gr.Box():
dropdown.render()
toggle_dark = gr.Button(value="Toggle Dark").style(full_width=True)
dropdown.change(None, dropdown, None, _js=js)
toggle_dark.click(
None,
_js="""
() => {
document.body.classList.toggle('dark');
document.querySelector('gradio-app').style.backgroundColor = 'var(--color-background-primary)'
}
""",
)
name = gr.Textbox(
label="Name",
info="Full name, including middle name. No special characters.",
placeholder="John Doe",
value="John Doe",
interactive=True,
)
with gr.Row():
slider1 = gr.Slider(label="Slider 1")
slider2 = gr.Slider(label="Slider 2")
gr.CheckboxGroup(["A", "B", "C"], label="Checkbox Group")
with gr.Row():
with gr.Column(variant="panel", scale=1):
gr.Markdown("## Panel 1")
radio = gr.Radio(
["A", "B", "C"],
label="Radio",
info="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
)
drop = gr.Dropdown(["Option 1", "Option 2", "Option 3"], show_label=False)
drop_2 = gr.Dropdown(
["Option A", "Option B", "Option C"],
multiselect=True,
value=["Option A"],
label="Dropdown",
interactive=True,
)
check = gr.Checkbox(label="Go")
with gr.Column(variant="panel", scale=2):
img = gr.Image(
"https://gradio.app/assets/img/header-image.jpg", label="Image"
).style(height=320)
with gr.Row():
go_btn = gr.Button("Go", label="Primary Button", variant="primary")
clear_btn = gr.Button(
"Clear", label="Secondary Button", variant="secondary"
)
def go(*args):
time.sleep(3)
return "https://gradio.app/assets/img/header-image.jpg"
go_btn.click(go, [radio, drop, drop_2, check, name], img, api_name="go")
def clear():
time.sleep(0.2)
return None
clear_btn.click(clear, None, img)
with gr.Row():
btn1 = gr.Button("Button 1").style(size="sm")
btn2 = gr.UploadButton().style(size="sm")
stop_btn = gr.Button("Stop", label="Stop Button", variant="stop").style(
size="sm"
)
with gr.Row():
gr.Dataframe(value=[[1, 2, 3], [4, 5, 6], [7, 8, 9]], label="Dataframe")
gr.JSON(
value={"a": 1, "b": 2, "c": {"test": "a", "test2": [1, 2, 3]}}, label="JSON"
)
gr.Label(value={"cat": 0.7, "dog": 0.2, "fish": 0.1})
gr.File()
with gr.Row():
gr.ColorPicker()
gr.Video("https://gradio-static-files.s3.us-west-2.amazonaws.com/world.mp4")
gr.Gallery(
[
(
"https://gradio-static-files.s3.us-west-2.amazonaws.com/lion.jpg",
"lion",
),
(
"https://gradio-static-files.s3.us-west-2.amazonaws.com/logo.png",
"logo",
),
(
"https://gradio-static-files.s3.us-west-2.amazonaws.com/tower.jpg",
"tower",
),
]
).style(height="200px", grid=2)
with gr.Row():
with gr.Column(scale=2):
chatbot = gr.Chatbot([("Hello", "Hi")], label="Chatbot")
chat_btn = gr.Button("Add messages")
def chat(history):
time.sleep(2)
yield [["How are you?", "I am good."]]
chat_btn.click(
lambda history: history
+ [["How are you?", "I am good."]]
+ (time.sleep(2) or []),
chatbot,
chatbot,
)
with gr.Column(scale=1):
with gr.Accordion("Advanced Settings"):
gr.Markdown("Hello")
gr.Number(label="Chatbot control 1")
gr.Number(label="Chatbot control 2")
gr.Number(label="Chatbot control 3")
if __name__ == "__main__":
demo.queue().launch()

View File

@ -1,9 +1,28 @@
from __future__ import annotations
import json
import re
from typing import Iterable
import tempfile
import textwrap
from pathlib import Path
from typing import Dict, Iterable
from gradio.themes.utils import colors, fonts, sizes
import huggingface_hub
import requests
import semantic_version as semver
from huggingface_hub import CommitOperationAdd
from gradio.documentation import document, set_documentation_group
from gradio.themes.utils import (
colors,
fonts,
get_matching_version,
get_theme_assets,
sizes,
)
from gradio.themes.utils.readme_content import README_CONTENT
set_documentation_group("themes")
class ThemeClass:
@ -74,7 +93,232 @@ class ThemeClass:
return css_code + "\n" + dark_css_code
def to_dict(self):
"""Convert the theme into a python dictionary."""
schema = {"theme": {}}
for prop in dir(self):
if (
not prop.startswith("_")
or prop.startswith("_font")
or prop == "_stylesheets"
) and isinstance(getattr(self, prop), (list, str)):
schema["theme"][prop] = getattr(self, prop)
return schema
@classmethod
def load(cls, path: str) -> "ThemeClass":
"""Load a theme from a json file.
Parameters:
path: The filepath to read.
"""
theme = json.load(open(path), object_hook=fonts.as_font)
return cls.from_dict(theme)
@classmethod
def from_dict(cls, theme: Dict[str, Dict[str, str]]) -> "ThemeClass":
"""Create a theme instance from a dictionary representation.
Parameters:
theme: The dictionary representation of the theme.
"""
base = cls()
for prop, value in theme["theme"].items():
setattr(base, prop, value)
return base
def dump(self, filename: str):
"""Write the theme to a json file.
Parameters:
filename: The path to write the theme too
"""
as_dict = self.to_dict()
json.dump(as_dict, open(Path(filename), "w"), cls=fonts.FontEncoder)
@classmethod
def from_hub(cls, repo_name: str, hf_token: str | None = None):
"""Load a theme from the hub.
This DOES NOT require a HuggingFace account for downloading publicly available themes.
Parameters:
repo_name: string of the form <author>/<theme-name>@<semantic-version-expression>. If a semantic version expression is omitted, the latest version will be fetched.
hf_token: HuggingFace Token. Only needed to download private themes.
"""
if "@" not in repo_name:
name, version = repo_name, None
else:
name, version = repo_name.split("@")
api = huggingface_hub.HfApi(token=hf_token)
try:
space_info = api.space_info(name)
except requests.HTTPError as e:
raise ValueError(f"The space {name} does not exist") from e
assets = get_theme_assets(space_info)
matching_version = get_matching_version(assets, version)
if not matching_version:
raise ValueError(
f"Cannot find a matching version for expression {version} "
f"from files {[f.filename for f in assets]}"
)
theme_file = huggingface_hub.hf_hub_download(
repo_id=name,
repo_type="space",
filename=f"themes/theme_schema@{matching_version.version}.json",
)
return cls.load(theme_file)
@staticmethod
def _get_next_version(space_info: huggingface_hub.hf_api.SpaceInfo) -> str:
assets = get_theme_assets(space_info)
print("assets", assets)
latest_version = max(assets, key=lambda asset: asset.version).version
return str(latest_version.next_patch())
@staticmethod
def _theme_version_exists(
space_info: huggingface_hub.hf_api.SpaceInfo, version: str
) -> bool:
assets = get_theme_assets(space_info)
return any(a.version == semver.Version(version) for a in assets)
def push_to_hub(
self,
repo_name: str,
org_name: str | None = None,
version: str | None = None,
hf_token: str | None = None,
theme_name: str | None = None,
description: str | None = None,
private: bool = False,
):
"""Upload a theme to the HuggingFace hub.
This requires a HuggingFace account.
Parameters:
repo_name: The name of the repository to store the theme assets, e.g. 'my_theme' or 'sunset'.
org_name: The name of the org to save the space in. If None (the default), the username corresponding to the logged in user, or hƒ_token is used.
version: A semantic version tag for theme. Bumping the version tag lets you publish updates to a theme without changing the look of applications that already loaded your theme.
hf_token: API token for your HuggingFace account
theme_name: Name for the name. If None, defaults to repo_name
description: A long form description to your theme.
"""
from gradio import __version__
api = huggingface_hub.HfApi()
if not hf_token:
try:
author = huggingface_hub.whoami()["name"]
except OSError as e:
raise ValueError(
"In order to push to hub, log in via `huggingface-cli login` "
"or provide a theme_token to push_to_hub. For more information "
"see https://huggingface.co/docs/huggingface_hub/quick-start#login"
) from e
else:
author = huggingface_hub.whoami(token=hf_token)["name"]
space_id = f"{org_name or author}/{repo_name}"
try:
space_info = api.space_info(space_id)
except requests.HTTPError:
space_info = None
space_exists = space_info is not None
# If no version, set the version to next patch release
if not version:
if space_exists:
version = self._get_next_version(space_info)
else:
version = "0.0.1"
else:
_ = semver.Version(version)
if space_exists and self._theme_version_exists(space_info, version):
raise ValueError(
f"The space {space_id} already has a "
f"theme with version {version}. See: themes/theme_schema@{version}.json. "
"To manually override this version, use the HuggingFace hub UI."
)
theme_name = theme_name or repo_name
with tempfile.NamedTemporaryFile(
mode="w", delete=False, suffix=".json"
) as css_file:
contents = self.to_dict()
contents["version"] = version
json.dump(contents, css_file, cls=fonts.FontEncoder)
with tempfile.NamedTemporaryFile(mode="w", delete=False) as readme_file:
readme_content = README_CONTENT.format(
theme_name=theme_name,
description=description or "Add a description of this theme here!",
author=author,
gradio_version=__version__,
)
readme_file.write(textwrap.dedent(readme_content))
with tempfile.NamedTemporaryFile(mode="w", delete=False) as app_file:
contents = open(str(Path(__file__).parent / "app.py")).read()
contents = re.sub(
r"theme=gr.themes.Default\(\)",
f"theme='{space_id}'",
contents,
)
contents = re.sub(r"{THEME}", theme_name or repo_name, contents)
contents = re.sub(r"{AUTHOR}", org_name or author, contents)
contents = re.sub(r"{SPACE_NAME}", repo_name, contents)
app_file.write(contents)
operations = [
CommitOperationAdd(
path_in_repo=f"themes/theme_schema@{version}.json",
path_or_fileobj=css_file.name,
),
CommitOperationAdd(
path_in_repo="README.md", path_or_fileobj=readme_file.name
),
CommitOperationAdd(path_in_repo="app.py", path_or_fileobj=app_file.name),
CommitOperationAdd(
path_in_repo="theme_dropdown.py",
path_or_fileobj=str(
Path(__file__).parent / "utils" / "theme_dropdown.py"
),
),
]
huggingface_hub.create_repo(
space_id,
repo_type="space",
space_sdk="gradio",
token=hf_token,
exist_ok=True,
private=private,
)
api.create_commit(
repo_id=space_id,
commit_message="Updating theme",
repo_type="space",
operations=operations,
token=hf_token,
)
url = f"https://huggingface.co/spaces/{space_id}"
print(f"See your theme here! {url}")
return url
@document("push_to_hub", "from_hub", "load", "dump", "from_dict", "to_dict")
class Base(ThemeClass):
def __init__(
self,
@ -1267,9 +1511,8 @@ class Base(ThemeClass):
self.stat_background_fill = stat_background_fill or getattr(
self, "stat_background_fill", "*primary_300"
)
self.stat_background_fill_dark = (
stat_background_fill_dark
or getattr(self, "stat_background_fill_dark", "*primary_500")
self.stat_background_fill_dark = stat_background_fill_dark or getattr(
self, "stat_background_fill_dark", "*primary_500"
)
self.table_border_color = table_border_color or getattr(
self, "table_border_color", "*neutral_300"

View File

@ -0,0 +1,59 @@
from __future__ import annotations
import argparse
from gradio.themes import ThemeClass
def main():
parser = argparse.ArgumentParser(description="Upload a demo to a space")
parser.add_argument("theme", type=str, help="Theme json file")
parser.add_argument("repo_name", type=str, help="HF repo name to store the theme")
parser.add_argument(
"--org_name",
type=str,
help="The name of the org to save the space in. If None (the default), the username corresponding to the logged in user, or hƒ_token is used.",
)
parser.add_argument("--version", type=str, help="Semver version")
parser.add_argument("--hf_token", type=str, help="HF Token")
parser.add_argument(
"--theme-name",
type=str,
help="Name of theme.",
)
parser.add_argument(
"--description",
type=str,
help="Description of theme",
)
args = parser.parse_args()
upload_theme(
args.theme,
args.repo_name,
args.org_name,
args.version,
args.hf_token,
args.theme_name,
args.description,
)
def upload_theme(
theme: str,
repo_name: str,
org_name: str | None = None,
version: str | None = None,
hf_token: str | None = None,
theme_name: str | None = None,
description: str | None = None,
):
theme = ThemeClass.load(theme)
return theme.push_to_hub(
repo_name=repo_name,
version=version,
hf_token=hf_token,
theme_name=theme_name,
description=description,
org_name=org_name,
)

View File

@ -1,3 +1,8 @@
from .colors import * # noqa: F401
from .fonts import * # noqa: F401
from .semver_match import ( # noqa: F401
ThemeAsset,
get_matching_version,
get_theme_assets,
)
from .sizes import * # noqa: F401

View File

@ -1,5 +1,26 @@
from __future__ import annotations
import json
class FontEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, Font):
return {
"__gradio_font__": True,
"name": obj.name,
"class": "google" if isinstance(obj, GoogleFont) else "font",
}
# Let the base class default method raise the TypeError
return json.JSONEncoder.default(self, obj)
def as_font(dct):
if "__gradio_font__" in dct:
name = dct["name"]
return GoogleFont(name) if dct["class"] == "google" else Font(name)
return dct
class Font:
def __init__(self, name: str):
@ -15,6 +36,9 @@ class Font:
def stylesheet(self) -> str:
return None
def __eq__(self, other: Font) -> bool:
return self.name == other.name and self.stylesheet() == other.stylesheet()
class GoogleFont(Font):
def stylesheet(self) -> str:

View File

@ -0,0 +1,18 @@
README_CONTENT = """
---
tags: [gradio-theme]
title: {theme_name}
colorFrom: orange
colorTo: purple
sdk: gradio
sdk_version: {gradio_version}
app_file: app.py
pinned: false
license: apache-2.0
---
# {theme_name}
## Description
{description}
## Contributions
Thanks to [@{author}](https://huggingface.co/{author}) for adding this gradio theme!
"""

View File

@ -0,0 +1,42 @@
from __future__ import annotations
from dataclasses import dataclass, field
from typing import List
import huggingface_hub
import semantic_version
import semantic_version as semver
@dataclass
class ThemeAsset:
filename: str
version: semver.Version = field(init=False)
def __post_init__(self):
self.version = semver.Version(self.filename.split("@")[1].replace(".json", ""))
def get_theme_assets(space_info: huggingface_hub.hf_api.SpaceInfo) -> List[ThemeAsset]:
if "gradio-theme" not in getattr(space_info, "tags", []):
raise ValueError(f"{space_info.id} is not a valid gradio-theme space!")
return [
ThemeAsset(filename.rfilename)
for filename in space_info.siblings
if filename.rfilename.startswith("themes/")
]
def get_matching_version(
assets: List[ThemeAsset], expression: str | None
) -> ThemeAsset | None:
expression = expression or "*"
# Return most recent version that matches
matching_version = semantic_version.SimpleSpec(expression).select(
[a.version for a in assets]
)
return next((a for a in assets if a.version == matching_version), None)

View File

@ -0,0 +1,57 @@
import os
import pathlib
from gradio.themes.utils import ThemeAsset
def create_theme_dropdown():
import gradio as gr
asset_path = pathlib.Path(__file__).parent / "themes"
themes = []
for theme_asset in os.listdir(str(asset_path)):
themes.append(
(ThemeAsset(theme_asset), gr.Theme.load(str(asset_path / theme_asset)))
)
def make_else_if(theme_asset):
return f"""
else if (theme == '{str(theme_asset[0].version)}') {{
var theme_css = `{theme_asset[1]._get_theme_css()}`
}}"""
head, tail = themes[0], themes[1:]
if_statement = f"""
if (theme == "{str(head[0].version)}") {{
var theme_css = `{head[1]._get_theme_css()}`
}} {" ".join(make_else_if(t) for t in tail)}
"""
latest_to_oldest = sorted([t[0] for t in themes], key=lambda asset: asset.version)[
::-1
]
latest_to_oldest = [str(t.version) for t in latest_to_oldest]
component = gr.Dropdown(
choices=latest_to_oldest,
value=latest_to_oldest[0],
render=False,
label="Select Version",
).style(container=False)
return (
component,
f"""
(theme) => {{
if (!document.querySelector('.theme-css')) {{
var theme_elem = document.createElement('style');
theme_elem.classList.add('theme-css');
document.head.appendChild(theme_elem);
}} else {{
var theme_elem = document.querySelector('.theme-css');
}}
{if_statement}
theme_elem.innerHTML = theme_css;
}}
""",
)

View File

@ -1 +1 @@
3.22.1
3.22.1b1

View File

@ -21,6 +21,7 @@ keywords = ["machine learning", "reproducibility", "visualization"]
[project.scripts]
gradio = "gradio.reload:run_in_reload_mode"
upload_theme = "gradio.themes.upload_theme:main"
[project.urls]
Homepage = "https://github.com/gradio-app/gradio"
@ -51,6 +52,7 @@ artifacts = [
"/gradio/templates",
]
[tool.hatch.build.targets.sdist]
include = [
"/gradio",

View File

@ -22,4 +22,6 @@ httpx
pydantic
websockets>=10.0
typing_extensions
aiofiles
aiofiles
huggingface_hub
semantic_version

View File

@ -382,6 +382,17 @@ class TestBlocksMethods:
finally:
server.close()
@patch(
"gradio.themes.ThemeClass.from_hub",
side_effect=ValueError("Something went wrong!"),
)
def test_use_default_theme_as_fallback(self, mock_from_hub):
with pytest.warns(
UserWarning, match="Cannot load freddyaboulton/this-theme-does-not-exist"
):
with gr.Blocks(theme="freddyaboulton/this-theme-does-not-exist") as demo:
assert demo.theme.to_dict() == gr.themes.Default().to_dict()
class TestComponentsInBlocks:
def test_slider_random_value_config(self):

399
test/test_theme_sharing.py Normal file
View File

@ -0,0 +1,399 @@
import tempfile
from unittest.mock import patch
import huggingface_hub
import pytest
from huggingface_hub.hf_api import SpaceInfo
import gradio as gr
from gradio.themes.utils import ThemeAsset, get_matching_version, get_theme_assets
versions = [
"0.1.0",
"0.1.1",
"0.1.2",
"0.1.3",
"0.4.4",
"0.5.0",
"0.7.0",
"0.9.2",
"0.9.3",
"0.9.4",
"0.9.5",
"0.9.6",
"0.9.7",
"0.9.8",
"2.2.0",
"2.2.1",
"2.2.10",
"2.2.11",
"2.2.12",
"2.2.13",
"2.2.14",
"2.2.15",
"2.2.2",
"2.2.3",
"2.2.4",
"2.2.5",
"2.2.6",
"2.2.7",
"2.2.8",
"3.0.1",
"3.0.10",
"3.0.11",
"3.0.12",
"3.0.13",
"3.0.14",
"3.0.15",
"3.0.16",
"3.0.17",
"3.0.18",
"3.0.19",
"3.0.2",
"3.0.20",
"3.0.20-dev0",
"3.0.21",
"3.0.22",
"3.0.23",
"3.0.23-dev1",
"3.0.24",
"3.0.25",
"3.0.26",
"3.0.3",
"3.0.4",
"3.0.5",
"3.0.6",
"3.0.7",
"3.0.8",
"3.0.9",
"3.1.0",
"3.1.1",
"3.1.2",
"3.1.3",
"3.1.4",
"3.1.5",
"3.1.6",
"3.1.7",
"3.10.0",
"3.10.1",
"3.11.0",
"3.12.0",
"3.13.0",
"3.13.1",
"3.13.2",
"3.14.0",
"3.15.0",
"3.16.0",
"3.16.1",
"3.16.2",
"3.17.0",
"3.17.1",
"3.18.0",
"3.18.1",
"3.18.1-dev0",
"3.18.2-dev0",
"3.18.2",
"3.19.0",
"3.19.1",
"3.20.0",
"3.20.1",
"3.3.1",
"3.4.1",
"3.8.1",
"3.8.2",
"3.9.1",
]
assets = [ThemeAsset(f"theme_schema@{version}") for version in versions]
dracula_gray = gr.themes.colors.Color(
*(
["#6272a4", "#7280ad", "#818eb6", "#919cbf", "#a1aac8"][::-1]
+ ["#586794", "#4e5b83", "#455073", "#3b4462", "#313952", "#272e42"]
)
)
dracula_pink = gr.themes.Color(
*(
[
"#ffd7ee",
"#ff79c6",
"#ff86cc",
"#ff94d1",
"#ffa1d7",
"#ffafdd",
"#ffbce3",
"#ffc9e8",
][::-1]
+ ["#e66db2", "#cc619e", "#b3558b"]
)
)
dracula_gray = gr.themes.colors.Color(
*(
["#6272a4", "#7280ad", "#818eb6", "#919cbf", "#a1aac8"][::-1]
+ ["#586794", "#4e5b83", "#455073", "#3b4462", "#313952", "#272e42"]
)
)
dracula = gr.themes.Base(
primary_hue=gr.themes.colors.pink,
neutral_hue=dracula_gray,
font=gr.themes.GoogleFont("Poppins"),
).set(
body_background_fill=dracula_gray.c500,
color_accent_soft=dracula_gray.c100,
background_fill_primary=dracula_gray.c500,
background_fill_secondary=dracula_gray.c500,
block_background_fill=dracula_gray.c300,
body_text_color="#f8f8f2",
body_text_color_dark="#f8f8f2",
body_text_color_subdued="#f8f8f2",
block_label_text_color="#f8f8f2",
block_label_text_color_dark="#f8f8f2",
table_even_background_fill=dracula_gray.c300,
border_color_accent=dracula_gray.c200,
block_info_text_color="#f8f8f2",
block_info_text_color_dark="#f8f8f2",
block_title_text_color="#f8f8f2",
block_title_text_color_dark="#f8f8f2",
checkbox_background_color_selected_dark="#ff79c6",
checkbox_background_color_selected="#ff79c6",
button_primary_background_fill_dark="#ff79c6",
button_primary_background_fill=dracula_pink.c300,
button_secondary_text_color="#f8f8f2",
slider_color_dark="#ff79c6",
slider_color=dracula_pink.c300,
panel_background_fill="#31395294",
block_background_fill_dark="#31395294",
)
class TestSemverMatch:
def test_simple_equality(self):
assert get_matching_version(assets, "3.10.0") == ThemeAsset(
"theme_schema@3.10.0"
)
def test_empty_expression_returns_latest(self):
assert get_matching_version(assets, None) == ThemeAsset("theme_schema@3.20.1")
def test_range(self):
assert get_matching_version(assets, ">=3.10.0,<3.15") == ThemeAsset(
"theme_schema@3.14.0"
)
def test_wildcard(self):
assert get_matching_version(assets, "2.2.*") == ThemeAsset(
"theme_schema@2.2.15"
)
def test_does_not_exist(self):
assert get_matching_version(assets, ">4.0.0") is None
def test_compatible_release_specifier(self):
assert get_matching_version(assets, "~=0.0") == ThemeAsset("theme_schema@0.9.8")
def test_breaks_ties_against_prerelease(self):
assert get_matching_version(assets, ">=3.18,<3.19") == ThemeAsset(
"theme_schema@3.18.2"
)
class TestGetThemeAssets:
def test_get_theme_assets(self):
space_info = huggingface_hub.hf_api.SpaceInfo(
id="freddyaboulton/dracula",
siblings=[
{
"blob_id": None,
"lfs": None,
"rfilename": "themes/theme_schema@0.1.0.json",
"size": None,
},
{
"blob_id": None,
"lfs": None,
"rfilename": "themes/theme_schema@0.1.1.json",
"size": None,
},
{
"blob_id": None,
"lfs": None,
"rfilename": "themes/theme_schema@0.2.5.json",
"size": None,
},
{
"blob_id": None,
"lfs": None,
"rfilename": "themes/theme_schema@1.5.9.json",
"size": None,
},
],
tags=["gradio-theme", "gradio"],
)
assert get_theme_assets(space_info) == [
ThemeAsset("themes/theme_schema@0.1.0.json"),
ThemeAsset("themes/theme_schema@0.1.1.json"),
ThemeAsset("themes/theme_schema@0.2.5.json"),
ThemeAsset("themes/theme_schema@1.5.9.json"),
]
assert gr.Theme._theme_version_exists(space_info, "0.1.1")
assert not gr.Theme._theme_version_exists(space_info, "2.0.0")
def test_raises_if_space_not_properly_tagged(self):
space_info = huggingface_hub.hf_api.SpaceInfo(
id="freddyaboulton/dracula", tags=["gradio"]
)
with pytest.raises(
ValueError,
match="freddyaboulton/dracula is not a valid gradio-theme space!",
):
with patch("huggingface_hub.HfApi.space_info", return_value=space_info):
get_theme_assets(space_info)
class TestThemeUploadDownload:
@patch("gradio.themes.base.get_theme_assets", return_value=assets)
def test_get_next_version(self, mock):
next_version = gr.themes.Base._get_next_version(
SpaceInfo(id="gradio/dracula_test")
)
assert next_version == "3.20.2"
@pytest.mark.flaky
def test_theme_download(self):
assert (
gr.themes.Base.from_hub("gradio/dracula_test@0.0.1").to_dict()
== dracula.to_dict()
)
with gr.Blocks(theme="gradio/dracula_test@0.0.1") as demo:
pass
assert demo.theme.to_dict() == dracula.to_dict()
def test_theme_download_raises_error_if_theme_does_not_exist(self):
with pytest.raises(
ValueError, match="The space freddyaboulton/nonexistent does not exist"
):
gr.themes.Base.from_hub("freddyaboulton/nonexistent").to_dict()
@patch("gradio.themes.base.huggingface_hub")
@patch("gradio.themes.base.Base._theme_version_exists", return_value=True)
def test_theme_upload_fails_if_duplicate_version(self, mock_1, mock_2):
with pytest.raises(ValueError, match="already has a theme with version 0.2.1"):
dracula.push_to_hub("dracula_revamped", version="0.2.1", hf_token="foo")
@patch("gradio.themes.base.huggingface_hub")
@patch("gradio.themes.base.huggingface_hub.HfApi")
def test_upload_fails_if_not_valid_semver(self, mock_1, mock_2):
with pytest.raises(ValueError, match="Invalid version string: '3.0'"):
dracula.push_to_hub("dracula_revamped", version="3.0", hf_token="s")
def test_dump_and_load(self):
with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as path:
dracula.dump(path.name)
assert gr.themes.Base.load(path.name).to_dict() == dracula.to_dict()
@patch("gradio.themes.base.Base._get_next_version", return_value="0.1.3")
@patch("gradio.themes.base.Base._theme_version_exists", return_value=False)
@patch("gradio.themes.base.huggingface_hub")
def test_version_and_token_optional(self, mock_1, mock_2, mock_3):
mock_1.whoami.return_value = {"name": "freddyaboulton"}
gr.themes.Monochrome().push_to_hub(repo_name="my_monochrome")
repo_call_args = mock_1.HfApi().create_commit.call_args_list[0][1]
assert repo_call_args["repo_id"] == "freddyaboulton/my_monochrome"
assert any(
o.path_in_repo == "themes/theme_schema@0.1.3.json"
for o in repo_call_args["operations"]
)
mock_1.whoami.assert_called_with()
@patch("gradio.themes.base.huggingface_hub")
def test_first_upload_no_version(self, mock_1):
mock_1.whoami.return_value = {"name": "freddyaboulton"}
mock_1.HfApi().space_info.side_effect = huggingface_hub.hf_api.HTTPError("Foo")
gr.themes.Monochrome().push_to_hub(repo_name="does_not_exist")
repo_call_args = mock_1.HfApi().create_commit.call_args_list[0][1]
assert repo_call_args["repo_id"] == "freddyaboulton/does_not_exist"
assert any(
o.path_in_repo == "themes/theme_schema@0.0.1.json"
for o in repo_call_args["operations"]
)
mock_1.whoami.assert_called_with()
@patch("gradio.themes.base.Base._get_next_version", return_value="0.1.3")
@patch("gradio.themes.base.Base._theme_version_exists", return_value=False)
@patch("gradio.themes.base.huggingface_hub")
def test_can_pass_version_and_theme(self, mock_1, mock_2, mock_3):
mock_1.whoami.return_value = {"name": "freddyaboulton"}
gr.themes.Monochrome().push_to_hub(
repo_name="my_monochrome", version="0.1.5", hf_token="foo"
)
repo_call_args = mock_1.HfApi().create_commit.call_args_list[0][1]
assert repo_call_args["repo_id"] == "freddyaboulton/my_monochrome"
assert any(
o.path_in_repo == "themes/theme_schema@0.1.5.json"
for o in repo_call_args["operations"]
)
mock_1.whoami.assert_called_with(token="foo")
@patch("gradio.themes.base.huggingface_hub")
def test_raise_error_if_no_token_and_not_logged_in(self, mock_1):
mock_1.whoami.side_effect = OSError("not logged in")
with pytest.raises(
ValueError,
match="In order to push to hub, log in via `huggingface-cli login`",
):
gr.themes.Monochrome().push_to_hub(
repo_name="my_monochrome", version="0.1.5"
)
@patch("gradio.themes.base.Base._get_next_version", return_value="0.1.3")
@patch("gradio.themes.base.Base._theme_version_exists", return_value=False)
@patch("gradio.themes.base.huggingface_hub")
def test_can_upload_to_org(self, mock_1, mock_2, mock_3):
mock_1.whoami.return_value = {"name": "freddyaboulton"}
gr.themes.Monochrome().push_to_hub(
repo_name="my_monochrome", version="0.1.9", org_name="gradio"
)
repo_call_args = mock_1.HfApi().create_commit.call_args_list[0][1]
assert repo_call_args["repo_id"] == "gradio/my_monochrome"
assert any(
o.path_in_repo == "themes/theme_schema@0.1.9.json"
for o in repo_call_args["operations"]
)
mock_1.whoami.assert_called_with()
@patch("gradio.themes.base.Base._get_next_version", return_value="0.1.3")
@patch("gradio.themes.base.Base._theme_version_exists", return_value=False)
@patch("gradio.themes.base.huggingface_hub")
def test_can_make_private(self, mock_1, mock_2, mock_3):
mock_1.whoami.return_value = {"name": "freddyaboulton"}
gr.themes.Monochrome().push_to_hub(
repo_name="my_monochrome", version="0.1.9", org_name="gradio", private=True
)
mock_1.create_repo.assert_called_with(
"gradio/my_monochrome",
repo_type="space",
space_sdk="gradio",
token=None,
exist_ok=True,
private=True,
)

View File

@ -136,6 +136,14 @@
{% endwith %}
{% endfor %}
</div>
<a class="thin-link px-4 block" href="#themes">Themes</a>
<div class="sub-links hidden" hash="#themes">
{% for theme in docs["themes"] %}
{% with obj=theme %}
<a class="thinner-link px-4 pl-8 block" href="#{{ obj['name'].lower()}}">{{ obj["name"] }}</a>
{% endwith %}
{% endfor %}
</div>
<a class="link px-4 my-2 block" href="#components">Components</a>
{% for component in docs["component"] %}
<a class="px-4 block thin-link" href="#{{ component['name'].lower() }}">{{ component['name'] }}</a>
@ -245,6 +253,21 @@
{% endfor %}
</div>
</section>
<section id="themes" class="pt-2 mb-8">
<h3 class="text-3xl font-light my-4">
Themes
</h3>
<p class="mb-12">
Customize the look of your app by writing your own custom theme
</p>
<div class="flex flex-col gap-10">
{% for layout in docs["themes"] %}
{% with obj=layout, is_class=True, parent="gradio" %}
{% include "docs/obj_doc_template.html" %}
{% endwith %}
{% endfor %}
</div>
</section>
</section>
<section id="components" class="pt-2 flex flex-col gap-10 mb-8">
<div>