summaryrefslogtreecommitdiff
path: root/scripts/swap_sources.py
diff options
context:
space:
mode:
authorYuriy Andamasov <yuriy@vyos.io>2026-05-06 20:55:18 +0300
committerYuriy Andamasov <yuriy@vyos.io>2026-05-06 20:55:18 +0300
commit97d12b42d997627be6f33528ba2625bd0378de74 (patch)
treeafe2bc5b4426837530f2b4e0ab9840c81337a4c2 /scripts/swap_sources.py
parent5d6fa52b8985f8068314aba26878a1d7d5cb84e5 (diff)
downloadvyos-documentation-97d12b42d997627be6f33528ba2625bd0378de74.tar.gz
vyos-documentation-97d12b42d997627be6f33528ba2625bd0378de74.zip
feat: flip swap mechanism — Phase 2 (swap_sources.py rewrite)
Phase 2 of the MD-as-primary flip. Inverts swap_sources.py so it activates RST overrides (rst-<stem>.rst → <stem>.rst, with the matching <stem>.md excluded via _md_exclude.txt) for stems listed in docs/_rst_overrides.txt. Changes: - scripts/swap_sources.py: rewritten with inverted rename direction and renamed runtime artifacts (_rst_override_state.json, _md_exclude.txt). CLI flags --swap/--restore/--dry-run/--status kept for compatibility with the Makefile and Read the Docs config. - docs/conf.py: clean up the runtime-artifact references that Phase 1 left pointing at the old _swap_state.json and _swap_exclude.txt names. - scripts/import_myst.py and tests/test_import_myst.py deleted; obsolete after the flip (MD is canonical, no separate import workflow needed). - tests/test_swap_sources.py: rewritten for the new semantics. All 10 tests pass under pytest. Smoke-tested end-to-end on a real worktree page (quick-start): adding the stem to _rst_overrides.txt, --dry-run, --swap, --status, --restore, all behave correctly. State JSON has version 2 (bumped from 1 to surface the incompatibility on rollback if old state lingers). Phase 3 will verify Makefile, .readthedocs.yml, docs/_ext/vyos.py don't reference any of the old names, then mark the PR ready-for-review. Generated by robots https://vyos.io
Diffstat (limited to 'scripts/swap_sources.py')
-rw-r--r--scripts/swap_sources.py201
1 files changed, 109 insertions, 92 deletions
diff --git a/scripts/swap_sources.py b/scripts/swap_sources.py
index 683d5ef9..8a03085e 100644
--- a/scripts/swap_sources.py
+++ b/scripts/swap_sources.py
@@ -1,26 +1,36 @@
#!/usr/bin/env python3
-"""swap_sources.py — Pre-build swap/restore for incremental RST-to-MyST migration.
+"""swap_sources.py — Pre-build swap/restore for the RST override mechanism.
+
+In this repo MD is the canonical source for migrated pages. RST kept around
+under the ``rst-<stem>.rst`` prefix is a *fallback* — only consulted when a
+page is explicitly listed in ``docs/_rst_overrides.txt`` to revert to the
+legacy RST rendering.
+
+This script flips the source files in place at build time so Sphinx renders
+the RST instead of the MD for the listed stems, then restores everything
+after the build.
Usage
-----
- python scripts/swap_sources.py --swap # rename md-{name}.md → {name}.md
- python scripts/swap_sources.py --restore # undo renames
- python scripts/swap_sources.py --dry-run # show what would be swapped
- python scripts/swap_sources.py --status # show current swap state
+ python scripts/swap_sources.py --swap # activate RST overrides
+ python scripts/swap_sources.py --restore # undo activated overrides
+ python scripts/swap_sources.py --dry-run # show what would be activated
+ python scripts/swap_sources.py --status # show current state
python scripts/swap_sources.py --swap --docs-dir /path/to/docs
-Swap list
----------
- docs/_swap.txt — one stem per line, relative to docs/, comments (#) skipped.
- Example:
- # migrated pages
- configuration/firewall/zone
- quick-start
+The ``--swap`` flag name is kept for backward compatibility with the Makefile
+and Read the Docs config; semantically it now applies RST overrides.
+
+Override list
+-------------
+ docs/_rst_overrides.txt — one stem per line, relative to docs/, comments
+ (#) skipped. Pages listed here render from rst-<stem>.rst instead of
+ <stem>.md at build time.
State files (in docs/_build/, gitignored)
-----------------------------------------
- _swap_state.json — records completed renames
- _swap_exclude.txt — RST paths to pass to Sphinx exclude_patterns
+ _rst_override_state.json — records completed RST activations
+ _md_exclude.txt — MD source paths to pass to Sphinx exclude_patterns
"""
import argparse
@@ -34,17 +44,17 @@ from pathlib import Path
# Constants
# ---------------------------------------------------------------------------
-STATE_FILE = "_swap_state.json"
-EXCLUDE_FILE = "_swap_exclude.txt"
-SWAP_LIST_FILE = "_swap.txt"
-STATE_VERSION = 1
+STATE_FILE = "_rst_override_state.json"
+EXCLUDE_FILE = "_md_exclude.txt"
+OVERRIDE_LIST_FILE = "_rst_overrides.txt"
+STATE_VERSION = 2 # bumped from 1 when the swap direction was inverted
# ---------------------------------------------------------------------------
# Core helpers
# ---------------------------------------------------------------------------
-def parse_swap_list(path: Path) -> list:
+def parse_override_list(path: Path) -> list:
"""Read *path* and return a list of stems (comments/blanks skipped).
Returns [] if the file does not exist.
@@ -74,7 +84,7 @@ def _exclude_path(docs_dir: Path) -> Path:
def _resolve_stem_paths(docs_dir: Path, stem: str):
- """Return (md_from, md_to, rst_path) as absolute Paths for a given stem."""
+ """Return (rst_from, rst_to, md_path) as absolute Paths for a given stem."""
p = Path(stem)
name = p.name
parent = p.parent # may be PosixPath('.')
@@ -83,70 +93,71 @@ def _resolve_stem_paths(docs_dir: Path, stem: str):
else:
target_dir = docs_dir / parent
- md_from = target_dir / f"md-{name}.md" # source: md-prefixed
- md_to = target_dir / f"{name}.md" # destination: plain .md
- rst_path = target_dir / f"{name}.rst" # RST that will be excluded
- return md_from, md_to, rst_path
+ rst_from = target_dir / f"rst-{name}.rst" # source: rst-prefixed
+ rst_to = target_dir / f"{name}.rst" # destination: plain .rst
+ md_path = target_dir / f"{name}.md" # MD that will be excluded
+ return rst_from, rst_to, md_path
# ---------------------------------------------------------------------------
-# do_swap
+# do_swap (apply RST overrides)
# ---------------------------------------------------------------------------
def do_swap(docs_dir: Path) -> None:
- """Rename md-{name}.md → {name}.md for each stem in _swap.txt.
+ """Rename rst-{name}.rst → {name}.rst for each stem in _rst_overrides.txt.
- Validates collisions, records state, writes exclude file.
+ The matching <stem>.md is recorded in _md_exclude.txt so Sphinx ignores it
+ during the build. Validates collisions, records state, writes exclude file.
Rolls back all completed renames on any failure.
"""
docs_dir = Path(docs_dir)
- swap_list = parse_swap_list(docs_dir / SWAP_LIST_FILE)
- if not swap_list:
- print("swap_sources: no stems in swap list, nothing to do.", file=sys.stderr)
+ override_list = parse_override_list(docs_dir / OVERRIDE_LIST_FILE)
+ if not override_list:
+ print("swap_sources: no stems in override list, nothing to do.", file=sys.stderr)
return
# Stale state: warn and auto-restore first
state_path = _state_path(docs_dir)
if state_path.exists():
print(
- "swap_sources: WARNING — stale swap state detected. Auto-restoring before new swap.",
+ "swap_sources: WARNING — stale override state detected. Auto-restoring before re-applying.",
file=sys.stderr,
)
do_restore(docs_dir)
# Validate all stems before touching the filesystem
- planned = [] # list of (md_from, md_to, rst_path, stem)
- for stem in swap_list:
- md_from, md_to, rst_path = _resolve_stem_paths(docs_dir, stem)
+ planned = [] # list of (rst_from, rst_to, md_path, stem)
+ for stem in override_list:
+ rst_from, rst_to, md_path = _resolve_stem_paths(docs_dir, stem)
- if not md_from.exists():
+ if not rst_from.exists():
print(
- f"swap_sources: skipping {stem!r} — md- source file not found: {md_from}",
+ f"swap_sources: skipping {stem!r} — rst- source file not found: {rst_from}",
file=sys.stderr,
)
continue
- if not rst_path.exists():
+ if not md_path.exists():
print(
- f"swap_sources: skipping {stem!r} — RST file not found: {rst_path}",
+ f"swap_sources: skipping {stem!r} — MD file not found: {md_path}",
file=sys.stderr,
)
continue
- if md_to.exists():
+ if rst_to.exists():
raise RuntimeError(
- f"collision: both RST and unprefixed .md exist for stem {stem!r}. "
- f"Remove {md_to} before swapping."
+ f"collision: both rst- and unprefixed .rst exist for stem {stem!r}. "
+ f"Remove {rst_to} before applying the override."
)
- planned.append((md_from, md_to, rst_path, stem))
+ planned.append((rst_from, rst_to, md_path, stem))
# Execute renames with rollback on failure
- completed = [] # list of (md_from, md_to) that succeeded
+ completed = [] # list of (rst_from, rst_to) that succeeded
try:
- for md_from, md_to, rst_path, stem in planned:
- os.rename(md_from, md_to)
- completed.append((md_from, md_to))
+ for rst_from, rst_to, md_path, stem in planned:
+ os.rename(rst_from, rst_to)
+ completed.append((rst_from, rst_to))
except Exception as exc:
# Rollback completed renames in reverse order
for from_path, to_path in reversed(completed):
@@ -164,25 +175,25 @@ def do_swap(docs_dir: Path) -> None:
# Build state and exclude data using relative paths (relative to docs_dir)
_build_dir(docs_dir)
- swaps = []
+ overrides = []
exclude_lines = []
- for md_from, md_to, rst_path, stem in planned:
- rel_from = md_from.relative_to(docs_dir)
- rel_to = md_to.relative_to(docs_dir)
- rel_rst = rst_path.relative_to(docs_dir)
- swaps.append({
+ for rst_from, rst_to, md_path, stem in planned:
+ rel_from = rst_from.relative_to(docs_dir)
+ rel_to = rst_to.relative_to(docs_dir)
+ rel_md = md_path.relative_to(docs_dir)
+ overrides.append({
"stem": stem,
- "md_from": str(rel_from),
- "md_to": str(rel_to),
- "rst_excluded": str(rel_rst),
+ "rst_from": str(rel_from),
+ "rst_to": str(rel_to),
+ "md_excluded": str(rel_md),
})
- exclude_lines.append(str(rel_rst))
+ exclude_lines.append(str(rel_md))
- state = {"version": STATE_VERSION, "swaps": swaps}
+ state = {"version": STATE_VERSION, "overrides": overrides}
state_path.write_text(json.dumps(state, indent=2), encoding="utf-8")
_exclude_path(docs_dir).write_text("\n".join(exclude_lines) + "\n", encoding="utf-8")
- print(f"swap_sources: swapped {len(completed)} file(s).", file=sys.stderr)
+ print(f"swap_sources: applied {len(completed)} RST override(s).", file=sys.stderr)
# ---------------------------------------------------------------------------
@@ -190,7 +201,7 @@ def do_swap(docs_dir: Path) -> None:
# ---------------------------------------------------------------------------
def do_restore(docs_dir: Path) -> None:
- """Undo all renames recorded in _swap_state.json (reverse order)."""
+ """Undo all renames recorded in _rst_override_state.json (reverse order)."""
docs_dir = Path(docs_dir)
state_path = _state_path(docs_dir)
@@ -212,21 +223,21 @@ def do_restore(docs_dir: Path) -> None:
f"expected {STATE_VERSION!r}. Inspect {state_path} manually."
)
- swaps = state.get("swaps", [])
+ overrides = state.get("overrides", [])
- for entry in reversed(swaps):
- if not isinstance(entry, dict) or "md_from" not in entry or "md_to" not in entry:
+ for entry in reversed(overrides):
+ if not isinstance(entry, dict) or "rst_from" not in entry or "rst_to" not in entry:
raise RuntimeError(
f"swap_sources: malformed entry in {state_path}: {entry!r}. "
f"Inspect manually."
)
- md_from = docs_dir / entry["md_from"] # original source (md-prefixed)
- md_to = docs_dir / entry["md_to"] # current location (plain .md)
- if md_to.exists():
- os.rename(md_to, md_from)
+ rst_from = docs_dir / entry["rst_from"] # original source (rst-prefixed)
+ rst_to = docs_dir / entry["rst_to"] # current location (plain .rst)
+ if rst_to.exists():
+ os.rename(rst_to, rst_from)
else:
print(
- f"swap_sources: WARNING — expected swapped file not found: {md_to}",
+ f"swap_sources: WARNING — expected overridden file not found: {rst_to}",
file=sys.stderr,
)
@@ -235,7 +246,7 @@ def do_restore(docs_dir: Path) -> None:
exclude_path = _exclude_path(docs_dir)
exclude_path.unlink(missing_ok=True)
- print(f"swap_sources: restored {len(swaps)} file(s).", file=sys.stderr)
+ print(f"swap_sources: restored {len(overrides)} RST override(s).", file=sys.stderr)
# ---------------------------------------------------------------------------
@@ -243,41 +254,42 @@ def do_restore(docs_dir: Path) -> None:
# ---------------------------------------------------------------------------
def do_dry_run(docs_dir: Path) -> None:
- """Print what would be swapped without making any changes."""
+ """Print what would be activated without making any changes."""
docs_dir = Path(docs_dir)
- swap_list = parse_swap_list(docs_dir / SWAP_LIST_FILE)
- if not swap_list:
- print("(dry-run) No stems in swap list.")
+ override_list = parse_override_list(docs_dir / OVERRIDE_LIST_FILE)
+ if not override_list:
+ print("(dry-run) No stems in override list.")
return
- for stem in swap_list:
- md_from, md_to, rst_path = _resolve_stem_paths(docs_dir, stem)
+ for stem in override_list:
+ rst_from, rst_to, md_path = _resolve_stem_paths(docs_dir, stem)
issues = []
- if not md_from.exists():
- issues.append("md- source missing")
- if not rst_path.exists():
- issues.append("RST missing")
- if md_to.exists():
- issues.append("COLLISION: plain .md already exists")
+ if not rst_from.exists():
+ issues.append("rst- source missing")
+ if not md_path.exists():
+ issues.append("MD missing")
+ if rst_to.exists():
+ issues.append("COLLISION: plain .rst already exists")
if issues:
print(f" SKIP {stem}: {', '.join(issues)}")
else:
- print(f" SWAP {md_from.name} → {md_to.name} (in {md_from.parent.relative_to(docs_dir) if md_from.parent != docs_dir else '.'})")
+ rel_dir = rst_from.parent.relative_to(docs_dir) if rst_from.parent != docs_dir else "."
+ print(f" APPLY {rst_from.name} → {rst_to.name} (in {rel_dir})")
def do_status(docs_dir: Path) -> None:
- """Show current swap state."""
+ """Show current override state."""
docs_dir = Path(docs_dir)
state_path = _state_path(docs_dir)
if not state_path.exists():
- print("swap_sources: no active swap state.")
+ print("swap_sources: no active RST overrides.")
return
state = json.loads(state_path.read_text(encoding="utf-8"))
- swaps = state.get("swaps", [])
- print(f"swap_sources: {len(swaps)} file(s) currently swapped:")
- for entry in swaps:
- print(f" {entry['md_from']} → {entry['md_to']} (excludes {entry['rst_excluded']})")
+ overrides = state.get("overrides", [])
+ print(f"swap_sources: {len(overrides)} RST override(s) currently active:")
+ for entry in overrides:
+ print(f" {entry['rst_from']} → {entry['rst_to']} (excludes {entry['md_excluded']})")
# ---------------------------------------------------------------------------
@@ -286,13 +298,18 @@ def do_status(docs_dir: Path) -> None:
def main(argv=None):
parser = argparse.ArgumentParser(
- description="Swap md-prefixed MyST files in place of RST files before Sphinx builds."
+ description="Activate rst-prefixed RST files as the build source for stems "
+ "listed in docs/_rst_overrides.txt."
)
group = parser.add_mutually_exclusive_group(required=True)
- group.add_argument("--swap", action="store_true", help="Perform swap")
- group.add_argument("--restore", action="store_true", help="Restore original files")
- group.add_argument("--dry-run", action="store_true", help="Show what would be swapped")
- group.add_argument("--status", action="store_true", help="Show current swap state")
+ group.add_argument("--swap", action="store_true",
+ help="Apply RST overrides (rename rst-<stem>.rst → <stem>.rst)")
+ group.add_argument("--restore", action="store_true",
+ help="Restore original files after a previous --swap")
+ group.add_argument("--dry-run", action="store_true",
+ help="Show what would be applied")
+ group.add_argument("--status", action="store_true",
+ help="Show current override state")
parser.add_argument(
"--docs-dir",