cvat/dev/update_version.py

278 lines
7.9 KiB
Python
Executable File

#!/usr/bin/env python3
import argparse
import functools
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from re import Match, Pattern
from typing import Callable
SUCCESS_CHAR = "\u2714"
FAIL_CHAR = "\u2716"
CVAT_VERSION_PATTERN = re.compile(
r"VERSION\s*=\s*\((\d+),\s*(\d*),\s*(\d+),\s*[\',\"](\w+)[\',\"],\s*(\d+)\)"
)
REPO_ROOT_DIR = Path(__file__).resolve().parents[1]
CVAT_INIT_PY_REL_PATH = "cvat/__init__.py"
CVAT_INIT_PY_PATH = REPO_ROOT_DIR / CVAT_INIT_PY_REL_PATH
@dataclass()
class Version:
major: int = 0
minor: int = 0
patch: int = 0
prerelease: str = ""
prerelease_number: int = 0
def __str__(self) -> str:
return f"{self.major}.{self.minor}.{self.patch}-{self.prerelease}.{self.prerelease_number}"
def cvat_repr(self):
return f'({self.major}, {self.minor}, {self.patch}, "{self.prerelease}", {self.prerelease_number})'
def compose_repr(self):
if self.prerelease != "final":
return "dev"
return f"v{self.major}.{self.minor}.{self.patch}"
def increment_prerelease_number(self) -> None:
self.prerelease_number += 1
def increment_prerelease(self) -> None:
flow = ("alpha", "beta", "rc", "final")
idx = flow.index(self.prerelease)
if idx == len(flow) - 1:
raise ValueError(f"Cannot increment current '{self.prerelease}' prerelease version")
self.prerelease = flow[idx + 1]
self._set_default_prerelease_number()
def set_prerelease(self, value: str) -> None:
values = ("alpha", "beta", "rc", "final")
if value not in values:
raise ValueError(f"{value} is a wrong, must be one of {values}")
self.prerelease = value
self._set_default_prerelease_number()
def increment_patch(self) -> None:
self.patch += 1
self._set_default_prerelease()
def increment_minor(self) -> None:
self.minor += 1
self._set_default_patch()
def increment_major(self) -> None:
self.major += 1
self._set_default_minor()
def set(self, v: str) -> None:
self.major, self.minor, self.patch = map(int, v.split("."))
self.prerelease = "final"
self.prerelease_number = 0
def _set_default_prerelease_number(self) -> None:
self.prerelease_number = 0
def _set_default_prerelease(self) -> None:
self.prerelease = "alpha"
self._set_default_prerelease_number()
def _set_default_patch(self) -> None:
self.patch = 0
self._set_default_prerelease()
def _set_default_minor(self) -> None:
self.minor = 0
self._set_default_patch()
@dataclass(frozen=True)
class ReplacementRule:
rel_path: str
pattern: Pattern[str]
replacement: Callable[[Version, Match[str]], str]
def apply(self, new_version: Version, *, verify_only: bool) -> bool:
path = REPO_ROOT_DIR / self.rel_path
text = path.read_text()
new_text, num_replacements = self.pattern.subn(
functools.partial(self.replacement, new_version), text
)
if not num_replacements:
print(f"{FAIL_CHAR} {self.rel_path}: failed to match version pattern.")
return False
if text == new_text:
if verify_only:
print(f"{SUCCESS_CHAR} {self.rel_path}: verified.")
else:
print(f"{SUCCESS_CHAR} {self.rel_path}: no need to update.")
else:
if verify_only:
print(f"{FAIL_CHAR} {self.rel_path}: verification failed.")
return False
else:
path.write_text(new_text)
print(f"{SUCCESS_CHAR} {self.rel_path}: updated.")
return True
REPLACEMENT_RULES = [
ReplacementRule(
CVAT_INIT_PY_REL_PATH, CVAT_VERSION_PATTERN, lambda v, m: f"VERSION = {v.cvat_repr()}"
),
ReplacementRule(
"docker-compose.yml",
re.compile(r"(\$\{CVAT_VERSION:-)([\w.]+)(\})"),
lambda v, m: m[1] + v.compose_repr() + m[3],
),
ReplacementRule(
"helm-chart/values.yaml",
re.compile(r"(^ image: cvat/(?:ui|server)\n tag: )([\w.]+)", re.M),
lambda v, m: m[1] + v.compose_repr(),
),
ReplacementRule(
"cvat-sdk/gen/generate.sh",
re.compile(r'^VERSION="[\d.]+"$', re.M),
lambda v, m: f'VERSION="{v.major}.{v.minor}.{v.patch}"',
),
ReplacementRule(
"cvat/schema.yml",
re.compile(r"^ version: [\d.]+$", re.M),
lambda v, m: f" version: {v.major}.{v.minor}.{v.patch}",
),
ReplacementRule(
"cvat-cli/src/cvat_cli/version.py",
re.compile(r'^VERSION = "[\d.]+"$', re.M),
lambda v, m: f'VERSION = "{v.major}.{v.minor}.{v.patch}"',
),
ReplacementRule(
"cvat-cli/requirements/base.txt",
re.compile(r"^cvat-sdk==[\d.]+$", re.M),
lambda v, m: f"cvat-sdk=={v.major}.{v.minor}.{v.patch}",
),
ReplacementRule(
"cvat-ui/package.json",
re.compile(r'^ "version": "[\d.]+",$', re.M),
lambda v, m: f' "version": "{v.major}.{v.minor}.{v.patch}",',
),
]
def get_current_version() -> Version:
version_text = CVAT_INIT_PY_PATH.read_text()
match = re.search(CVAT_VERSION_PATTERN, version_text)
if not match:
raise RuntimeError(f"Failed to find version in {CVAT_INIT_PY_PATH}")
return Version(int(match[1]), int(match[2]), int(match[3]), match[4], int(match[5]))
def main() -> None:
parser = argparse.ArgumentParser(description="Bump CVAT version")
action_group = parser.add_mutually_exclusive_group(required=True)
action_group.add_argument(
"--major", action="store_true", help="Increment the existing major version by 1"
)
action_group.add_argument(
"--minor", action="store_true", help="Increment the existing minor version by 1"
)
action_group.add_argument(
"--patch", action="store_true", help="Increment the existing patch version by 1"
)
action_group.add_argument(
"--prerelease",
nargs="?",
const="increment",
help="""Increment prerelease version alpha->beta->rc->final,
Also it's possible to pass value explicitly""",
)
action_group.add_argument(
"--prerelease_number", action="store_true", help="Increment prerelease number by 1"
)
action_group.add_argument(
"--current", "--show-current", action="store_true", help="Display current version"
)
action_group.add_argument(
"--verify-current",
action="store_true",
help="Check that all version numbers are consistent",
)
action_group.add_argument(
"--set", metavar="X.Y.Z", help="Set the version to the specified version"
)
args = parser.parse_args()
version = get_current_version()
verify_only = False
if args.current:
print(version)
return
elif args.verify_current:
verify_only = True
elif args.prerelease_number:
version.increment_prerelease_number()
elif args.prerelease:
if args.prerelease == "increment":
version.increment_prerelease()
else:
version.set_prerelease(args.prerelease)
elif args.patch:
version.increment_patch()
elif args.minor:
version.increment_minor()
elif args.major:
version.increment_major()
elif args.set is not None:
version.set(args.set)
else:
assert False, "Unreachable code"
if verify_only:
print(f"Verifying that version is {version}...")
else:
print(f"Bumping version to {version}...")
print()
success = True
for rule in REPLACEMENT_RULES:
if not rule.apply(version, verify_only=verify_only):
success = False
if not success:
if verify_only:
sys.exit("\nFailed to verify one or more files!")
else:
sys.exit("\nFailed to update one or more files!")
if __name__ == "__main__":
main()