Spaces:
Running on Zero
Running on Zero
refactor(tooltips): rename copy.py → tooltips.py to avoid stdlib shadow
Browse filesThe original copy.py shadowed Python's stdlib copy module, requiring a
fragile conftest hack to pre-import torch/pydantic/fastapi/gradio so
they captured the real stdlib before sys.modules['copy'] was swapped.
That coupling — every new dep with a lazy 'from copy import deepcopy'
would silently break tests — is bad enough to avoid entirely.
Rename to tooltips.py. conftest.py reverts to a simple sys.path
insert. Spec and plan amendments updated.
docs/superpowers/plans/2026-05-13-z-image-studio.md
CHANGED
|
@@ -2622,17 +2622,17 @@ Additions decided after Tasks 1–3 landed. References spec sections 4.6 (toolti
|
|
| 2622 |
|
| 2623 |
---
|
| 2624 |
|
| 2625 |
-
### Task A:
|
| 2626 |
|
| 2627 |
**Files:**
|
| 2628 |
-
- Create: `
|
| 2629 |
-
- Test: `tests/
|
| 2630 |
|
| 2631 |
- [ ] **Step A.1: Write failing test**
|
| 2632 |
|
| 2633 |
```python
|
| 2634 |
-
# tests/
|
| 2635 |
-
import
|
| 2636 |
|
| 2637 |
REQUIRED_KEYS = {
|
| 2638 |
"prompt", "negative_prompt", "model", "lora", "lora_strength",
|
|
@@ -2642,21 +2642,21 @@ REQUIRED_KEYS = {
|
|
| 2642 |
}
|
| 2643 |
|
| 2644 |
def test_tooltips_has_all_required_keys():
|
| 2645 |
-
assert REQUIRED_KEYS <= set(
|
| 2646 |
|
| 2647 |
def test_tooltips_values_are_non_empty_strings():
|
| 2648 |
-
for key, val in
|
| 2649 |
assert isinstance(val, str) and val.strip(), f"{key} is empty or non-string"
|
| 2650 |
|
| 2651 |
def test_tooltips_values_are_short_enough_for_a_tooltip():
|
| 2652 |
# 200-char ceiling so tooltips don't overflow on phone
|
| 2653 |
-
for key, val in
|
| 2654 |
assert len(val) <= 200, f"{key} is too long for a tooltip ({len(val)} chars)"
|
| 2655 |
```
|
| 2656 |
|
| 2657 |
-
- [ ] **Step A.2: Run test — expect FAIL** (`pytest tests/
|
| 2658 |
|
| 2659 |
-
- [ ] **Step A.3: Implement `
|
| 2660 |
|
| 2661 |
```python
|
| 2662 |
"""User-facing copy — tooltips and similar short strings.
|
|
@@ -2692,7 +2692,7 @@ TOOLTIPS: dict[str, str] = {
|
|
| 2692 |
- [ ] **Step A.5: Commit**
|
| 2693 |
|
| 2694 |
```bash
|
| 2695 |
-
git add
|
| 2696 |
git commit -m "feat(copy): tooltip strings dict — one source of truth for param descriptions"
|
| 2697 |
```
|
| 2698 |
|
|
|
|
| 2622 |
|
| 2623 |
---
|
| 2624 |
|
| 2625 |
+
### Task A: tooltips.py — TOOLTIPS dict
|
| 2626 |
|
| 2627 |
**Files:**
|
| 2628 |
+
- Create: `tooltips.py`
|
| 2629 |
+
- Test: `tests/test_tooltips.py`
|
| 2630 |
|
| 2631 |
- [ ] **Step A.1: Write failing test**
|
| 2632 |
|
| 2633 |
```python
|
| 2634 |
+
# tests/test_tooltips.py
|
| 2635 |
+
import tooltips
|
| 2636 |
|
| 2637 |
REQUIRED_KEYS = {
|
| 2638 |
"prompt", "negative_prompt", "model", "lora", "lora_strength",
|
|
|
|
| 2642 |
}
|
| 2643 |
|
| 2644 |
def test_tooltips_has_all_required_keys():
|
| 2645 |
+
assert REQUIRED_KEYS <= set(tooltips.TOOLTIPS)
|
| 2646 |
|
| 2647 |
def test_tooltips_values_are_non_empty_strings():
|
| 2648 |
+
for key, val in tooltips.TOOLTIPS.items():
|
| 2649 |
assert isinstance(val, str) and val.strip(), f"{key} is empty or non-string"
|
| 2650 |
|
| 2651 |
def test_tooltips_values_are_short_enough_for_a_tooltip():
|
| 2652 |
# 200-char ceiling so tooltips don't overflow on phone
|
| 2653 |
+
for key, val in tooltips.TOOLTIPS.items():
|
| 2654 |
assert len(val) <= 200, f"{key} is too long for a tooltip ({len(val)} chars)"
|
| 2655 |
```
|
| 2656 |
|
| 2657 |
+
- [ ] **Step A.2: Run test — expect FAIL** (`pytest tests/test_tooltips.py -v` → ModuleNotFoundError).
|
| 2658 |
|
| 2659 |
+
- [ ] **Step A.3: Implement `tooltips.py`**
|
| 2660 |
|
| 2661 |
```python
|
| 2662 |
"""User-facing copy — tooltips and similar short strings.
|
|
|
|
| 2692 |
- [ ] **Step A.5: Commit**
|
| 2693 |
|
| 2694 |
```bash
|
| 2695 |
+
git add tooltips.py tests/test_tooltips.py
|
| 2696 |
git commit -m "feat(copy): tooltip strings dict — one source of truth for param descriptions"
|
| 2697 |
```
|
| 2698 |
|
docs/superpowers/specs/2026-05-13-z-image-studio-design.md
CHANGED
|
@@ -137,7 +137,7 @@ Every user-facing param gets an info-icon affordance: a subtle circled `i` super
|
|
| 137 |
|
| 138 |
`gr.HTML(labeled_label("Steps", TOOLTIPS["steps"]))` is placed immediately before the corresponding `gr.Slider` (which uses `show_label=False`). One `gr.HTML` per labeled component.
|
| 139 |
|
| 140 |
-
**Tooltip text source.** All strings live in a new `
|
| 141 |
|
| 142 |
**Styling.** Pure CSS in `theme.CSS`:
|
| 143 |
|
|
@@ -293,7 +293,7 @@ llm/z-image-studio/
|
|
| 293 |
├── lora.py # LoRA safetensors header sniffer + apply/revert ctx
|
| 294 |
├── ui.py # Per-tab Gradio component builders + labeled_label() + model selector HTML helpers
|
| 295 |
├── theme.py # Amber tokens + gr.themes.Base subclass + CSS string (includes .zis-info / .zis-models / .zis-model rules)
|
| 296 |
-
├──
|
| 297 |
├── pyproject.toml # ruff config; py311
|
| 298 |
├── requirements.txt # diffsynth-studio, gradio==5.x, spaces, controlnet-aux, realesrgan, ...
|
| 299 |
├── README.md # HF Space YAML frontmatter (preload_from_hub) + user docs
|
|
@@ -483,7 +483,7 @@ No mocks for `ZImagePipeline` internals — only for its `__call__` boundary. Te
|
|
| 483 |
4. **License MIT** — say otherwise during review if you'd prefer Apache-2.0.
|
| 484 |
5. **ControlNet preload is in the YAML** — accept the larger startup at the gain of zero first-ControlNet-call wait. If RAM is tight at boot we'll move it to lazy.
|
| 485 |
6. **Custom model selector replaces `gr.Radio` in T2I** (per § 4.7) — necessary because two of the four cards are external-link `<a>` elements, not selectable options. A hidden `gr.Textbox` carries the state.
|
| 486 |
-
7. **Tooltips wrap every param via a `labeled_label()` HTML helper** (per § 4.6) — every `gr.Slider`/`gr.Textbox`/`gr.Image`/`gr.Dropdown`/`gr.Number`/`gr.File` uses `show_label=False` with a preceding `gr.HTML(labeled_label(...))`. Tooltip strings centralized in `
|
| 487 |
8. **Slider typing comes free** — `gr.Slider` already renders an editable numeric input next to the track (Gradio default; matches LTX repo). No custom widget needed.
|
| 488 |
|
| 489 |
**Out of scope additions (v1):**
|
|
|
|
| 137 |
|
| 138 |
`gr.HTML(labeled_label("Steps", TOOLTIPS["steps"]))` is placed immediately before the corresponding `gr.Slider` (which uses `show_label=False`). One `gr.HTML` per labeled component.
|
| 139 |
|
| 140 |
+
**Tooltip text source.** All strings live in a new `tooltips.py` module as a flat dict `TOOLTIPS[component_id]`. Keeping them out of `ui.py` lets copy edits stay separate from component wiring. Initial keys: `prompt`, `negative_prompt`, `model`, `lora`, `lora_strength`, `steps`, `cfg`, `width`, `height`, `seed`, `controlnet_image`, `controlnet_preprocessor`, `controlnet_scale`, `upscale_image`, `refine_steps`, `refine_denoise`, `output`.
|
| 141 |
|
| 142 |
**Styling.** Pure CSS in `theme.CSS`:
|
| 143 |
|
|
|
|
| 293 |
├── lora.py # LoRA safetensors header sniffer + apply/revert ctx
|
| 294 |
├── ui.py # Per-tab Gradio component builders + labeled_label() + model selector HTML helpers
|
| 295 |
├── theme.py # Amber tokens + gr.themes.Base subclass + CSS string (includes .zis-info / .zis-models / .zis-model rules)
|
| 296 |
+
├── tooltips.py # TOOLTIPS dict — short info strings per component id (one source of truth)
|
| 297 |
├── pyproject.toml # ruff config; py311
|
| 298 |
├── requirements.txt # diffsynth-studio, gradio==5.x, spaces, controlnet-aux, realesrgan, ...
|
| 299 |
├── README.md # HF Space YAML frontmatter (preload_from_hub) + user docs
|
|
|
|
| 483 |
4. **License MIT** — say otherwise during review if you'd prefer Apache-2.0.
|
| 484 |
5. **ControlNet preload is in the YAML** — accept the larger startup at the gain of zero first-ControlNet-call wait. If RAM is tight at boot we'll move it to lazy.
|
| 485 |
6. **Custom model selector replaces `gr.Radio` in T2I** (per § 4.7) — necessary because two of the four cards are external-link `<a>` elements, not selectable options. A hidden `gr.Textbox` carries the state.
|
| 486 |
+
7. **Tooltips wrap every param via a `labeled_label()` HTML helper** (per § 4.6) — every `gr.Slider`/`gr.Textbox`/`gr.Image`/`gr.Dropdown`/`gr.Number`/`gr.File` uses `show_label=False` with a preceding `gr.HTML(labeled_label(...))`. Tooltip strings centralized in `tooltips.py`.
|
| 487 |
8. **Slider typing comes free** — `gr.Slider` already renders an editable numeric input next to the track (Gradio default; matches LTX repo). No custom widget needed.
|
| 488 |
|
| 489 |
**Out of scope additions (v1):**
|
tests/conftest.py
CHANGED
|
@@ -1,34 +1,5 @@
|
|
| 1 |
-
import importlib.util
|
| 2 |
import sys
|
| 3 |
from pathlib import Path
|
| 4 |
|
| 5 |
-
_PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
| 6 |
-
|
| 7 |
# Make top-level modules importable in tests
|
| 8 |
-
sys.path.insert(0, str(
|
| 9 |
-
|
| 10 |
-
# ── copy.py shadow fix ────────────────────────────────────────────────────────
|
| 11 |
-
# The project ships a file named ``copy.py`` which shadows the stdlib module.
|
| 12 |
-
# Third-party packages (pydantic, gradio) use ``from copy import deepcopy``;
|
| 13 |
-
# they must see the stdlib module. test_copy.py uses ``import copy as zis_copy``
|
| 14 |
-
# and must see the project module.
|
| 15 |
-
#
|
| 16 |
-
# Fix: eagerly import all third-party packages that use stdlib copy so their
|
| 17 |
-
# module-level ``from copy import ...`` calls are resolved against stdlib NOW,
|
| 18 |
-
# before we swap sys.modules["copy"]. After the swap only new ``import copy``
|
| 19 |
-
# statements are affected — and test_copy.py is the only file that issues one.
|
| 20 |
-
try:
|
| 21 |
-
import torch # noqa: F401 — torch._tensor uses `from copy import deepcopy`
|
| 22 |
-
import pydantic # noqa: F401 — resolves `from copy import deepcopy` in pydantic.main
|
| 23 |
-
import fastapi # noqa: F401 — depends on pydantic
|
| 24 |
-
import gradio # noqa: F401 — depends on fastapi/pydantic
|
| 25 |
-
except ImportError:
|
| 26 |
-
pass # optional deps; if absent the swap is harmless
|
| 27 |
-
|
| 28 |
-
# Now replace sys.modules["copy"] with our project module so that any
|
| 29 |
-
# subsequent ``import copy`` (i.e., in test_copy.py) gets TOOLTIPS.
|
| 30 |
-
_project_copy_path = _PROJECT_ROOT / "copy.py"
|
| 31 |
-
_spec = importlib.util.spec_from_file_location("copy", _project_copy_path)
|
| 32 |
-
_project_copy_module = importlib.util.module_from_spec(_spec)
|
| 33 |
-
_spec.loader.exec_module(_project_copy_module)
|
| 34 |
-
sys.modules["copy"] = _project_copy_module
|
|
|
|
|
|
|
| 1 |
import sys
|
| 2 |
from pathlib import Path
|
| 3 |
|
|
|
|
|
|
|
| 4 |
# Make top-level modules importable in tests
|
| 5 |
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/{test_copy.py → test_tooltips.py}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import
|
| 2 |
|
| 3 |
REQUIRED_KEYS = {
|
| 4 |
"prompt", "negative_prompt", "model", "lora", "lora_strength",
|
|
@@ -8,12 +8,12 @@ REQUIRED_KEYS = {
|
|
| 8 |
}
|
| 9 |
|
| 10 |
def test_tooltips_has_all_required_keys():
|
| 11 |
-
assert REQUIRED_KEYS <= set(
|
| 12 |
|
| 13 |
def test_tooltips_values_are_non_empty_strings():
|
| 14 |
-
for key, val in
|
| 15 |
assert isinstance(val, str) and val.strip(), f"{key} is empty or non-string"
|
| 16 |
|
| 17 |
def test_tooltips_values_are_short_enough_for_a_tooltip():
|
| 18 |
-
for key, val in
|
| 19 |
assert len(val) <= 200, f"{key} is too long for a tooltip ({len(val)} chars)"
|
|
|
|
| 1 |
+
import tooltips
|
| 2 |
|
| 3 |
REQUIRED_KEYS = {
|
| 4 |
"prompt", "negative_prompt", "model", "lora", "lora_strength",
|
|
|
|
| 8 |
}
|
| 9 |
|
| 10 |
def test_tooltips_has_all_required_keys():
|
| 11 |
+
assert REQUIRED_KEYS <= set(tooltips.TOOLTIPS)
|
| 12 |
|
| 13 |
def test_tooltips_values_are_non_empty_strings():
|
| 14 |
+
for key, val in tooltips.TOOLTIPS.items():
|
| 15 |
assert isinstance(val, str) and val.strip(), f"{key} is empty or non-string"
|
| 16 |
|
| 17 |
def test_tooltips_values_are_short_enough_for_a_tooltip():
|
| 18 |
+
for key, val in tooltips.TOOLTIPS.items():
|
| 19 |
assert len(val) <= 200, f"{key} is too long for a tooltip ({len(val)} chars)"
|
copy.py → tooltips.py
RENAMED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""
|
| 2 |
|
| 3 |
Kept separate from ``ui.py`` so copy edits don't touch component wiring. Every
|
| 4 |
key here MUST be referenced from a labeled component in ``ui.py`` (and vice versa).
|
|
|
|
| 1 |
+
"""Tooltip strings for every labeled UI component.
|
| 2 |
|
| 3 |
Kept separate from ``ui.py`` so copy edits don't touch component wiring. Every
|
| 4 |
key here MUST be referenced from a labeled component in ``ui.py`` (and vice versa).
|