Ashkan Taghipour (The University of Western Australia) commited on
Commit
4965d2c
·
1 Parent(s): 14ba315

Fix Gradio 6.x compatibility for HF Spaces

Browse files

- Move theme/css from gr.Blocks() to launch() per Gradio 6 API
- Set ssr_mode=False to prevent localhost-access crash on HF Spaces
- Add 11 Gradio 6 compatibility tests

Files changed (4) hide show
  1. app.py +4 -2
  2. tests/test_gradio6_compat.py +169 -0
  3. ui/layout.py +8 -3
  4. ui/theme.py +1 -1
app.py CHANGED
@@ -85,7 +85,7 @@ gene_choices = sorted(DATA["protein"]["gene_id"].tolist())
85
  # ===========================================================
86
  # Build UI
87
  # ===========================================================
88
- demo, C = build_app(line_choices, contig_choices, gene_choices)
89
 
90
  # ===========================================================
91
  # Wire callbacks
@@ -292,4 +292,6 @@ with demo:
292
  # Launch
293
  # ===========================================================
294
  if __name__ == "__main__":
295
- demo.launch()
 
 
 
85
  # ===========================================================
86
  # Build UI
87
  # ===========================================================
88
+ demo, C, _theme, _css = build_app(line_choices, contig_choices, gene_choices)
89
 
90
  # ===========================================================
91
  # Wire callbacks
 
292
  # Launch
293
  # ===========================================================
294
  if __name__ == "__main__":
295
+ # Gradio 6.x moved theme/css from Blocks() to launch().
296
+ # ssr_mode=False prevents the SSR localhost-access crash on HF Spaces.
297
+ demo.launch(ssr_mode=False, theme=_theme, css=_css)
tests/test_gradio6_compat.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for Gradio 6.x compatibility.
2
+
3
+ Verifies that the app conforms to Gradio 6 API conventions:
4
+ - theme and css are NOT passed to gr.Blocks() constructor
5
+ - build_app() returns (demo, components, theme, css)
6
+ - theme is a valid gr.themes.Base instance
7
+ - css is a non-empty string
8
+ - demo.launch() would receive ssr_mode, theme, css kwargs
9
+ """
10
+
11
+ import sys
12
+ import warnings
13
+ from pathlib import Path
14
+ from unittest.mock import patch, MagicMock
15
+
16
+ import pytest
17
+
18
+ # Ensure project root is in path
19
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
20
+
21
+ import gradio as gr
22
+ from ui.layout import build_app
23
+ from ui.theme import build_theme, CUSTOM_CSS
24
+
25
+
26
+ class TestBuildAppSignature:
27
+ """Verify build_app() returns the Gradio-6-compatible tuple."""
28
+
29
+ def test_returns_four_values(self):
30
+ """build_app() must return (demo, components, theme, css)."""
31
+ result = build_app(["line_a", "line_b"], ["contig_0"], ["g00001"])
32
+ assert len(result) == 4, (
33
+ f"Expected build_app() to return 4 values, got {len(result)}"
34
+ )
35
+
36
+ def test_demo_is_blocks(self):
37
+ """First return value is a gr.Blocks instance."""
38
+ demo, _, _, _ = build_app(["line_a"], ["contig_0"], ["g00001"])
39
+ assert isinstance(demo, gr.Blocks)
40
+
41
+ def test_components_is_dict(self):
42
+ """Second return value is a dict of UI components."""
43
+ _, components, _, _ = build_app(["line_a"], ["contig_0"], ["g00001"])
44
+ assert isinstance(components, dict)
45
+ assert len(components) > 0
46
+
47
+ def test_theme_is_gradio_theme(self):
48
+ """Third return value is a Gradio theme object."""
49
+ _, _, theme, _ = build_app(["line_a"], ["contig_0"], ["g00001"])
50
+ assert isinstance(theme, gr.themes.Base)
51
+
52
+ def test_css_is_nonempty_string(self):
53
+ """Fourth return value is a non-empty CSS string."""
54
+ _, _, _, css = build_app(["line_a"], ["contig_0"], ["g00001"])
55
+ assert isinstance(css, str)
56
+ assert len(css) > 0
57
+
58
+
59
+ class TestThemeAndCSS:
60
+ """Verify theme and CSS are valid objects."""
61
+
62
+ def test_build_theme_returns_base(self):
63
+ """build_theme() returns a gr.themes.Base instance."""
64
+ theme = build_theme()
65
+ assert isinstance(theme, gr.themes.Base)
66
+
67
+ def test_custom_css_contains_expected_classes(self):
68
+ """CUSTOM_CSS contains key class names used in the app."""
69
+ for cls in [
70
+ ".progress-tracker",
71
+ ".metric-card",
72
+ ".gene-badge-core",
73
+ ".backpack-chip",
74
+ ]:
75
+ assert cls in CUSTOM_CSS, f"Expected '{cls}' in CUSTOM_CSS"
76
+
77
+
78
+ class TestBlocksConstructor:
79
+ """Verify gr.Blocks() is NOT called with theme or css kwargs."""
80
+
81
+ def test_blocks_no_theme_or_css_kwarg(self):
82
+ """gr.Blocks() should only receive title, not theme/css."""
83
+ original_init = gr.Blocks.__init__
84
+
85
+ captured_kwargs = {}
86
+
87
+ def spy_init(self, *args, **kwargs):
88
+ captured_kwargs.update(kwargs)
89
+ return original_init(self, *args, **kwargs)
90
+
91
+ with patch.object(gr.Blocks, "__init__", spy_init):
92
+ build_app(["line_a"], ["contig_0"], ["g00001"])
93
+
94
+ assert "theme" not in captured_kwargs, (
95
+ "theme should not be passed to gr.Blocks() in Gradio 6.x -- "
96
+ "pass it to launch() instead"
97
+ )
98
+ assert "css" not in captured_kwargs, (
99
+ "css should not be passed to gr.Blocks() in Gradio 6.x -- "
100
+ "pass it to launch() instead"
101
+ )
102
+
103
+
104
+ class TestNoDeprecationWarnings:
105
+ """Verify build_app() does not emit Gradio deprecation warnings."""
106
+
107
+ def test_no_gradio_warnings(self):
108
+ """build_app() should not trigger Gradio 6.x deprecation warnings."""
109
+ with warnings.catch_warnings(record=True) as caught:
110
+ warnings.simplefilter("always")
111
+ build_app(["line_a"], ["contig_0"], ["g00001"])
112
+
113
+ gradio_warnings = [
114
+ w for w in caught
115
+ if "gradio" in str(w.category).lower()
116
+ or "gradio" in str(w.message).lower()
117
+ or "moved" in str(w.message).lower()
118
+ ]
119
+ assert len(gradio_warnings) == 0, (
120
+ f"Unexpected Gradio warnings: "
121
+ f"{[str(w.message) for w in gradio_warnings]}"
122
+ )
123
+
124
+
125
+ class TestComponentsPresent:
126
+ """Verify that build_app() returns all expected component keys."""
127
+
128
+ def test_required_component_keys(self):
129
+ """Essential UI components are present in the returned dict."""
130
+ _, components, _, _ = build_app(
131
+ ["line_a", "line_b"], ["contig_0"], ["g00001"]
132
+ )
133
+
134
+ required_keys = [
135
+ "state", "tabs", "data_health_html",
136
+ # Quest 0
137
+ "q0_tab", "q0_globe_plot", "q0_line_dropdown",
138
+ "q0_country_dropdown", "q0_start_btn",
139
+ # Quest 1
140
+ "q1_tab", "q1_umap_plot", "q1_color_radio",
141
+ # Quest 2
142
+ "q2_tab", "q2_donut_plot", "q2_treasure_table",
143
+ # Quest 3
144
+ "q3_tab", "q3_heatmap_plot", "q3_contig_dropdown",
145
+ # Quest 4
146
+ "q4_tab", "q4_gene_dropdown",
147
+ # Final
148
+ "final_generate_btn", "final_report_md",
149
+ ]
150
+ for key in required_keys:
151
+ assert key in components, f"Missing component key: {key}"
152
+
153
+
154
+ class TestSSRModeDisabled:
155
+ """Verify ssr_mode=False is passed at launch time."""
156
+
157
+ def test_launch_kwargs_in_app_module(self):
158
+ """app.py should pass ssr_mode=False, theme, and css to launch()."""
159
+ app_path = Path(__file__).resolve().parent.parent / "app.py"
160
+ source = app_path.read_text()
161
+
162
+ assert "ssr_mode=False" in source, (
163
+ "app.py must pass ssr_mode=False to demo.launch() "
164
+ "to prevent SSR localhost crash on HF Spaces"
165
+ )
166
+ assert "theme=" in source and "css=" in source, (
167
+ "app.py must pass theme= and css= to demo.launch() "
168
+ "for Gradio 6.x compatibility"
169
+ )
ui/layout.py CHANGED
@@ -16,11 +16,16 @@ def build_app(line_choices: list[str], contig_choices: list[str],
16
  gene_choices: list[str]) -> tuple:
17
  """
