from __future__ import annotations
from argparse import ArgumentParser
from pathlib import Path
import re
import tomllib
from jinja2 import Environment, StrictUndefined
from strif import atomic_output_file
UPSTREAM_DOCS_BASE_URL = "https://github.com/jlevy/flowmark/blob/main/docs/"
def parse_args() -> tuple[Path, Path, Path]:
repo_root = Path(__file__).resolve().parents[1]
parser = ArgumentParser(description="Generate README.md from shared docs + Rust wrapper template.")
parser.add_argument(
"--shared-docs",
"--python-readme",
dest="shared_docs",
type=Path,
default=repo_root / "repos/flowmark/docs/shared/flowmark-readme-shared.md",
help="Path to canonical shared docs source.",
)
parser.add_argument(
"--template",
type=Path,
default=repo_root / "docs/templates/rust-readme-wrapper.md",
help="Path to Markdown wrapper template rendered with Jinja.",
)
parser.add_argument(
"--output",
type=Path,
default=repo_root / "README.md",
help="Output README path.",
)
args = parser.parse_args()
return args.shared_docs, args.template, args.output
def rewrite_upstream_local_docs_links(markdown: str) -> str:
return re.sub(
r"\]\(docs/(?!development\.md)([^)]+)\)",
rf"]({UPSTREAM_DOCS_BASE_URL}\1)",
markdown,
)
PERSPECTIVE_FLIP_OLD = (
"Flowmark comes in two flavors: this Python reference implementation and an "
"auto-synced\n[Rust port (flowmark-rs)](https://github.com/jlevy/flowmark-rs)."
)
PERSPECTIVE_FLIP_NEW = (
"Flowmark comes in two flavors: the "
"[Python reference implementation](https://github.com/jlevy/flowmark) and this "
"auto-synced Rust port (flowmark-rs)."
)
def rewrite_perspective_for_rust_repo(markdown: str) -> str:
if PERSPECTIVE_FLIP_OLD not in markdown:
raise ValueError(
"shared docs no longer contain the expected Python-perspective sentence; "
"update PERSPECTIVE_FLIP_OLD in scripts/generate_rust_readme.py"
)
return markdown.replace(PERSPECTIVE_FLIP_OLD, PERSPECTIVE_FLIP_NEW)
def read_msrv(repo_root: Path) -> str:
cargo_toml = repo_root / "Cargo.toml"
if not cargo_toml.exists():
raise FileNotFoundError(f"missing Cargo.toml at {cargo_toml}")
metadata = tomllib.loads(cargo_toml.read_text(encoding="utf-8"))
package = metadata.get("package", {})
msrv = package.get("rust-version")
if not isinstance(msrv, str) or not msrv:
raise ValueError(f"missing [package].rust-version in {cargo_toml}")
return msrv
def read_parity_version(repo_root: Path) -> str:
cargo_toml = repo_root / "Cargo.toml"
if not cargo_toml.exists():
raise FileNotFoundError(f"missing Cargo.toml at {cargo_toml}")
metadata = tomllib.loads(cargo_toml.read_text(encoding="utf-8"))
package = metadata.get("package", {})
parity = package.get("metadata", {}).get("parity", {}).get("version")
if not isinstance(parity, str) or not parity:
raise ValueError(f"missing [package.metadata.parity].version in {cargo_toml}")
return parity
def read_last_sync_date(repo_root: Path) -> str:
port_status = repo_root / "docs/port-status.md"
if not port_status.exists():
raise FileNotFoundError(f"missing port status file at {port_status}")
text = port_status.read_text(encoding="utf-8")
match = re.search(r"^\*\*Last updated:\*\*\s+([0-9]{4}-[0-9]{2}-[0-9]{2})$", text, re.MULTILINE)
if not match:
raise ValueError(f"missing '**Last updated:** YYYY-MM-DD' in {port_status}")
return match.group(1)
def render_readme(
template_path: Path,
shared_docs_body: str,
msrv: str,
parity_version: str,
last_sync_date: str,
) -> str:
environment = Environment(
autoescape=False,
undefined=StrictUndefined,
keep_trailing_newline=True,
)
template = environment.from_string(template_path.read_text(encoding="utf-8"))
rendered = template.render(
shared_docs_body=shared_docs_body,
msrv=msrv,
parity_version=parity_version,
last_sync_date=last_sync_date,
)
if not rendered.endswith("\n"):
rendered += "\n"
return rendered
def write_atomic(output_path: Path, content: str) -> None:
with atomic_output_file(output_path, make_parents=True) as temp_path:
Path(temp_path).write_text(content, encoding="utf-8")
def main() -> int:
shared_docs_path, template_path, output_path = parse_args()
repo_root = Path(__file__).resolve().parents[1]
if not shared_docs_path.exists():
raise FileNotFoundError(f"missing shared docs source at {shared_docs_path}")
if not template_path.exists():
raise FileNotFoundError(f"missing wrapper template at {template_path}")
shared_docs_body = shared_docs_path.read_text(encoding="utf-8").rstrip() + "\n"
shared_docs_body = rewrite_upstream_local_docs_links(shared_docs_body)
shared_docs_body = rewrite_perspective_for_rust_repo(shared_docs_body)
rendered = render_readme(
template_path,
shared_docs_body,
read_msrv(repo_root),
read_parity_version(repo_root),
read_last_sync_date(repo_root),
)
write_atomic(output_path, rendered)
print(f"Generated {output_path} from {shared_docs_path} via {template_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())