added docs
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
"""The user documentation names every profile key, live key and on-screen label, and its links resolve."""
|
||||
|
||||
import dataclasses
|
||||
import re
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterator
|
||||
|
||||
from python_app.models.gui_profile_schema import GuiProfileModel
|
||||
from python_app.orchestration.live_processing_config import ProcessingLiveConfig
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
DOCS = ROOT / "docs"
|
||||
GUI = ROOT / "python_app" / "gui"
|
||||
# A full profile: every model section, both switches, geometry and the whole gui section.
|
||||
PROFILE = ROOT / "run_config_examples" / "run_config_librevna_multi_per_point.example.json"
|
||||
# Text the widgets show: constructor captions, and ("Label", widget) form rows of the sections and dialogs.
|
||||
CAPTION = re.compile(r'\b(?:QPushButton|QCheckBox|QGroupBox|QLabel|QToolButton|addRow|setWindowTitle|setTitle)\(\s*"([^"%{}]{2,})"')
|
||||
FORM_ROW = re.compile(r'\(\s*"([^"%{}]{1,60})"\s*,\s*[a-z_][a-z_0-9]*\s*\)')
|
||||
LINK = re.compile(r"\]\(([^)#\s]+)(?:#[^)]*)?\)")
|
||||
CODE = re.compile(r"(`+)(.+?)\1")
|
||||
BOLD = re.compile(r"\*\*(.+?)\*\*")
|
||||
HEADING = re.compile(r"^#+\s+(.+)$", re.MULTILINE)
|
||||
|
||||
|
||||
def leaf_names(value: Any) -> Iterator[str]:
|
||||
if isinstance(value, dict):
|
||||
for key, item in value.items():
|
||||
if isinstance(item, (dict, list)) and item:
|
||||
yield from leaf_names(item)
|
||||
else:
|
||||
yield key
|
||||
elif isinstance(value, list) and isinstance(value[0], dict):
|
||||
yield from leaf_names(value[0])
|
||||
|
||||
|
||||
def on_screen_labels() -> set[str]:
|
||||
labels: set[str] = set()
|
||||
for path in GUI.rglob("*.py"):
|
||||
source = path.read_text(encoding="utf-8")
|
||||
labels.update(CAPTION.findall(source))
|
||||
if path.parent.name == "sections" or path.name.endswith(("_dialog.py", "_editor.py")):
|
||||
labels.update(FORM_ROW.findall(source))
|
||||
return labels
|
||||
|
||||
|
||||
class DocumentationTest(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.chapters = {path: path.read_text(encoding="utf-8") for path in [ROOT / "README.md", *DOCS.glob("*.md")]}
|
||||
configuration = self.chapters[DOCS / "configuration.md"]
|
||||
self.configuration_names = {part for _, span in CODE.findall(configuration)
|
||||
for part in re.split(r"[.,*\s\[\]{}\"]+", span) if part}
|
||||
|
||||
def test_configuration_names_every_profile_and_live_key(self) -> None:
|
||||
profile = GuiProfileModel.load_from_path(PROFILE).to_dict()
|
||||
live = [field.name for field in dataclasses.fields(ProcessingLiveConfig)]
|
||||
missing = sorted(set(leaf_names(profile)).union(live) - self.configuration_names)
|
||||
self.assertEqual(missing, [], "keys missing from docs/configuration.md")
|
||||
|
||||
def test_every_on_screen_label_is_documented(self) -> None:
|
||||
# A label counts only as a whole bold span, code span or heading; a hint may continue with "…".
|
||||
text = "\n".join(self.chapters.values())
|
||||
spans = {*BOLD.findall(text), *(span.strip() for _, span in CODE.findall(text)), *HEADING.findall(text)}
|
||||
|
||||
def documented(label: str) -> bool:
|
||||
label = label.strip()
|
||||
return label in spans or any(span.startswith(label) and span[len(label):].lstrip().startswith("…")
|
||||
for span in spans)
|
||||
|
||||
self.assertEqual(sorted(label for label in on_screen_labels() if not documented(label)), [])
|
||||
|
||||
def test_relative_links_resolve(self) -> None:
|
||||
broken = [f"{path.name}: {target}" for path, text in self.chapters.items()
|
||||
for target in LINK.findall(text)
|
||||
if "://" not in target and not (path.parent / target).exists()]
|
||||
self.assertEqual(broken, [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -340,7 +340,7 @@ btnApply.addEventListener("click", async () => {
|
||||
btnResetHistory.addEventListener("click", async () => {
|
||||
btnResetHistory.disabled = true;
|
||||
try {
|
||||
await api("/api/clear_history");
|
||||
await api("/api/clear_history", {});
|
||||
settingsNote.textContent = "History reset requested.";
|
||||
} catch (err) {
|
||||
settingsNote.textContent = err.message;
|
||||
|
||||
Reference in New Issue
Block a user