18
  Build the full Gradio Blocks app.
19
- Returns (demo, components_dict) where components_dict maps all UI elements.
 
 
 
 
 
20
  """
21
  theme = build_theme()
22
 
23
- with gr.Blocks(theme=theme, css=CUSTOM_CSS, title="Pigeon Pea Pangenome Atlas") as demo:
24
  # State
25
  state = gr.State(value=None)
26
 
@@ -67,7 +72,7 @@ def build_app(line_choices: list[str], contig_choices: list[str],
67
  **{f"gc_{k}": v for k, v in gc.items()},
68
  }
69
 
70
- return demo, components
71
 
72
 
73
  def _build_progress_html(active_quest: int) -> str:
 
16
  gene_choices: list[str]) -> tuple:
17
  """
18
  Build the full Gradio Blocks app.
19
+
20
+ Returns (demo, components_dict, theme, css) where:
21
+ - demo is the gr.Blocks instance
22
+ - components_dict maps all UI elements
23
+ - theme is the Gradio theme object (pass to launch() for Gradio >= 6.0)
24
+ - css is the custom CSS string (pass to launch() for Gradio >= 6.0)
25
  """
26
  theme = build_theme()
27
 
28
+ with gr.Blocks(title="Pigeon Pea Pangenome Atlas") as demo:
29
  # State
30
  state = gr.State(value=None)
31
 
 
72
  **{f"gc_{k}": v for k, v in gc.items()},
73
  }
74
 
75
+ return demo, components, theme, CUSTOM_CSS
76
 
77
 
78
  def _build_progress_html(active_quest: int) -> str:
ui/theme.py CHANGED
@@ -7,7 +7,7 @@ Exports
7
  -------
8
  get_theme() – returns the configured ``gr.themes.Base`` instance.
9
  build_theme() – alias kept for backward compatibility.
10
- CUSTOM_CSS – CSS string injected into ``gr.Blocks(css=...)``.
11
  """
12
 
13
  import gradio as gr
 
7
  -------
8
  get_theme() – returns the configured ``gr.themes.Base`` instance.
9
  build_theme() – alias kept for backward compatibility.
10
+ CUSTOM_CSS – CSS string passed to ``demo.launch(css=...)`` (Gradio 6.x).
11
  """
12
 
13
  import gradio as gr