techfreakworm commited on
Commit
0592951
·
unverified ·
1 Parent(s): 232f8d6

refactor(tooltips): rename copy.py → tooltips.py to avoid stdlib shadow

Browse files

The 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: copy.py — TOOLTIPS dict
2626
 
2627
  **Files:**
2628
- - Create: `copy.py`
2629
- - Test: `tests/test_copy.py`
2630
 
2631
  - [ ] **Step A.1: Write failing test**
2632
 
2633
  ```python
2634
- # tests/test_copy.py
2635
- import copy as zis_copy # name shadows the stdlib `copy` but the module is unrelated
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(zis_copy.TOOLTIPS)
2646
 
2647
  def test_tooltips_values_are_non_empty_strings():
2648
- for key, val in zis_copy.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 zis_copy.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_copy.py -v` → ModuleNotFoundError).
2658
 
2659
- - [ ] **Step A.3: Implement `copy.py`**
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 copy.py tests/test_copy.py
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 `copy.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,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
- ├── copy.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,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 `copy.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):**
 
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(_PROJECT_ROOT))
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 copy as zis_copy # name shadows the stdlib `copy` but the module is unrelated
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(zis_copy.TOOLTIPS)
12
 
13
  def test_tooltips_values_are_non_empty_strings():
14
- for key, val in zis_copy.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 zis_copy.TOOLTIPS.items():
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
- """User-facing copy tooltips and similar short strings.
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).