|
| 1 | +"""Keep package metadata and the public client version synchronized.""" |
| 2 | + |
| 3 | +import argparse |
| 4 | +import os |
| 5 | +import re |
| 6 | +from pathlib import Path |
| 7 | +from typing import Dict, Optional, Sequence, Tuple |
| 8 | + |
| 9 | +ROOT = Path(__file__).resolve().parents[1] |
| 10 | +SEMVER = re.compile(r"(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)") |
| 11 | + |
| 12 | + |
| 13 | +def version_files(root: Path) -> Tuple[str, Dict[Path, Tuple[str, re.Match]]]: |
| 14 | + """Read both declarations and reject missing, invalid, or mismatched versions.""" |
| 15 | + files = {} |
| 16 | + versions = [] |
| 17 | + for relative, pattern in ( |
| 18 | + ("pyproject.toml", r'(?ms)^\[project\]\s*\n(?:(?!^\[).)*?^version = "([^"]+)"'), |
| 19 | + ("src/subfork/__init__.py", r'(?m)^__version__ = "([^"]+)"'), |
| 20 | + ): |
| 21 | + path = root / relative |
| 22 | + content = path.read_text(encoding="utf-8") |
| 23 | + matches = list(re.finditer(pattern, content)) |
| 24 | + if len(matches) != 1 or not SEMVER.fullmatch(matches[0].group(1)): |
| 25 | + raise ValueError("Expected one stable x.y.z version in {}.".format(relative)) |
| 26 | + files[path] = (content, matches[0]) |
| 27 | + versions.append(matches[0].group(1)) |
| 28 | + if versions[0] != versions[1]: |
| 29 | + raise ValueError("Version drift between pyproject.toml and __version__; no files changed.") |
| 30 | + return versions[0], files |
| 31 | + |
| 32 | + |
| 33 | +def next_version(current: str, requested: str) -> str: |
| 34 | + """Compute a stable release version, resetting lower components on major/minor bumps.""" |
| 35 | + if SEMVER.fullmatch(requested): |
| 36 | + return requested |
| 37 | + major, minor, patch = map(int, current.split(".")) |
| 38 | + if requested == "major": |
| 39 | + return "{}.0.0".format(major + 1) |
| 40 | + if requested == "minor": |
| 41 | + return "{}.{}.0".format(major, minor + 1) |
| 42 | + if requested == "patch": |
| 43 | + return "{}.{}.{}".format(major, minor, patch + 1) |
| 44 | + raise ValueError("Use major, minor, patch, or a stable x.y.z version.") |
| 45 | + |
| 46 | + |
| 47 | +def main(argv: Optional[Sequence[str]] = None) -> int: |
| 48 | + """Show, check, or update the version without committing or tagging a release.""" |
| 49 | + parser = argparse.ArgumentParser(description=__doc__) |
| 50 | + group = parser.add_mutually_exclusive_group(required=True) |
| 51 | + group.add_argument("--show", action="store_true") |
| 52 | + group.add_argument("--check", action="store_true") |
| 53 | + group.add_argument("--bump", choices=("major", "minor", "patch")) |
| 54 | + group.add_argument("--set", action="store_true", help="Read the explicit version from VERSION") |
| 55 | + args = parser.parse_args(argv) |
| 56 | + try: |
| 57 | + current, files = version_files(ROOT) |
| 58 | + if args.show or args.check: |
| 59 | + print(current if args.show else "Version fields match: " + current) |
| 60 | + return 0 |
| 61 | + requested = os.environ.get("VERSION", "") if args.set else args.bump |
| 62 | + if not requested: |
| 63 | + raise ValueError("Usage: make bump-version VERSION=x.y.z") |
| 64 | + version = next_version(current, requested) |
| 65 | + for path, (content, match) in files.items(): |
| 66 | + updated = content[: match.start(1)] + version + content[match.end(1) :] |
| 67 | + if updated != content: |
| 68 | + path.write_text(updated, encoding="utf-8") |
| 69 | + print("{} -> {}".format(current, version)) |
| 70 | + return 0 |
| 71 | + except (OSError, ValueError) as error: |
| 72 | + parser.exit(1, str(error) + "\n") |
| 73 | + |
| 74 | + |
| 75 | +if __name__ == "__main__": |
| 76 | + raise SystemExit(main()) |
0 commit comments