137 lines
4.5 KiB
Python
137 lines
4.5 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
if sys.version_info >= (3, 11):
|
|
import tomllib
|
|
else:
|
|
import tomli as tomllib
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
CONTRACT = ROOT / "tests" / "fixtures" / "released_api_contract.json"
|
|
POLICY = ROOT / "tests" / "fixtures" / "released_api_contract_policy.json"
|
|
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from integration_tests._contract_support import ( # noqa: E402
|
|
build_released_api_contract,
|
|
load_api_contract,
|
|
load_submodule_export_policy,
|
|
)
|
|
|
|
|
|
def _parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(
|
|
description="Update the rolling released public API contract from the local checkout."
|
|
)
|
|
parser.add_argument("--version", required=True, help="Release version without a leading v.")
|
|
parser.add_argument(
|
|
"--check",
|
|
action="store_true",
|
|
help="Fail instead of writing when the committed contract is out of date.",
|
|
)
|
|
parser.add_argument(
|
|
"--output",
|
|
type=Path,
|
|
help="Write a prospective contract to this path instead of changing the released fixture.",
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
def _project_version() -> str:
|
|
data = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8"))
|
|
version = data.get("project", {}).get("version")
|
|
if not isinstance(version, str):
|
|
raise RuntimeError("pyproject.toml is missing project.version")
|
|
return version
|
|
|
|
|
|
def _head_commit() -> str:
|
|
return subprocess.check_output(
|
|
["git", "rev-parse", "HEAD"],
|
|
cwd=ROOT,
|
|
text=True,
|
|
).strip()
|
|
|
|
|
|
def _render(contract: dict[str, object]) -> str:
|
|
return json.dumps(contract, indent=2, sort_keys=True) + "\n"
|
|
|
|
|
|
def main() -> int:
|
|
args = _parse_args()
|
|
if args.check and args.output is not None:
|
|
raise SystemExit("--check and --output cannot be used together")
|
|
version = args.version
|
|
if (
|
|
version.startswith("v")
|
|
or re.fullmatch(r"\d+\.\d+(?:\.\d+)*(?:[A-Za-z0-9.-]+)?", version) is None
|
|
):
|
|
raise SystemExit("--version must be a semver-like value without a leading v")
|
|
|
|
project_version = _project_version()
|
|
if project_version != version:
|
|
raise SystemExit(
|
|
f"--version {version!r} does not match pyproject.toml version {project_version!r}"
|
|
)
|
|
output = args.output.resolve() if args.output is not None else None
|
|
protected_outputs = {CONTRACT.resolve(), POLICY.resolve()}
|
|
if output in protected_outputs:
|
|
raise SystemExit(
|
|
"--output must not overwrite released API contract inputs: "
|
|
"tests/fixtures/released_api_contract.json or "
|
|
"tests/fixtures/released_api_contract_policy.json"
|
|
)
|
|
|
|
current = load_api_contract(CONTRACT)
|
|
policy = load_submodule_export_policy(POLICY)
|
|
try:
|
|
updated = build_released_api_contract(
|
|
current,
|
|
baseline=f"v{version}",
|
|
baseline_commit=_head_commit(),
|
|
release_policy=policy,
|
|
)
|
|
except ValueError as error:
|
|
raise SystemExit(str(error)) from None
|
|
rendered = _render(updated)
|
|
if output is not None:
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
output.write_text(rendered, encoding="utf-8")
|
|
print(f"Wrote prospective released API contract to {output}.")
|
|
return 0
|
|
|
|
existing = CONTRACT.read_text(encoding="utf-8")
|
|
if rendered == existing:
|
|
print(f"Released API contract is current for v{version}.")
|
|
return 0
|
|
if args.check:
|
|
print(
|
|
f"Released API contract is out of date for v{version}; "
|
|
f"run `make update-released-api-contract VERSION={version}`.",
|
|
file=sys.stderr,
|
|
)
|
|
return 1
|
|
|
|
previous_exports = set(current["required_top_level_exports"])
|
|
current_exports = set(updated["required_top_level_exports"])
|
|
CONTRACT.write_text(rendered, encoding="utf-8")
|
|
print(f"Updated released API contract for v{version}.")
|
|
print(f"Added exports: {sorted(current_exports - previous_exports)!r}")
|
|
print(f"Removed exports: {sorted(previous_exports - current_exports)!r}")
|
|
print(
|
|
"Review shipped example imports and update released_api_contract_policy.json when "
|
|
"the release adds canonical imports, public properties, public type aliases, public "
|
|
"TypedDict fields, or public modules."
|
|
)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|