Daryl Lim Claude Opus 4.8 (1M context) commited on
Commit
f8a5dc6
·
1 Parent(s): 9bd40fb

feat: expose stable /translate API endpoint and tidy UI

Browse files

Add api_name="translate" to the submit handler so the deployed Space
presents a clean, documented text+language -> string endpoint. Mark the
two UI-only handlers (swap, button loading) as api_visibility="private"
so they no longer leak as _swap_languages / _translate_with_loading
endpoints; only /translate is public. Autofocus the input textbox.

Add tests for the stable api_name, the single-public-endpoint surface
(via get_api_info), and input autofocus. Update CLAUDE.md test counts
(53 total / 43 fast).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files changed (3) hide show
  1. CLAUDE.md +3 -3
  2. app.py +5 -0
  3. tests/test_app.py +22 -0
CLAUDE.md CHANGED
@@ -25,8 +25,8 @@ uv run ruff format .
25
  uv run ty check
26
 
27
  # Test
28
- uv run pytest # all 50 tests (slow require CUDA + model download)
29
- uv run pytest -m "not slow" # 40 fast tests only
30
  uv run pytest -m slow # 10 model tests only (CUDA only)
31
 
32
  # Generate language mapping (dev only)
@@ -41,7 +41,7 @@ uv run scripts/generate_langmap.py <path-to-paper.pdf>
41
 
42
  **`scripts/`** — `generate_langmap.py` parses the MADLAD-400 paper PDF (Table 9, pages 16-22) using pdfplumber and generates the static language mapping with region assignments. Dev-only tool; requires `requirements-dev.txt` dependencies.
43
 
44
- **`tests/`** — 50 tests (40 fast, 10 slow). `test_langmap.py` has 10 fast tests for mapping validation (dict shape, regions, spot-checks). `test_app.py` has 30 fast tests (signatures, device fallback, UI layout with symmetric dropdowns, swap button, textbox config including toolbar buttons, handler wiring, no HTML elements, locale codes, no title) and 10 slow tests (translation with various parameters, language mapping). Slow tests require CUDA and model download; auto-skipped without CUDA.
45
 
46
  ## Tooling
47
 
 
25
  uv run ty check
26
 
27
  # Test
28
+ uv run pytest # all 53 tests (slow require CUDA + model download)
29
+ uv run pytest -m "not slow" # 43 fast tests only
30
  uv run pytest -m slow # 10 model tests only (CUDA only)
31
 
32
  # Generate language mapping (dev only)
 
41
 
42
  **`scripts/`** — `generate_langmap.py` parses the MADLAD-400 paper PDF (Table 9, pages 16-22) using pdfplumber and generates the static language mapping with region assignments. Dev-only tool; requires `requirements-dev.txt` dependencies.
43
 
44
+ **`tests/`** — 53 tests (43 fast, 10 slow). `test_langmap.py` has 10 fast tests for mapping validation (dict shape, regions, spot-checks). `test_app.py` has 33 fast tests (signatures, device fallback, UI layout with symmetric dropdowns, swap button, textbox config including toolbar buttons and input autofocus, handler wiring, stable `translate` API endpoint with UI-only handlers kept private, no HTML elements, locale codes, no title) and 10 slow tests (translation with various parameters, language mapping). Slow tests require CUDA and model download; auto-skipped without CUDA.
45
 
46
  ## Tooling
47
 
app.py CHANGED
@@ -128,6 +128,7 @@ def _build_demo() -> gr.Blocks:
128
  lines=6,
129
  max_length=2000,
130
  show_label=False,
 
131
  )
132
  output_text = gr.Textbox(
133
  placeholder="Translation",
@@ -139,20 +140,24 @@ def _build_demo() -> gr.Blocks:
139
 
140
  translate_btn = gr.Button("Translate", variant="primary")
141
 
 
142
  swap_btn.click(
143
  fn=_swap_languages,
144
  inputs=[source_language, target_language, input_text, output_text],
145
  outputs=[source_language, target_language, input_text, output_text],
 
146
  )
147
  translate_btn.click(
148
  fn=_translate_with_loading,
149
  inputs=[input_text, target_language],
150
  outputs=[translate_btn, output_text],
 
151
  )
152
  input_text.submit(
153
  fn=translate,
154
  inputs=[input_text, target_language],
155
  outputs=output_text,
 
156
  )
157
 
158
  return demo
 
128
  lines=6,
129
  max_length=2000,
130
  show_label=False,
131
+ autofocus=True,
132
  )
