Revision 539276395ea2f921bab79cfa6266f224fcdda7d0 authored by Andrey Zhavoronkov on 06 November 2023, 07:19:02 UTC, committed by GitHub on 06 November 2023, 07:19:02 UTC
2 parent s 9819e6d + 7726281
Raw File
update_version.py
#!/usr/bin/env python3

import argparse
import functools
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Callable, Match, Pattern


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}'),
]

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()
back to top