Back to Repositories

Validating Theme Management and Sharing Integration in Gradio

This test suite validates theme management functionality in the Gradio framework, focusing on theme versioning, sharing, and customization capabilities. It covers theme asset handling, version matching, and Hub integration features.

Test Coverage Overview

The test suite provides comprehensive coverage of Gradio’s theme management system, including:
  • Version matching and semantic versioning compliance
  • Theme asset management and retrieval
  • Built-in theme configurations and properties
  • Theme upload/download functionality with Hugging Face Hub
Key integration points include HuggingFace Hub API and theme versioning system.

Implementation Analysis

The testing approach employs pytest fixtures and mocking to validate theme handling logic. Tests utilize patch decorators for HuggingFace Hub interactions and implement systematic checks for version management. The implementation follows a modular pattern with distinct test classes for different theme functionalities.

Technical Details

Testing tools and configuration:
  • pytest framework with parametrize and mark decorators
  • unittest.mock for HuggingFace Hub API mocking
  • tempfile for temporary file operations
  • Custom theme asset handling classes
  • Semantic version parsing and matching utilities

Best Practices Demonstrated

The test suite exemplifies robust testing practices including:
  • Comprehensive error case handling
  • Isolation of external dependencies through mocking
  • Systematic version compatibility testing
  • Clear test organization by functionality
  • Effective use of pytest features for test parameterization

gradio-app/gradio

test/test_theme_sharing.py

            
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,
    error_border_color="#fecaca",
    error_background_fill="#fee2e2",
    error_icon_color="#b91c1c",
    error_icon_color_dark="#ef4444",
    error_text_color="#ef4444",
    error_text_color_dark="#ef4444",
    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",
)
dracula.name = "gradio/dracula_test"


class TestSemverMatch:
    def test_simple_equality(self):
        assert get_matching_version(assets, "3.10.0") == ThemeAsset(
            "[email protected]"
        )

    def test_empty_expression_returns_latest(self):
        assert get_matching_version(assets, None) == ThemeAsset("[email protected]")

    def test_range(self):
        assert get_matching_version(assets, ">=3.10.0,<3.15") == ThemeAsset(
            "[email protected]"
        )

    def test_wildcard(self):
        assert get_matching_version(assets, "2.2.*") == ThemeAsset(
            "[email protected]"
        )

    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("[email protected]")

    def test_breaks_ties_against_prerelease(self):
        assert get_matching_version(assets, ">=3.18,<3.19") == ThemeAsset(
            "[email protected]"
        )


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/[email protected]",
                    "size": None,
                },
                {
                    "blob_id": None,
                    "lfs": None,
                    "rfilename": "themes/[email protected]",
                    "size": None,
                },
                {
                    "blob_id": None,
                    "lfs": None,
                    "rfilename": "themes/[email protected]",
                    "size": None,
                },
                {
                    "blob_id": None,
                    "lfs": None,
                    "rfilename": "themes/[email protected]",
                    "size": None,
                },
            ],
            tags=["gradio-theme", "gradio"],
            private=False,
            likes=0,
        )

        assert get_theme_assets(space_info) == [
            ThemeAsset("themes/[email protected]"),
            ThemeAsset("themes/[email protected]"),
            ThemeAsset("themes/[email protected]"),
            ThemeAsset("themes/[email protected]"),
        ]

        assert gr.Theme._theme_version_exists(space_info, "0.1.1")
        assert not gr.Theme._theme_version_exists(space_info, "2.0.0")

    @pytest.mark.flaky
    def test_load_space_from_hub_works(self):
        theme = gr.Theme.from_hub("gradio/seafoam")
        assert isinstance(theme, gr.Theme)


class TestBuiltInThemes:
    @pytest.mark.parametrize(
        "theme, name",
        [
            (gr.themes.Base(), "base"),
            (gr.themes.Glass(), "glass"),
            (gr.themes.Monochrome(), "monochrome"),
            (gr.themes.Soft(), "soft"),
            (gr.themes.Default(), "default"),
        ],
    )
    def test_theme_name(self, theme, name):
        assert theme.name == name


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", private=False, likes=0, tags=[])
        )
        assert next_version == "3.20.2"

    ## Commenting out until after 4.0 Spaces are up
    # @pytest.mark.flaky
    # def test_theme_download(self):
    #     assert (
    #         gr.themes.Base.from_hub("gradio/[email protected]").to_dict()
    #         == dracula.to_dict()
    #     )

    #     with gr.Blocks(theme="gradio/[email protected]") as demo:
    #         pass

    #     assert demo.theme.to_dict() == dracula.to_dict()
    #     assert demo.theme.name == "gradio/dracula_test"

    @pytest.mark.flaky
    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/[email protected]"
            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/[email protected]"
            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/[email protected]"
            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/[email protected]"
            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,
        )


def test_theme_builder_launches():
    gr.themes.builder(prevent_thread_lock=True)
    gr.close_all()