133
  output_text = gr.Textbox(
134
  placeholder="Translation",
 
140
 
141
  translate_btn = gr.Button("Translate", variant="primary")
142
 
143
+ # UI-only handlers: kept off the public API surface (private) so only /translate is exposed.
144
  swap_btn.click(
145
  fn=_swap_languages,
146
  inputs=[source_language, target_language, input_text, output_text],
147
  outputs=[source_language, target_language, input_text, output_text],
148
+ api_visibility="private",
149
  )
150
  translate_btn.click(
151
  fn=_translate_with_loading,
152
  inputs=[input_text, target_language],
153
  outputs=[translate_btn, output_text],
154
+ api_visibility="private",
155
  )
156
  input_text.submit(
157
  fn=translate,
158
  inputs=[input_text, target_language],
159
  outputs=output_text,
160
+ api_name="translate",
161
  )
162
 
163
  return demo
tests/test_app.py CHANGED
@@ -153,6 +153,13 @@ def test_input_textbox_has_no_placeholder(demo):
153
  assert interactive[0].placeholder is None, f"Expected no placeholder, got {interactive[0].placeholder!r}"
154
 
155
 
 
 
 
 
 
 
 
156
  def test_input_textbox_has_no_buttons(demo):
157
  """Input textbox should expose no toolbar buttons (gradio accepts invalid button values silently)."""
158
  textboxes = [b for b in demo.blocks.values() if type(b).__name__ == "Textbox"]
@@ -234,6 +241,21 @@ def test_all_handlers_wired(demo):
234
  assert len(demo.fns) == 3, f"Expected 3 handlers, found {len(demo.fns)}"
235
 
236
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  def test_no_title(demo):
238
  """UI should not have an H1 title."""
239
  markdowns = [b for b in demo.blocks.values() if type(b).__name__ == "Markdown"]
 
153
  assert interactive[0].placeholder is None, f"Expected no placeholder, got {interactive[0].placeholder!r}"
154
 
155
 
156
+ def test_input_textbox_autofocus(demo):
157
+ """Input textbox should autofocus so the cursor lands there on page load."""
158
+ textboxes = [b for b in demo.blocks.values() if type(b).__name__ == "Textbox"]
159
+ interactive = [t for t in textboxes if t.interactive is not False]
160
+ assert interactive[0].autofocus is True, f"Expected autofocus=True, got {interactive[0].autofocus!r}"
161
+
162
+
163
  def test_input_textbox_has_no_buttons(demo):
164
  """Input textbox should expose no toolbar buttons (gradio accepts invalid button values silently)."""
165
  textboxes = [b for b in demo.blocks.values() if type(b).__name__ == "Textbox"]
 
241
  assert len(demo.fns) == 3, f"Expected 3 handlers, found {len(demo.fns)}"
242
 
243
 
244
+ def test_translate_endpoint_has_stable_api_name(demo):
245
+ """The lean submit handler (text + language -> string) should expose a stable 'translate' API endpoint."""
246
+ api_fns = [fn for fn in demo.fns.values() if getattr(fn, "api_name", None) == "translate"]
247
+ assert len(api_fns) == 1, "Expected exactly one handler with api_name='translate'"
248
+ fn = api_fns[0]
249
+ assert [type(i).__name__ for i in fn.inputs] == ["Textbox", "Dropdown"]
250
+ assert [type(o).__name__ for o in fn.outputs] == ["Textbox"]
251
+
252
+
253
+ def test_only_translate_endpoint_is_public(demo):
254
+ """UI-only handlers (swap, button loading) should be private; only /translate is a public API endpoint."""
255
+ named = list(demo.get_api_info()["named_endpoints"].keys())
256
+ assert named == ["/translate"], f"Expected only ['/translate'] exposed, found {named}"
257
+
258
+
259
  def test_no_title(demo):
260
  """UI should not have an H1 title."""
261
  markdowns = [b for b in demo.blocks.values() if type(b).__name__ == "Markdown"]