# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html # For details: https://github.com/pylint-dev/astroid/blob/main/LICENSE # Copyright (c) https://github.com/pylint-dev/astroid/blob/main/CONTRIBUTORS.txt """ This script permits to upgrade the changelog in astroid or pylint when releasing a version. """ # pylint: disable=logging-fstring-interpolation from __future__ import annotations import argparse import enum import logging from datetime import datetime from pathlib import Path DEFAULT_CHANGELOG_PATH = Path("ChangeLog") RELEASE_DATE_TEXT = "Release date: TBA" WHATS_NEW_TEXT = "What's New in astroid" TODAY = datetime.now() FULL_WHATS_NEW_TEXT = WHATS_NEW_TEXT + " {version}?" NEW_RELEASE_DATE_MESSAGE = f"Release date: {TODAY.strftime('%Y-%m-%d')}" def main() -> None: parser = argparse.ArgumentParser(__doc__) parser.add_argument("version", help="The version we want to release") parser.add_argument( "-v", "--verbose", action="store_true", default=False, help="Logging or not" ) args = parser.parse_args() if args.verbose: logging.basicConfig(level=logging.DEBUG) logging.debug(f"Launching bump_changelog with args: {args}") if any(s in args.version for s in ("dev", "a", "b")): return with open(DEFAULT_CHANGELOG_PATH, encoding="utf-8") as f: content = f.read() content = transform_content(content, args.version) with open(DEFAULT_CHANGELOG_PATH, "w", encoding="utf8") as f: f.write(content) class VersionType(enum.Enum): MAJOR = 0 MINOR = 1 PATCH = 2 def get_next_version(version: str, version_type: VersionType) -> str: new_version = version.split(".") part_to_increase = new_version[version_type.value] if "-" in part_to_increase: part_to_increase = part_to_increase.split("-")[0] for i in range(version_type.value, 3): new_version[i] = "0" new_version[version_type.value] = str(int(part_to_increase) + 1) return ".".join(new_version) def get_next_versions(version: str, version_type: VersionType) -> list[str]: if version_type == VersionType.PATCH: # "2.6.1" => ["2.6.2"] return [get_next_version(version, VersionType.PATCH)] if version_type == VersionType.MINOR: # "2.6.0" => ["2.7.0", "2.6.1"] assert version.endswith(".0"), f"{version} does not look like a minor version" else: # "3.0.0" => ["3.1.0", "3.0.1"] assert version.endswith(".0.0"), f"{version} does not look like a major version" next_minor_version = get_next_version(version, VersionType.MINOR) next_patch_version = get_next_version(version, VersionType.PATCH) logging.debug(f"Getting the new version for {version} - {version_type.name}") return [next_minor_version, next_patch_version] def get_version_type(version: str) -> VersionType: if version.endswith(".0.0"): version_type = VersionType.MAJOR elif version.endswith(".0"): version_type = VersionType.MINOR else: version_type = VersionType.PATCH return version_type def get_whats_new( version: str, add_date: bool = False, change_date: bool = False ) -> str: whats_new_text = FULL_WHATS_NEW_TEXT.format(version=version) result = [whats_new_text, "=" * len(whats_new_text)] if add_date and change_date: result += [NEW_RELEASE_DATE_MESSAGE] elif add_date: result += [RELEASE_DATE_TEXT] elif change_date: raise ValueError("Can't use change_date=True with add_date=False") logging.debug( f"version='{version}', add_date='{add_date}', change_date='{change_date}': {result}" ) return "\n".join(result) def get_all_whats_new(version: str, version_type: VersionType) -> str: result = "" for version_ in get_next_versions(version, version_type=version_type): result += get_whats_new(version_, add_date=True) + "\n" * 4 return result def transform_content(content: str, version: str) -> str: version_type = get_version_type(version) next_version = get_next_version(version, version_type) old_date = get_whats_new(version, add_date=True) new_date = get_whats_new(version, add_date=True, change_date=True) next_version_with_date = get_all_whats_new(version, version_type) do_checks(content, next_version, version, version_type) index = content.find(old_date) logging.debug(f"Replacing\n'{old_date}'\nby\n'{new_date}'\n") content = content.replace(old_date, new_date) end_content = content[index:] content = content[:index] logging.debug(f"Adding:\n'{next_version_with_date}'\n") content += next_version_with_date + end_content return content def do_checks(content, next_version, version, version_type): err = "in the changelog, fix that first!" NEW_VERSION_ERROR_MSG = ( "The text for this version '{version}' did not exists %s" % err ) NEXT_VERSION_ERROR_MSG = ( "The text for the next version '{version}' already exists %s" % err ) wn_next_version = get_whats_new(next_version) wn_this_version = get_whats_new(version) # There is only one field where the release date is TBA if version_type in [VersionType.MAJOR, VersionType.MINOR]: assert ( content.count(RELEASE_DATE_TEXT) <= 1 ), f"There should be only one release date 'TBA' ({version}) {err}" else: next_minor_version = get_next_version(version, VersionType.MINOR) assert ( content.count(RELEASE_DATE_TEXT) <= 2 ), f"There should be only two release dates 'TBA' ({version} and {next_minor_version}) {err}" # There is already a release note for the version we want to release assert content.count(wn_this_version) == 1, NEW_VERSION_ERROR_MSG.format( version=version ) # There is no release notes for the next version assert content.count(wn_next_version) == 0, NEXT_VERSION_ERROR_MSG.format( version=next_version ) if __name__ == "__main__": main()