diff options
| author | Yuriy Andamasov <yuriy@vyos.io> | 2026-05-06 20:55:18 +0300 |
|---|---|---|
| committer | Yuriy Andamasov <yuriy@vyos.io> | 2026-05-06 20:55:18 +0300 |
| commit | 97d12b42d997627be6f33528ba2625bd0378de74 (patch) | |
| tree | afe2bc5b4426837530f2b4e0ab9840c81337a4c2 | |
| parent | 5d6fa52b8985f8068314aba26878a1d7d5cb84e5 (diff) | |
| download | vyos-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.py | 6 | ||||
| -rw-r--r-- | scripts/import_myst.py | 270 | ||||
| -rw-r--r-- | scripts/swap_sources.py | 201 | ||||
| -rw-r--r-- | tests/test_import_myst.py | 101 | ||||
| -rw-r--r-- | tests/test_swap_sources.py | 121 |
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() |
