from __future__ import annotations
import argparse
import difflib
import re
import typing
import pathlib
import shutil
import subprocess
import sys
ROOT = pathlib.Path(__file__).resolve().parent.parent
BASELINE = ROOT / "docs" / "architecture" / "public-api.txt"
APPENDIX = ROOT / "docs" / "architecture" / "appendices.md"
APPENDIX_COUNT = re.compile(r"is the surface — \*\*([\d,]+) items\*\*")
ARGS = [
"+nightly",
"public-api",
"--all-features",
"--omit",
"blanket-impls,auto-trait-impls,auto-derived-impls",
]
HEADER = [
"# The public API of `macrame-db`, as `cargo-public-api` reports it.",
"#",
"# Generated. Do not hand-edit: `python scripts/check_public_api.py --bless`",
"# rewrites it, and that script's docstring says why this file is checked in",
"# (D-205). A change here is a change to what 1.x promises, so it belongs in",
"# the same commit as the code that caused it, with the diff in the message.",
"#",
"# cargo " + " ".join(ARGS),
"#",
]
def cannot_measure(message: str) -> "typing.NoReturn":
print(message, file=sys.stderr)
raise SystemExit(2)
def measure() -> list[str]:
if shutil.which("cargo") is None:
cannot_measure("cargo is not on PATH; cannot measure the public API")
try:
proc = subprocess.run(
["cargo", *ARGS],
cwd=ROOT,
capture_output=True,
text=True,
check=False,
)
except OSError as e: cannot_measure(f"could not run cargo-public-api: {e}")
if proc.returncode != 0:
stderr = proc.stderr.strip()
hint = ""
if "no such command" in stderr or "public-api" in stderr and "not" in stderr:
hint = "\n\n cargo install cargo-public-api --locked"
if "nightly" in stderr and "not installed" in stderr:
hint = "\n\n rustup toolchain install nightly --profile minimal"
print(stderr, file=sys.stderr)
cannot_measure(f"cargo-public-api failed{hint}")
return [line.rstrip() for line in proc.stdout.splitlines() if line.strip()]
def is_header(line: str) -> bool:
return line.startswith("#") and not line.startswith("#[")
def appendix_count() -> int:
try:
text = APPENDIX.read_text(encoding="utf-8")
except OSError as e:
cannot_measure(f"could not read {APPENDIX.relative_to(ROOT)}: {e}")
found = APPENDIX_COUNT.findall(text)
if len(found) != 1:
cannot_measure(
f"expected exactly one surface count in "
f"{APPENDIX.relative_to(ROOT)}, found {len(found)}; the anchor "
f"'is the surface — **N items**' moved"
)
return int(found[0].replace(",", ""))
def check_appendix(n: int) -> bool:
stated = appendix_count()
if stated == n:
return True
print(
f"\nappendix D says the surface is {stated:,} items; it is {n:,}. "
f"Edit {APPENDIX.relative_to(ROOT)} to read '**{n:,} items**'.\n"
"Note when reading the delta: `macrame::prelude` re-exports the flat "
"aliases, so one new enum variant is TWO baseline items, not one. A "
"one-variant change reads as +2 and that is correct.",
)
return False
def stored() -> list[str]:
if not BASELINE.exists():
return []
lines = BASELINE.read_text(encoding="utf-8").splitlines()
return [ln.rstrip() for ln in lines if ln.strip() and not is_header(ln)]
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument(
"--bless",
action="store_true",
help="rewrite the baseline from the current surface",
)
args = ap.parse_args()
current = measure()
if args.bless:
BASELINE.parent.mkdir(parents=True, exist_ok=True)
BASELINE.write_text(
"\n".join([*HEADER, *current]) + "\n", encoding="utf-8", newline="\n"
)
print(f"blessed {BASELINE.relative_to(ROOT)}: {len(current)} items")
return 0 if check_appendix(len(current)) else 1
baseline = stored()
if not baseline:
cannot_measure(
f"no baseline at {BASELINE.relative_to(ROOT)}; run with --bless"
)
if current == baseline:
print(f"public API unchanged: {len(current)} items")
return 0 if check_appendix(len(current)) else 1
diff = difflib.unified_diff(
baseline,
current,
fromfile="baseline",
tofile="current",
lineterm="",
n=0,
)
print("\n".join(diff))
added = len([ln for ln in current if ln not in set(baseline)])
removed = len([ln for ln in baseline if ln not in set(current)])
print(
f"\npublic API moved: +{added} -{removed} "
f"({len(baseline)} -> {len(current)} items). "
"If this is intended, re-run with --bless and put the diff above in the "
"commit message."
)
return 1
if __name__ == "__main__":
raise SystemExit(main())