Skip to content

Commit cdc9604

Browse files
yilibinbinAaronhaohaoclaude
authored
refactor(desktop): single-source result-kind CSV spec + reviewed Batch 10 plan (#80)
* refactor(desktop): single-source result-kind CSV headers + filename (dedup) The (kind -> CSV base headers + export filename) mapping was hardcoded 2-4x per kind — once in each _show_*_results mixin (first render) and again in _refresh_display_format (reformat). Changing a header meant editing both or the reformat path would silently emit stale headers. Introduce app_desktop/result_csv_spec.py (a leaf module, no app_desktop imports, so window.py and the mixins it composes can both import it without a cycle) with result_csv_headers(kind)/result_csv_filename(kind). Route the extrapolation and statistics first-render paths and the extrapolation/statistics/fit_single reformat branches through it. The statistics dynamic *_unit column extension and error/snapshot dynamic-header paths are unchanged. This is the narrow, low-risk dedup — NOT the larger _refresh_display_format polymorphic refactor, which was consciously declined (result kinds are not added frequently and the 11 branches diverge too much for a clean abstraction). New test asserts the spec values, that headers are a fresh mutable copy, and (via getsource) that neither path re-hardcodes the literals — RED when a literal is re-inlined. ruff clean; 122 desktop tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: Batch 10 window-decomposition plan, revised after Codex adversarial review Staged plan (no code) for decomposing the 3197-line ExtrapolationWindow god-class via the proven window_fitting_mixin.py shim pattern. Revised per external adversarial review (Codex/gpt-5.5) which flagged real gaps: - Stage 2 (extrapolation mixin) NO-GO as written: it's a cross-family run controller (run_calculation dispatches stats/fitting/root; owns unit collectors), not extrapolation-only. Re-scope + park. - Stage 1 CONDITIONAL: _on_stats_mode_change lives in window.py, not the stats mixin — keep it out of the split. - Stage 0 expanded: MRO/provider-order snapshots, shim-override checks (the fitting shim is NOT empty — it overrides _on_fit_finished), no-__init__ checks, and enumerate direct-import sites of internal helpers (tests import them). - Stage 4 (Protocol facade) NO-GO for Batch 10: 182 owner.* names; do as narrow per-view typing spikes later. Revised order: 0 -> 3 (latex, cleanest) -> 1 (stats, small PRs) -> 2 (re-scoped). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: fold Gemini (Antigravity) findings into Batch 10 plan (two-model review) Second external review (Antigravity/Gemini) verdict REJECT — agreed with all Codex findings and added three verified hazards: - Qt MRO severance: QMainWindow is the leftmost base (window.py:467); PySide6 C++ wrappers don't cooperatively super(), so a split mixin overriding a Qt event handler (closeEvent/resizeEvent/__init__) is silently ignored. Stage 0 must forbid Qt-event overrides in mixins. - Precedence reversal: splitting a monolith bottom->top and composing Shim(A_top, B, C_bottom) reverses shadowing (A shadows C under left-to-right MRO). Stage 0 must prohibit duplicate method names across siblings. - Stage 3: _TectonicInstallWorker (:55) and _LatexCompileWorker (:135) are defined inside window_latex_pdf_mixin.py while all other workers live in workers_qt.py — extract them for consistency. All re-verified against the code. Plan remains a plan (no code executed). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: fanghao <fanghaoaa@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent a7e230a commit cdc9604

6 files changed

Lines changed: 285 additions & 8 deletions

File tree

app_desktop/result_csv_spec.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""Single source of truth for each result kind's CSV base headers + export
2+
filename.
3+
4+
Both the first-render path (the ``_show_*_results`` methods in the
5+
``window_*_mixin`` files) and the reformat path
6+
(``ExtrapolationWindow._refresh_display_format``) read from here, so a
7+
header/filename change is made once and can't drift between the two paths
8+
(audit R3 — the same header literals + filenames were duplicated 2-4x).
9+
10+
Kinds whose CSV headers are computed dynamically are intentionally absent:
11+
- ``error`` appends ``output_unit`` when a unit is present;
12+
- ``statistics`` may append ``value_unit``/``uncertainty_unit`` columns;
13+
- snapshot-based kinds (statistics_matrix/bootstrap/hypothesis/time_series/
14+
grouped, fitting_comparison) derive their headers from the rendered snapshot.
15+
16+
This is a leaf module (no ``app_desktop`` imports) so both ``window.py`` and the
17+
mixins it composes can import it without a circular dependency.
18+
"""
19+
20+
from __future__ import annotations
21+
22+
_RESULT_CSV_SPEC: dict[str, tuple[tuple[str, ...], str]] = {
23+
"extrapolation": (("index", "value", "uncertainty", "latex"), "extrapolation_results.csv"),
24+
"statistics": (("batch", "metric", "value", "uncertainty"), "statistics_results.csv"),
25+
"fit_single": (
26+
("batch", "section", "name", "value", "uncertainty", "stat_error", "sys_error", "note"),
27+
"fitting_results.csv",
28+
),
29+
}
30+
31+
32+
def result_csv_headers(kind: str) -> list[str]:
33+
"""Base CSV headers for a result kind (fresh mutable copy, so callers may
34+
extend it with dynamic unit columns)."""
35+
return list(_RESULT_CSV_SPEC[kind][0])
36+
37+
38+
def result_csv_filename(kind: str) -> str:
39+
"""Suggested export filename for a result kind."""
40+
return _RESULT_CSV_SPEC[kind][1]

app_desktop/window.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@
169169

170170
from .about_dialog import show_about_dialog
171171
from .docs_dialog import DocsDialog
172+
from .result_csv_spec import result_csv_filename, result_csv_headers
172173
from .resources import (
173174
_apply_system_theme,
174175
_compute_default_pdf_dpi,
@@ -2912,7 +2913,7 @@ def _refresh_display_format(self):
29122913
text, csv_rows = self._format_extrapolation_display(**payload)
29132914
self._set_result_text(text)
29142915
if csv_rows:
2915-
self._set_csv_data(csv_rows, ["index", "value", "uncertainty", "latex"], suggestion="extrapolation_results.csv")
2916+
self._set_csv_data(csv_rows, result_csv_headers("extrapolation"), suggestion=result_csv_filename("extrapolation"))
29162917
else:
29172918
self._reset_csv_data()
29182919
elif kind == "error":
@@ -2932,14 +2933,14 @@ def _refresh_display_format(self):
29322933
text, csv_rows = self._format_statistics_display(**payload)
29332934
self._set_result_text(text)
29342935
if csv_rows:
2935-
self._set_csv_data(csv_rows, ["batch", "metric", "value", "uncertainty"], suggestion="statistics_results.csv")
2936+
self._set_csv_data(csv_rows, result_csv_headers("statistics"), suggestion=result_csv_filename("statistics"))
29362937
else:
29372938
self._reset_csv_data()
29382939
elif kind == "statistics_batches":
29392940
text, csv_rows = self._format_statistics_batches_display(**payload)
29402941
self._set_result_text(text)
29412942
if csv_rows:
2942-
self._set_csv_data(csv_rows, ["batch", "metric", "value", "uncertainty"], suggestion="statistics_results.csv")
2943+
self._set_csv_data(csv_rows, result_csv_headers("statistics"), suggestion=result_csv_filename("statistics"))
29432944
else:
29442945
self._reset_csv_data()
29452946
elif kind in {
@@ -2966,8 +2967,8 @@ def _refresh_display_format(self):
29662967
if csv_rows:
29672968
self._set_csv_data(
29682969
csv_rows,
2969-
["batch", "section", "name", "value", "uncertainty", "stat_error", "sys_error", "note"],
2970-
suggestion="fitting_results.csv",
2970+
result_csv_headers("fit_single"),
2971+
suggestion=result_csv_filename("fit_single"),
29712972
)
29722973
else:
29732974
self._reset_csv_data()

app_desktop/window_extrapolation_mixin.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
CalcJob,
2424
)
2525
from .workers_qt import CalcWorker, RootSolvingWorker
26+
from .result_csv_spec import result_csv_filename, result_csv_headers
2627

2728
_DIRECT_STATISTICS_WORKFLOWS = {
2829
"bootstrap_confidence_intervals",
@@ -817,7 +818,7 @@ def _show_extrapolation_results(
817818
figure_paths.append(img_path)
818819
self._set_result_text(text, final_result=True)
819820
if csv_rows:
820-
self._set_csv_data(csv_rows, ["index", "value", "uncertainty", "latex"], suggestion="extrapolation_results.csv")
821+
self._set_csv_data(csv_rows, result_csv_headers("extrapolation"), suggestion=result_csv_filename("extrapolation"))
821822
else:
822823
self._reset_csv_data()
823824
if render_plots and figure_paths:

app_desktop/window_statistics_mixin.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
from datalab_latex.latex_tables_statistics_matrix import generate_statistics_matrix_latex
4444

4545
from .parallel_preferences import current_parallel_config_from_widgets
46+
from .result_csv_spec import result_csv_filename, result_csv_headers
4647
from .workers_core import _mp_precision_guard, _safe_read_text
4748

4849

@@ -1643,10 +1644,10 @@ def _display_statistics_result(
16431644
self._append_statistics_warning_logs(result)
16441645
self._set_result_text(text, final_result=True)
16451646
if csv_rows:
1646-
headers = ["batch", "metric", "value", "uncertainty"]
1647+
headers = result_csv_headers("statistics")
16471648
if any("value_unit" in row or "uncertainty_unit" in row for row in csv_rows):
16481649
headers.extend(["value_unit", "uncertainty_unit"])
1649-
self._set_csv_data(csv_rows, headers, suggestion="statistics_results.csv")
1650+
self._set_csv_data(csv_rows, headers, suggestion=result_csv_filename("statistics"))
16501651
else:
16511652
self._reset_csv_data()
16521653
remembered: dict[str, object] = {"result": result, "value_col": value_col, "n": n}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# Batch 10 — `ExtrapolationWindow` decomposition (staged plan, no code yet)
2+
3+
> R3-soft KEEP `[_idx 17]`; CODE_REVIEW_2026 §2.5 (high). **This document is a
4+
> plan only.** Each stage is a separate, independently-mergeable PR gated by the
5+
> project review workflow (cluster tests + 3-model + CodeRabbit + full-suite).
6+
>
7+
> **Revised after two-model external adversarial review (Codex / gpt-5.5 +
8+
> Antigravity / Gemini), each finding re-verified against the code.** Gemini's
9+
> verdict was **REJECT** (not safe to execute as originally written); it agreed
10+
> with every Codex finding and added the Qt-MRO-severance, precedence-reversal,
11+
> and Stage-3 worker-placement hazards below. Net: the shim/MRO approach is
12+
> viable, but "behavior-identical pure move" is only credible with the expanded
13+
> Stage-0 characterization first — original staging was ROI-first, this revision
14+
> is safety-first.
15+
>
16+
> ### Critical MRO hazards (Gemini, verified) — must be encoded in Stage 0
17+
> - **Qt base severs the MRO chain.** `ExtrapolationWindow(QMainWindow, …Mixins)`
18+
> has `QMainWindow` **leftmost** (window.py:467). PySide6 C++ wrappers do NOT
19+
> cooperatively call `super()`, so any Qt lifecycle override (`closeEvent`,
20+
> `resizeEvent`, `__init__`) in a split mixin is **silently ignored**. → The
21+
> plan MUST forbid split mixins from overriding Qt event handlers (or audit for
22+
> dormant ones being ignored today).
23+
> - **Splitting reverses definition precedence.** In the monolith a method at the
24+
> *bottom* shadows a duplicate at the *top*. Split sequentially into A(top) /
25+
> B / C(bottom) and composed `class Shim(A, B, C)`, left-to-right MRO makes
26+
> **A shadow C** — the opposite of the original. → Stage 0 must **strictly
27+
> prohibit duplicate method names across sibling mixins** (a test that fails on
28+
> any cross-sibling name collision).
29+
30+
## Current state (measured)
31+
32+
- `app_desktop/window.py`**3197 lines**, `ExtrapolationWindow` with **~150
33+
methods**, composed of **7 mixins** (MRO order):
34+
`WindowLatexPdfMixin, WindowI18nMixin, WindowImagesMixin,
35+
WindowStatisticsMixin, WindowDataMixin, WindowFittingMixin,
36+
WindowExtrapolationMixin`.
37+
- Mixin sizes (the god-files):
38+
- `window_statistics_mixin.py`**1921** ← biggest
39+
- `window_extrapolation_mixin.py`**1128**
40+
- `window_latex_pdf_mixin.py` — 969
41+
- fitting already split into 4 (`formatters` 561 / `residuals` 614 /
42+
`models` 673) behind a 67-line shim — **the proven pattern**.
43+
- `window_data_mixin.py` 646, `window_i18n_mixin.py` 423,
44+
`window_images_mixin.py` 388.
45+
- `app_desktop/views/` (extrapolation/fitting/statistics/error/root_solving.py,
46+
~3524 lines) already extracts **widget construction** via
47+
`build_*_mode_view(owner)` functions. The mixins hold **behavior**
48+
(run/format/handlers). The separation is: `views/` = build UI, mixins = drive
49+
it.
50+
51+
## Guiding constraints (why this is staged, not one refactor)
52+
53+
1. **Public API stable.** `window.py` must keep inheriting the same mixin names;
54+
external imports (`from app_desktop.window_* import Window*Mixin`) keep
55+
working. Follow the `WindowFittingMixin` shim: an empty subclass that composes
56+
split files and re-exports the original name. Zero caller changes per stage.
57+
2. **MRO discipline.** Method-resolution order is load-bearing (some mixins
58+
override same-named methods). Any split must preserve left-to-right MRO;
59+
pin it explicitly and document the order (as the fitting shim does).
60+
3. **Behavior-identical.** No logic changes inside a split — pure move. Verify
61+
with the existing per-mode desktop UI tests + full suite each stage.
62+
4. **Surgical, reversible.** One mixin per PR. If a split reveals a real coupling
63+
bug, fix it in its own commit, not silently.
64+
5. **Test coverage first.** Before splitting a mixin, confirm the per-mode UI
65+
test file exercises its public methods (statistics/extrapolation/fitting all
66+
have `test_desktop_*_ui.py`). Add characterization tests for any method with
67+
no coverage BEFORE moving it.
68+
69+
## Staged sequence (safest-first, one PR each)
70+
71+
### Stage 0 — Guardrails (prep) — **GO, but expanded per review**
72+
- Characterization tests: for each mixin to split, list public methods + confirm
73+
coverage; fill gaps with thin output-pinning tests. No production change.
74+
- **(review) MRO / provider-order snapshot:** capture `ExtrapolationWindow.__mro__`
75+
and each split's base order in a test so a re-order is caught.
76+
- **(review) Shim overrides are NOT always empty:** `window_fitting_mixin.py:48`
77+
overrides `_on_fit_finished` and calls `super()` before adding fallback-history
78+
UI. Stage 0 must add checks for (a) duplicate method names across siblings,
79+
(b) intentional shim-level overrides, (c) **no `__init__` in split mixins**
80+
(construction order is load-bearing) — and a construction test proving signal
81+
slots exist before `build_*_mode_view()` runs.
82+
- **(review) Import-stability is broader than `Window*Mixin`:** tests import
83+
internal helpers directly, e.g. `_statistics_raw_table_preserving_cells` from
84+
`window_statistics_mixin.py` (`test_desktop_statistics_ui.py:705`), and the
85+
release matrix names old FQNs (`test_release_test_matrix.py:70`). Any split
86+
must **re-export moved internals** from the original module OR update that
87+
evidence deliberately — Stage 0 enumerates these direct-import sites.
88+
- Freeze the file-size ratchet baselines for the new split files.
89+
90+
### Stage 1 — Split `window_statistics_mixin.py` (1921 → ~4 files) — biggest win
91+
Mirror the fitting shim. Suggested responsibility split (validate against the
92+
actual method groupings before coding):
93+
- `window_statistics_formatters_mixin.py` — result-text / CSV-row / snapshot
94+
rendering helpers (pure-ish).
95+
- `window_statistics_modes_mixin.py` — per-mode execution + dispatch
96+
(`_run_statistics_mode` and the standard/bootstrap/hypothesis/time-series/
97+
matrix/grouped paths).
98+
- `window_statistics_results_mixin.py` — worker-result handlers + plot wiring.
99+
- `window_statistics_mixin.py` — 67-line shim composing the above, MRO-pinned,
100+
re-exporting `WindowStatisticsMixin`. Public import unchanged.
101+
- **Verdict: CONDITIONAL-GO (review).** **`_on_stats_mode_change` lives in
102+
`window.py`, NOT in the statistics mixin** (its visibility logic touches
103+
`views/statistics.py:379` and is called from `workspace_controller.py:981`).
104+
Do NOT pull it into Stage 1 — either freeze it explicitly in `window.py` or
105+
make a separate stats-visibility stage. The snapshot/semantic-output path is
106+
the other tricky seam. Split stats into smaller PRs if the first is too large.
107+
108+
### Stage 2 — `window_extrapolation_mixin.py` (1128) — **NO-GO as written; re-scope (review)**
109+
**This mixin is misnamed for splitting purposes: it is a cross-family run
110+
controller, not extrapolation-only.** Its `run_calculation()` dispatches direct
111+
statistics + fitting paths (`window_extrapolation_mixin.py:389`),
112+
`_on_calc_finished()` handles statistics results (`:582`), and it owns the
113+
root/error/statistics/fitting **unit collectors** (`:1035`). A naive
114+
"extrapolation run" split would break stats/fitting/root/unit-propagation.
115+
Re-scope as a **cross-family run-controller split** with full mode coverage:
116+
first extract the pure formatters and the unit-collectors (self-contained), and
117+
only then consider separating the dispatch — treating it as its own multi-PR
118+
track, not a low-medium extrapolation task. F10's mode-switch/result-clear timing
119+
must move verbatim. **Park until Stage 1 + 3 are done and characterized.**
120+
121+
### Stage 3 — Split `window_latex_pdf_mixin.py` (969 → ~2 files) — **GO (cleanest)**
122+
- Separate compile-worker orchestration from PDF-preview/zoom UI.
123+
- **(review, Gemini) Also extract the two workers.** `_TectonicInstallWorker`
124+
(`window_latex_pdf_mixin.py:55`) and `_LatexCompileWorker` (`:135`) are `QThread`
125+
subclasses defined *inside* this mixin, whereas all other workers
126+
(`CalcWorker`, `FitWorker`, `RootSolvingWorker`, …) live in `workers_qt.py`.
127+
Amend Stage 3 to move these two to `workers_qt.py` for architectural
128+
consistency (do it as part of, or just before, the split).
129+
- **Risk: low.** Self-contained; good coverage in `test_update_*` /
130+
`test_latex_compile_*`.
131+
132+
### Stage 4 — Typed window facade `Protocol`**NO-GO for Batch 10 (review)**
133+
A `WindowFacade` Protocol would have to cover **182 distinct `owner.*` names**
134+
across `app_desktop/views/*.py`, and the project already records broad desktop
135+
statistics mypy as blocked by dynamic-mixin-attr debt (`task_plan.md:277`). One
136+
giant Protocol is noisy and brittle. **Do not bundle into Batch 10.** If pursued
137+
later, do it as narrow **per-view / per-helper Protocols**, as its own typing
138+
spike — not a window-wide facade.
139+
140+
### Explicitly NOT in scope
141+
- No "delegating controllers" rewrite / no MVC re-architecture. The mixin+shim
142+
composition is the target end-state — it already fits every file in one editor
143+
view and keeps the public API. A full controller rewrite is high-risk with no
144+
proven payoff and is **not recommended**.
145+
146+
## Effort / risk summary (post-review verdicts)
147+
148+
| Stage | File | Effort | Risk | Verdict |
149+
|---|---|---|---|---|
150+
| 0 | (tests + MRO/import characterization) | S-M | none | **GO — expanded** |
151+
| 1 | statistics_mixin | M | medium | **CONDITIONAL-GO** (keep `_on_stats_mode_change` out; split into smaller PRs) |
152+
| 3 | latex_pdf_mixin | S-M | low | **GO — cleanest** |
153+
| 2 | extrapolation_mixin (run-controller) | L | med-high | **NO-GO as written** — re-scope as cross-family run-controller, park |
154+
| 4 | Protocol facade | L || **NO-GO for Batch 10** — separate narrow typing spike |
155+
156+
**Revised recommendation (safety-first):**
157+
1. **Stage 0** — build the characterization/MRO/import-stability guardrails first
158+
(this is what makes every later "pure move" provable). Do this next if Batch 10
159+
proceeds.
160+
2. **Stage 3** (latex_pdf) — the cleanest, lowest-risk split; good second PR to
161+
validate the shim mechanics on real code.
162+
3. **Stage 1** (statistics) — biggest win, but only after 0+3, and only with
163+
`_on_stats_mode_change` frozen in `window.py`; split into small PRs.
164+
4. **Stage 2** (the run-controller) — re-scope and revisit LAST, or not at all.
165+
5. **Stage 4** — out of scope.
166+
167+
Do NOT attempt a monolithic "decompose the whole window" PR. Order changed from
168+
1→2→3 to **0 → 3 → 1 → (2 re-scoped)** on review: do the provably-safe splits
169+
first, defer the cross-family run-controller.
170+
171+
## Open question for maintainer
172+
- Proceed with Stages 0–3 now, or park Batch 10 (the god-class works, is tested,
173+
and the value is maintainability-only)? The per-stage payoff is real but purely
174+
structural — no user-facing change and no bug fixed.

tests/test_result_csv_spec.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""The result-CSV spec is the single source of truth for each result kind's
2+
base CSV headers + export filename, shared by the first-render path (_show_*
3+
mixins) and the reformat path (_refresh_display_format) so they cannot drift
4+
(audit R3 CSV-spec dedup)."""
5+
6+
from __future__ import annotations
7+
8+
import inspect
9+
import linecache
10+
11+
from app_desktop.result_csv_spec import (
12+
_RESULT_CSV_SPEC,
13+
result_csv_filename,
14+
result_csv_headers,
15+
)
16+
17+
18+
def test_result_csv_headers_returns_fresh_mutable_copy():
19+
a = result_csv_headers("extrapolation")
20+
a.append("EXTRA")
21+
b = result_csv_headers("extrapolation")
22+
assert b == ["index", "value", "uncertainty", "latex"], "spec must not be mutated by callers"
23+
assert "EXTRA" not in b
24+
25+
26+
def test_result_csv_spec_values():
27+
assert result_csv_headers("extrapolation") == ["index", "value", "uncertainty", "latex"]
28+
assert result_csv_filename("extrapolation") == "extrapolation_results.csv"
29+
assert result_csv_headers("statistics") == ["batch", "metric", "value", "uncertainty"]
30+
assert result_csv_filename("statistics") == "statistics_results.csv"
31+
assert result_csv_headers("fit_single")[:4] == ["batch", "section", "name", "value"]
32+
assert result_csv_filename("fit_single") == "fitting_results.csv"
33+
34+
35+
def test_reformat_and_first_render_read_the_same_spec():
36+
"""Both the reformat path (window._refresh_display_format) and the first-render
37+
paths (window_*_mixin _show_* methods) must source headers/filename from the
38+
spec — no hardcoded '..._results.csv' string literals should remain in those
39+
methods, or the two paths can silently diverge again."""
40+
from app_desktop import window as window_mod
41+
from app_desktop import window_extrapolation_mixin, window_statistics_mixin
42+
43+
# inspect.getsource reads through linecache; drop stale entries a prior test
44+
# may have left so getsource re-reads the current source (see #72).
45+
linecache.clearcache()
46+
refresh_src = inspect.getsource(window_mod.ExtrapolationWindow._refresh_display_format)
47+
# The spec's kinds must be routed through the accessors, not re-hardcoded.
48+
for kind in _RESULT_CSV_SPEC:
49+
fname = result_csv_filename(kind)
50+
# A bare filename literal next to _set_csv_data in the reformat method
51+
# would mean the literal was re-hardcoded rather than sourced from spec.
52+
assert f'suggestion="{fname}"' not in refresh_src, (
53+
f"reformat path hardcodes {fname} instead of result_csv_filename({kind!r})"
54+
)
55+
56+
extrap_src = inspect.getsource(window_extrapolation_mixin)
57+
stats_src = inspect.getsource(window_statistics_mixin)
58+
# The extrapolation/statistics first-render _show_* paths use the accessors.
59+
assert 'result_csv_filename("extrapolation")' in extrap_src
60+
assert 'result_csv_filename("statistics")' in stats_src

0 commit comments

Comments
 (0)