From b34d62117ecb18342b1807f6aedca588c167ecce Mon Sep 17 00:00:00 2001 From: Marco Clemencic <marco.clemencic@cern.ch> Date: Thu, 21 Sep 2023 17:41:11 +0200 Subject: [PATCH] Add utility script to update Gaudi version in various files --- utils/update_version.py | 182 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100755 utils/update_version.py diff --git a/utils/update_version.py b/utils/update_version.py new file mode 100755 index 0000000000..7589f6f15a --- /dev/null +++ b/utils/update_version.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +##################################################################################### +# (c) Copyright 2023 CERN for the benefit of the LHCb and ATLAS collaborations # +# # +# This software is distributed under the terms of the Apache version 2 licence, # +# copied verbatim in the file "LICENSE". # +# # +# In applying this licence, CERN does not waive the privileges and immunities # +# granted to it by virtue of its status as an Intergovernmental Organization # +# or submit itself to any jurisdiction. # +##################################################################################### +import datetime +import re +import sys +from collections.abc import Callable, Iterable +from difflib import unified_diff +from typing import Union + +import click + + +def normalize_version(version: str) -> tuple[str, str]: + """ + Convert a version in format "vXrY" or "X.Y" in the pair ("X.Y", "vXrY"). + + >>> normalize_version("v37r0") + ('37.0', 'v37r0') + >>> normalize_version("37.0.1") + ('37.0.1', 'v37r0p1') + """ + # extract the digits + numbers = re.findall(r"\d+", version) + return ( + ".".join(numbers), + "".join("{}{}".format(*pair) for pair in zip("vrpt", numbers)), + ) + + +class Fields: + """ + Helper to carry the allowed fields for formatting replacement strings. + + >>> f = Fields("v37r1", datetime.date(2023, 9, 25)) + >>> f + Fields('37.1', datetime.date(2023, 9, 25)) + >>> f.data + {'cmake_version': '37.1', 'tag_version': 'v37r1', 'date': datetime.date(2023, 9, 25)} + """ + + def __init__(self, version: str, date: datetime.date): + cmake_version, tag_version = normalize_version(version) + self._data = dict( + cmake_version=cmake_version, + tag_version=tag_version, + date=date, + ) + + def __repr__(self): + return ( + f"Fields({repr(self._data['cmake_version'])}, {repr(self._data['date'])})" + ) + + @property + def data(self): + return self._data + + +class ReplacementRule: + """ + Helper to replace lines with patterns or applying functions. + + >>> r = ReplacementRule(r"^version: ", "version: {cmake_version}") + >>> f = Fields("v1r1", datetime.date(2023, 9, 25)) + >>> r("nothing to change\\n", f) + 'nothing to change\\n' + >>> r("version: 1.0\\n", f) + 'version: 1.1\\n' + """ + + def __init__( + self, + pattern: Union[str, re.Pattern], + replace: Union[str, Callable[[str, Fields], str]], + ): + self.pattern = re.compile(pattern) + if isinstance(replace, str): + replace = f"{replace.rstrip()}\n" + self.replace = lambda _line, fields: replace.format(**fields.data) + else: + self.replace = replace + + def __call__(self, line: str, fields: Fields): + if self.pattern.match(line): + return self.replace(line, fields) + return line + + +class FileUpdater: + def __init__( + self, filename: str, rules: Iterable[Union[ReplacementRule, tuple[str, str]]] + ): + self.filename = filename + self.rules = [ + r if isinstance(r, ReplacementRule) else ReplacementRule(*r) for r in rules + ] + + def _apply_rules(self, line: str, fields: Fields): + for rule in self.rules: + line = rule(line, fields) + return line + + def __call__(self, fields: Fields, dry_run: bool): + with open(self.filename) as f: + old = f.readlines() + data = [self._apply_rules(line, fields) for line in old] + + if old == data: + raise RuntimeError(f"no changes in {self.filename}") + + if dry_run: + sys.stdout.writelines( + unified_diff( + old, + data, + fromfile=f"a/{self.filename}", + tofile=f"b/{self.filename}", + ) + ) + else: + with open(self.filename, "w") as f: + f.writelines(data) + + +@click.command() +@click.argument("version", type=str) +@click.argument( + "date", + type=click.DateTime(("%Y-%m-%d",)), + metavar="[DATE]", + default=datetime.datetime.now(), +) +@click.option( + "--dry-run", + "-n", + default=False, + is_flag=True, + help="only show what would change, but do not modify the files", +) +def update_version(version: str, date: datetime.datetime, dry_run: bool): + """ + Helper to easily update the project version number in all needed files. + """ + fields = Fields(version, date.date()) + click.echo( + "Bumping version to {cmake_version} (tag: {tag_version})".format(**fields.data) + ) + + for updater in [ + FileUpdater( + "CMakeLists.txt", + [(r"^project\(Gaudi VERSION", "project(Gaudi VERSION {cmake_version}")], + ), + FileUpdater( + "CITATION.cff", + [ + (r"^version: ", "version: {tag_version}"), + (r"^date-released: ", "date-released: '{date}'"), + ], + ), + FileUpdater( + "docs/source/conf.py", + [ + (r"^version = ", "version = {cmake_version}"), + (r"^release = ", "release = {tag_version}"), + ], + ), + ]: + updater(fields, dry_run) + + +if __name__ == "__main__": + update_version() -- GitLab