summaryrefslogtreecommitdiff
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
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
-rw-r--r--docs/conf.py6
-rw-r--r--scripts/import_myst.py270
-rw-r--r--scripts/swap_sources.py201
-rw-r--r--tests/test_import_myst.py101
-rw-r--r--tests/test_swap_sources.py121
5 files changed, 176 insertions, 523 deletions
diff --git a/docs/conf.py b/docs/conf.py
index b5019407..76a00577 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -91,14 +91,14 @@ gettext_uuid = False
# This pattern also affects html_static_path and html_extra_path .
exclude_patterns = [
u'_build', 'Thumbs.db', '.DS_Store', '_include/vyos-1x',
- 'md-*.md', '**/md-*.md',
+ 'rst-*.rst', '**/rst-*.rst',
]
import pathlib
_build = pathlib.Path(__file__).parent / '_build'
-if (_build / '_swap_state.json').exists() and (_build / '_swap_exclude.txt').exists():
+if (_build / '_rst_override_state.json').exists() and (_build / '_md_exclude.txt').exists():
exclude_patterns.extend(
- s for s in (line.strip() for line in (_build / '_swap_exclude.txt').read_text().splitlines()) if s
+ s for s in (line.strip() for line in (_build / '_md_exclude.txt').read_text().splitlines()) if s
)
# The name of the Pygments (syntax highlighting) style to use.
diff --git a/scripts/import_myst.py b/scripts/import_myst.py
deleted file mode 100644
index b9dd7898..00000000
--- a/scripts/import_myst.py
+++ /dev/null
@@ -1,270 +0,0 @@
-#!/usr/bin/env python3
-"""import_myst.py — Import converted MD files from myst/* branches.
-
-Usage
------
- python scripts/import_myst.py --source myst/current --all
- python scripts/import_myst.py --source myst/current --pages quick-start configuration/firewall/zone
- python scripts/import_myst.py --source myst/current --all --force
- python scripts/import_myst.py --source myst/current --all --dry-run
-
-Each imported file is written as md-{name}.md alongside the existing RST
-file. Importing does not activate a page — use swap_sources.py for that.
-"""
-
-import argparse
-import subprocess
-import sys
-import warnings
-from pathlib import Path
-from typing import Optional
-
-
-# ---------------------------------------------------------------------------
-# Constants
-# ---------------------------------------------------------------------------
-
-REPO_ROOT = Path(__file__).resolve().parent.parent
-DOCS_DIR = REPO_ROOT / "docs"
-
-
-# ---------------------------------------------------------------------------
-# Core helpers
-# ---------------------------------------------------------------------------
-
-def git_show(ref: str, path: str) -> Optional[bytes]:
- """Run `git show {ref}:{path}` and return raw bytes, or None on error."""
- result = subprocess.run(
- ["git", "show", f"{ref}:{path}"],
- capture_output=True,
- cwd=REPO_ROOT,
- )
- if result.returncode != 0:
- return None
- return result.stdout
-
-
-def list_myst_files(source: str) -> list[str]:
- """Return list of MD stems from `git ls-tree -r --name-only {source} -- docs/`.
-
- Stems are relative to docs/ with the .md extension stripped.
- Only .md files are returned.
- """
- result = subprocess.run(
- ["git", "ls-tree", "-r", "--name-only", source, "--", "docs/"],
- capture_output=True,
- text=True,
- cwd=REPO_ROOT,
- )
- if result.returncode != 0:
- raise SystemExit(
- f"import_myst: git ls-tree {source!r} failed "
- f"(exit {result.returncode}): {result.stderr.strip()}"
- )
-
- stems = []
- for line in result.stdout.splitlines():
- line = line.strip()
- if not line.endswith(".md"):
- continue
- # Strip leading "docs/" prefix and .md extension
- rel = line[len("docs/"):] if line.startswith("docs/") else line
- stem = rel[:-3] # strip ".md"
- stems.append(stem)
- return stems
-
-
-def list_rst_files(docs_dir: Path) -> set[str]:
- """Return set of stems (relative to docs_dir, no extension) for all *.rst files."""
- build_dir = (docs_dir / "_build").resolve()
- stems = set()
- for rst in docs_dir.rglob("*.rst"):
- try:
- rst.resolve().relative_to(build_dir)
- continue # inside _build/, skip
- except ValueError:
- pass
- rel = rst.relative_to(docs_dir)
- stem = str(rel)[:-4] # strip ".rst"
- stems.add(stem)
- return stems
-
-
-def import_page(stem: str, md_content: bytes, docs_dir: Path, force: bool) -> bool:
- """Write md-{name}.md for the given stem.
-
- - Creates parent dirs as needed.
- - Skips (returns False) if identical content already exists.
- - Without --force: warns and skips (returns False) if different content exists.
- - With --force: overwrites (returns True).
- - Returns True on successful write.
-
- Refuses (raises ValueError) if *stem* would resolve outside *docs_dir*.
- """
- p = Path(stem)
- if p.is_absolute() or ".." in p.parts:
- raise ValueError(f"import_myst: refusing unsafe stem {stem!r}")
- name = p.name
- parent = p.parent
- if str(parent) == ".":
- target_dir = docs_dir
- else:
- target_dir = docs_dir / parent
-
- dest = target_dir / f"md-{name}.md"
- docs_root = docs_dir.resolve()
- if not dest.resolve().parent.is_relative_to(docs_root):
- raise ValueError(f"import_myst: refusing stem {stem!r} — escapes docs/")
-
- if dest.exists():
- existing = dest.read_bytes()
- if existing == md_content:
- return False # identical — skip silently
- if not force:
- warnings.warn(
- f"import_myst: {dest} exists with different content; "
- f"use --force to overwrite",
- stacklevel=2,
- )
- return False
- # force=True — fall through to write
-
- target_dir.mkdir(parents=True, exist_ok=True)
- dest.write_bytes(md_content)
- return True
-
-
-# ---------------------------------------------------------------------------
-# do_import
-# ---------------------------------------------------------------------------
-
-def do_import(
- source: str,
- pages: Optional[list],
- all_pages: bool,
- force: bool,
- dry_run: bool,
- docs_dir: Path,
-) -> None:
- """Orchestrate import from source branch.
-
- If --all: list MD files from source, intersect with RST stems in docs_dir,
- import only those that have an RST counterpart.
- If --pages: import only the listed stems.
- """
- docs_dir = Path(docs_dir)
-
- if all_pages:
- myst_stems = list_myst_files(source)
- rst_stems = list_rst_files(docs_dir)
- candidate_stems = [s for s in myst_stems if s in rst_stems]
- else:
- candidate_stems = list(pages or [])
-
- imported = 0
- skipped = 0
- would_import = 0
-
- for stem in candidate_stems:
- # Path inside the branch: docs/{stem}.md
- ref_path = f"docs/{stem}.md"
- content = git_show(source, ref_path)
- if content is None:
- print(
- f"import_myst: WARNING — could not fetch {source}:{ref_path}",
- file=sys.stderr,
- )
- skipped += 1
- continue
-
- if dry_run:
- print(f" (dry-run) would import {stem}")
- would_import += 1
- continue
-
- wrote = import_page(stem, content, docs_dir, force)
- if wrote:
- imported += 1
- else:
- skipped += 1
-
- if dry_run:
- print(
- f"import_myst: (dry-run) would import {would_import}, "
- f"skipped {skipped}.",
- file=sys.stderr,
- )
- else:
- print(
- f"import_myst: imported {imported}, skipped {skipped}.",
- file=sys.stderr,
- )
-
-
-# ---------------------------------------------------------------------------
-# CLI
-# ---------------------------------------------------------------------------
-
-def main(argv=None):
- parser = argparse.ArgumentParser(
- description="Import MyST MD files from a myst/* branch with md- prefix."
- )
- parser.add_argument(
- "--source",
- required=True,
- help="Git ref to import from (e.g. myst/current)",
- )
-
- pages_group = parser.add_mutually_exclusive_group(required=True)
- pages_group.add_argument(
- "--pages",
- nargs="+",
- metavar="STEM",
- help="Specific page stems to import (e.g. quick-start configuration/firewall/zone)",
- )
- pages_group.add_argument(
- "--all",
- action="store_true",
- dest="all_pages",
- help="Import all MD files that have a matching RST counterpart",
- )
-
- parser.add_argument(
- "--force",
- action="store_true",
- help="Overwrite existing md- files even if content differs",
- )
- parser.add_argument(
- "--dry-run",
- action="store_true",
- help="Print what would be imported without writing files",
- )
- parser.add_argument(
- "--docs-dir",
- type=Path,
- default=None,
- help="Path to docs/ directory (default: auto-detected from script location)",
- )
-
- args = parser.parse_args(argv)
-
- if args.docs_dir is None:
- docs_dir = DOCS_DIR
- else:
- docs_dir = args.docs_dir.resolve()
-
- if not docs_dir.is_dir():
- parser.error(f"docs directory not found: {docs_dir}")
-
- do_import(
- source=args.source,
- pages=args.pages,
- all_pages=args.all_pages,
- force=args.force,
- dry_run=args.dry_run,
- docs_dir=docs_dir,
- )
-
-
-if __name__ == "__main__":
- main()
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",
diff --git a/tests/test_import_myst.py b/tests/test_import_myst.py
deleted file mode 100644
index 1d975b45..00000000
--- a/tests/test_import_myst.py
+++ /dev/null
@@ -1,101 +0,0 @@
-"""Tests for import_myst.py — run with:
- cd tests && PYTHONPATH=../scripts python3 -m pytest test_import_myst.py -v
-"""
-import warnings
-from pathlib import Path
-
-import pytest
-
-
-# ---------------------------------------------------------------------------
-# import_page tests (no git dependency)
-# ---------------------------------------------------------------------------
-
-def test_import_single_page(tmp_path):
- """Creates md-{name}.md with the given content."""
- from import_myst import import_page
-
- docs = tmp_path / "docs"
- docs.mkdir()
- content = b"# quick-start\nHello MyST\n"
-
- result = import_page("quick-start", content, docs, force=False)
-
- assert result is True
- dest = docs / "md-quick-start.md"
- assert dest.exists()
- assert dest.read_bytes() == content
-
-
-def test_import_skip_identical(tmp_path):
- """Returns False and does not modify the file when content is identical."""
- from import_myst import import_page
-
- docs = tmp_path / "docs"
- docs.mkdir()
- content = b"# quick-start\nIdentical content\n"
- dest = docs / "md-quick-start.md"
- dest.write_bytes(content)
- mtime_before = dest.stat().st_mtime_ns
-
- result = import_page("quick-start", content, docs, force=False)
-
- assert result is False
- assert dest.read_bytes() == content
- assert dest.stat().st_mtime_ns == mtime_before
-
-
-def test_import_warn_different_without_force(tmp_path):
- """Returns False and keeps old content when content differs and force=False."""
- from import_myst import import_page
-
- docs = tmp_path / "docs"
- docs.mkdir()
- old_content = b"# old content\n"
- new_content = b"# new content\n"
- dest = docs / "md-quick-start.md"
- dest.write_bytes(old_content)
-
- with warnings.catch_warnings(record=True) as caught:
- warnings.simplefilter("always")
- result = import_page("quick-start", new_content, docs, force=False)
-
- assert result is False
- assert dest.read_bytes() == old_content
- assert len(caught) == 1
- assert "force" in str(caught[0].message).lower()
-
-
-def test_import_overwrite_with_force(tmp_path):
- """Returns True and writes new content when force=True."""
- from import_myst import import_page
-
- docs = tmp_path / "docs"
- docs.mkdir()
- old_content = b"# old content\n"
- new_content = b"# new content\n"
- dest = docs / "md-quick-start.md"
- dest.write_bytes(old_content)
-
- result = import_page("quick-start", new_content, docs, force=True)
-
- assert result is True
- assert dest.read_bytes() == new_content
-
-
-def test_import_nested_path(tmp_path):
- """Creates parent dirs and writes md-{name}.md in the correct nested location."""
- from import_myst import import_page
-
- docs = tmp_path / "docs"
- docs.mkdir()
- content = b"# zone\nFirewall zone docs\n"
-
- result = import_page("configuration/firewall/zone", content, docs, force=False)
-
- assert result is True
- dest = docs / "configuration" / "firewall" / "md-zone.md"
- assert dest.exists()
- assert dest.read_bytes() == content
- # Parent dirs were created
- assert (docs / "configuration" / "firewall").is_dir()
diff --git a/tests/test_swap_sources.py b/tests/test_swap_sources.py
index 8576e056..7e9f68be 100644
--- a/tests/test_swap_sources.py
+++ b/tests/test_swap_sources.py
@@ -1,19 +1,25 @@
"""Tests for swap_sources.py — run with:
cd tests && PYTHONPATH=../scripts python -m pytest test_swap_sources.py -v
+
+In the inverted model MD is the canonical source for migrated pages and RST
+under the ``rst-<stem>.rst`` prefix is a fallback used only for stems listed
+in ``docs/_rst_overrides.txt``. ``do_swap`` activates the override (renames
+``rst-<stem>.rst`` → ``<stem>.rst`` and excludes ``<stem>.md``); ``do_restore``
+undoes that.
"""
import json
import os
-import sys
from pathlib import Path
import pytest
+
# ---------------------------------------------------------------------------
# Helper
# ---------------------------------------------------------------------------
def _setup_docs(tmp_path: Path, stems: list[str]) -> Path:
- """Create a minimal docs/ tree with RST + md-prefixed MD files for each stem."""
+ """Create a minimal docs/ tree with MD primary + rst-prefixed RST for each stem."""
docs = tmp_path / "docs"
for stem in stems:
p = Path(stem)
@@ -21,22 +27,22 @@ def _setup_docs(tmp_path: Path, stems: list[str]) -> Path:
parent = p.parent # may be "." for flat stems
target_dir = docs / parent if str(parent) != "." else docs
target_dir.mkdir(parents=True, exist_ok=True)
- # RST file that would normally be the Sphinx source
- (target_dir / f"{name}.rst").write_text(f".. _{name}:\n\nDummy RST for {stem}\n")
- # md- prefixed MyST file waiting to be swapped in
- (target_dir / f"md-{name}.md").write_text(f"# {name}\nDummy MyST for {stem}\n")
+ # MD file is the canonical source after the flip.
+ (target_dir / f"{name}.md").write_text(f"# {name}\nDummy MyST for {stem}\n")
+ # rst-prefixed RST file waiting to be activated as an override.
+ (target_dir / f"rst-{name}.rst").write_text(f".. _{name}:\n\nDummy RST for {stem}\n")
return docs
# ---------------------------------------------------------------------------
-# parse_swap_list
+# parse_override_list
# ---------------------------------------------------------------------------
-def test_parse_swap_list_basic(tmp_path):
- from swap_sources import parse_swap_list
+def test_parse_override_list_basic(tmp_path):
+ from swap_sources import parse_override_list
- swap_txt = tmp_path / "_swap.txt"
- swap_txt.write_text(
+ overrides_txt = tmp_path / "_rst_overrides.txt"
+ overrides_txt.write_text(
"# comment\n"
"\n"
"configuration/firewall/zone\n"
@@ -44,7 +50,7 @@ def test_parse_swap_list_basic(tmp_path):
"# another comment\n"
"installation/virtual/vmware\n"
)
- stems = parse_swap_list(swap_txt)
+ stems = parse_override_list(overrides_txt)
assert stems == [
"configuration/firewall/zone",
"quick-start",
@@ -52,15 +58,15 @@ def test_parse_swap_list_basic(tmp_path):
]
-def test_parse_swap_list_empty(tmp_path):
- from swap_sources import parse_swap_list
+def test_parse_override_list_empty(tmp_path):
+ from swap_sources import parse_override_list
- missing = tmp_path / "_swap.txt"
- assert parse_swap_list(missing) == []
+ missing = tmp_path / "_rst_overrides.txt"
+ assert parse_override_list(missing) == []
# ---------------------------------------------------------------------------
-# do_swap
+# do_swap (apply RST overrides)
# ---------------------------------------------------------------------------
def test_swap_renames_files(tmp_path):
@@ -68,13 +74,13 @@ def test_swap_renames_files(tmp_path):
stems = ["quick-start"]
docs = _setup_docs(tmp_path, stems)
- (tmp_path / "docs" / "_swap.txt").write_text("quick-start\n")
+ (docs / "_rst_overrides.txt").write_text("quick-start\n")
do_swap(docs)
- assert (docs / "quick-start.md").exists(), "md- file should be renamed to plain .md"
- assert not (docs / "md-quick-start.md").exists(), "md- prefix file should be gone"
- assert (docs / "quick-start.rst").exists(), "RST file should still exist"
+ assert (docs / "quick-start.rst").exists(), "rst- file should be renamed to plain .rst"
+ assert not (docs / "rst-quick-start.rst").exists(), "rst- prefix file should be gone"
+ assert (docs / "quick-start.md").exists(), "MD file should still exist (just excluded)"
def test_swap_creates_state_file(tmp_path):
@@ -82,22 +88,22 @@ def test_swap_creates_state_file(tmp_path):
stems = ["quick-start"]
docs = _setup_docs(tmp_path, stems)
- (docs / "_swap.txt").write_text("quick-start\n")
+ (docs / "_rst_overrides.txt").write_text("quick-start\n")
do_swap(docs)
- state_file = docs / "_build" / "_swap_state.json"
+ state_file = docs / "_build" / "_rst_override_state.json"
assert state_file.exists(), "state file should be created in _build/"
state = json.loads(state_file.read_text())
- assert state["version"] == 1
- assert len(state["swaps"]) == 1
+ assert state["version"] == 2
+ assert len(state["overrides"]) == 1
- entry = state["swaps"][0]
+ entry = state["overrides"][0]
assert entry["stem"] == "quick-start"
- assert "md_from" in entry
- assert "md_to" in entry
- assert "rst_excluded" in entry
+ assert "rst_from" in entry
+ assert "rst_to" in entry
+ assert "md_excluded" in entry
def test_swap_creates_exclude_file(tmp_path):
@@ -105,15 +111,15 @@ def test_swap_creates_exclude_file(tmp_path):
stems = ["quick-start"]
docs = _setup_docs(tmp_path, stems)
- (docs / "_swap.txt").write_text("quick-start\n")
+ (docs / "_rst_overrides.txt").write_text("quick-start\n")
do_swap(docs)
- exclude_file = docs / "_build" / "_swap_exclude.txt"
+ exclude_file = docs / "_build" / "_md_exclude.txt"
assert exclude_file.exists(), "exclude file should be created in _build/"
lines = [l.strip() for l in exclude_file.read_text().splitlines() if l.strip()]
- assert "quick-start.rst" in lines
+ assert "quick-start.md" in lines
# ---------------------------------------------------------------------------
@@ -125,18 +131,19 @@ def test_restore_undoes_renames(tmp_path):
stems = ["quick-start"]
docs = _setup_docs(tmp_path, stems)
- (docs / "_swap.txt").write_text("quick-start\n")
+ (docs / "_rst_overrides.txt").write_text("quick-start\n")
do_swap(docs)
- assert (docs / "quick-start.md").exists()
+ assert (docs / "quick-start.rst").exists()
do_restore(docs)
- assert (docs / "md-quick-start.md").exists(), "md- prefix file should be restored"
- assert not (docs / "quick-start.md").exists(), "plain .md should be gone after restore"
+ assert (docs / "rst-quick-start.rst").exists(), "rst- prefix file should be restored"
+ assert not (docs / "quick-start.rst").exists(), "plain .rst should be gone after restore"
+ assert (docs / "quick-start.md").exists(), "MD file should remain untouched throughout"
- state_file = docs / "_build" / "_swap_state.json"
- exclude_file = docs / "_build" / "_swap_exclude.txt"
+ state_file = docs / "_build" / "_rst_override_state.json"
+ exclude_file = docs / "_build" / "_md_exclude.txt"
assert not state_file.exists(), "state file should be deleted after restore"
assert not exclude_file.exists(), "exclude file should be deleted after restore"
@@ -159,20 +166,20 @@ def test_swap_fails_on_collision(tmp_path):
stems = ["quick-start", "cli"]
docs = _setup_docs(tmp_path, stems)
- (docs / "_swap.txt").write_text("quick-start\ncli\n")
+ (docs / "_rst_overrides.txt").write_text("quick-start\ncli\n")
- # Simulate a collision: unprefixed .md already exists for "cli"
- (docs / "cli.md").write_text("# collision\n")
+ # Simulate a collision: unprefixed .rst already exists for "cli"
+ (docs / "cli.rst").write_text(".. _cli:\nCollision RST\n")
with pytest.raises(RuntimeError, match="collision"):
do_swap(docs)
# Rollback: first stem's rename should be undone
- assert (docs / "md-quick-start.md").exists(), "rollback should undo completed renames"
- assert not (docs / "quick-start.md").exists(), "renamed file should not exist after rollback"
+ assert (docs / "rst-quick-start.rst").exists(), "rollback should undo completed renames"
+ assert not (docs / "quick-start.rst").exists(), "renamed file should not exist after rollback"
# State file should NOT be written on failure
- state_file = docs / "_build" / "_swap_state.json"
+ state_file = docs / "_build" / "_rst_override_state.json"
assert not state_file.exists(), "no state file should be written on failure"
@@ -186,7 +193,7 @@ def test_swap_rollback_on_rename_failure(tmp_path, monkeypatch):
stems = ["quick-start", "cli"]
docs = _setup_docs(tmp_path, stems)
- (docs / "_swap.txt").write_text("quick-start\ncli\n")
+ (docs / "_rst_overrides.txt").write_text("quick-start\ncli\n")
call_count = {"n": 0}
real_rename = os.rename
@@ -203,11 +210,11 @@ def test_swap_rollback_on_rename_failure(tmp_path, monkeypatch):
do_swap(docs)
# First rename (quick-start) should have been rolled back
- assert (docs / "md-quick-start.md").exists(), "rollback should undo the first rename"
- assert not (docs / "quick-start.md").exists(), "renamed file should not exist after rollback"
+ assert (docs / "rst-quick-start.rst").exists(), "rollback should undo the first rename"
+ assert not (docs / "quick-start.rst").exists(), "renamed file should not exist after rollback"
# No state file should exist
- assert not (docs / "_build" / "_swap_state.json").exists()
+ assert not (docs / "_build" / "_rst_override_state.json").exists()
# ---------------------------------------------------------------------------
@@ -219,20 +226,20 @@ def test_swap_nested_paths(tmp_path):
stems = ["configuration/firewall/zone"]
docs = _setup_docs(tmp_path, stems)
- (docs / "_swap.txt").write_text("configuration/firewall/zone\n")
+ (docs / "_rst_overrides.txt").write_text("configuration/firewall/zone\n")
do_swap(docs)
- swapped = docs / "configuration" / "firewall" / "zone.md"
- md_source = docs / "configuration" / "firewall" / "md-zone.md"
- assert swapped.exists(), "nested md file should be renamed"
- assert not md_source.exists()
+ activated = docs / "configuration" / "firewall" / "zone.rst"
+ rst_source = docs / "configuration" / "firewall" / "rst-zone.rst"
+ assert activated.exists(), "nested rst file should be renamed"
+ assert not rst_source.exists()
- exclude_file = docs / "_build" / "_swap_exclude.txt"
+ exclude_file = docs / "_build" / "_md_exclude.txt"
lines = [l.strip() for l in exclude_file.read_text().splitlines() if l.strip()]
- assert "configuration/firewall/zone.rst" in lines
+ assert "configuration/firewall/zone.md" in lines
do_restore(docs)
- assert md_source.exists(), "nested md- file should be restored"
- assert not swapped.exists()
+ assert rst_source.exists(), "nested rst- file should be restored"
+ assert not activated.exists()