mirror of
https://github.com/gradio-app/gradio.git
synced 2025-04-12 12:40:29 +08:00
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:
parent
27be008d58
commit
8ec2b0b98a
12
.vscode/settings.json
vendored
12
.vscode/settings.json
vendored
@ -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"
|
||||
]
|
||||
}
|
68
CHANGELOG.md
68
CHANGELOG.md
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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
147
gradio/themes/app.py
Normal 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()
|
@ -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"
|
||||
|
59
gradio/themes/upload_theme.py
Normal file
59
gradio/themes/upload_theme.py
Normal 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,
|
||||
)
|
@ -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
|
||||
|
@ -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:
|
||||
|
18
gradio/themes/utils/readme_content.py
Normal file
18
gradio/themes/utils/readme_content.py
Normal 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!
|
||||
"""
|
42
gradio/themes/utils/semver_match.py
Normal file
42
gradio/themes/utils/semver_match.py
Normal 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)
|
57
gradio/themes/utils/theme_dropdown.py
Normal file
57
gradio/themes/utils/theme_dropdown.py
Normal 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;
|
||||
}}
|
||||
""",
|
||||
)
|
@ -1 +1 @@
|
||||
3.22.1
|
||||
3.22.1b1
|
||||
|
@ -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",
|
||||
|
@ -22,4 +22,6 @@ httpx
|
||||
pydantic
|
||||
websockets>=10.0
|
||||
typing_extensions
|
||||
aiofiles
|
||||
aiofiles
|
||||
huggingface_hub
|
||||
semantic_version
|
@ -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
399
test/test_theme_sharing.py
Normal 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,
|
||||
)
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user