From bd130ecd7ec5220c5371e2e056507ca8b1f02219 Mon Sep 17 00:00:00 2001 From: DudeNr33 <3929834+DudeNr33@users.noreply.github.com> Date: Sun, 17 Jul 2022 17:31:41 +0200 Subject: Edit `bump_changelog.py` to use `towncrier` and update its configuration --- script/bump_changelog.py | 245 +++++++++++++++++++---------------------------- 1 file changed, 100 insertions(+), 145 deletions(-) (limited to 'script') diff --git a/script/bump_changelog.py b/script/bump_changelog.py index f27195bdc..420ca1d22 100644 --- a/script/bump_changelog.py +++ b/script/bump_changelog.py @@ -2,167 +2,122 @@ # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt -# ORIGINAL here: https://github.com/PyCQA/astroid/blob/main/script/bump_changelog.py -# DO NOT MODIFY DIRECTLY - -"""This script permits to upgrade the changelog in astroid or pylint when releasing a version.""" -# pylint: disable=logging-fstring-interpolation - +"""This script updates towncrier.toml and creates a new newsfile and intermediate +folders if necessary. +""" from __future__ import annotations import argparse -import enum -import logging -from datetime import datetime +import re from pathlib import Path +from subprocess import check_call + +NEWSFILE_PATTERN = re.compile(r"doc/whatsnew/\d/\d.\d+/index\.rst") +NEWSFILE_PATH = "doc/whatsnew/{major}/{major}.{minor}/index.rst" +TOWNCRIER_CONFIG_FILE = Path("towncrier.toml") +TOWNCRIER_VERSION_PATTERN = re.compile(r"version = \"(\d+\.\d+\.\d+)\"") + +NEWSFILE_CONTENT_TEMPLATE = """ +*************************** + What's New in Pylint {major}.{minor} +*************************** -# TODO: 2.15.0 Modify the way we handle the patch version -# release notes -DEFAULT_CHANGELOG_PATH = Path("doc/whatsnew/2/2.14/full.rst") +.. toctree:: + :maxdepth: 2 -RELEASE_DATE_TEXT = "Release date: TBA" -WHATS_NEW_TEXT = "What's New in Pylint" -TODAY = datetime.now() -FULL_WHATS_NEW_TEXT = WHATS_NEW_TEXT + " {version}?" -NEW_RELEASE_DATE_MESSAGE = f"Release date: {TODAY.strftime('%Y-%m-%d')}" +:Release:{major}.{minor} +:Date: TBA + +Summary -- Release highlights +============================= + + +.. towncrier release notes start +""" def main() -> None: - parser = argparse.ArgumentParser(__doc__) - parser.add_argument("version", help="The version we want to release") + parser = argparse.ArgumentParser() + parser.add_argument("version", help="The new version to set") parser.add_argument( - "-v", "--verbose", action="store_true", default=False, help="Logging or not" + "--dry-run", + action="store_true", + help="Just show what would be done, don't write anything", ) args = parser.parse_args() - if args.verbose: - logging.basicConfig(level=logging.DEBUG) - logging.debug(f"Launching bump_changelog with args: {args}") + if "dev" in args.version: - 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="utf-8") 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 = ( - # pylint: disable-next=consider-using-f-string - "The text for this version '{version}' did not exists %s" - % err + print("'-devXY' will be cut from version in towncrier.toml") + match = re.match( + r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-\w+\d*)*", args.version ) - NEXT_VERSION_ERROR_MSG = ( - # pylint: disable-next=consider-using-f-string - "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 + if not match: + print( + "Fatal error - new version did not match the " + "expected format (major.minor.patch[.*]). Abort!" + ) + return + major, minor, patch, suffix = match.groups() + new_version = f"{major}.{minor}.{patch}" + + new_newsfile = NEWSFILE_PATH.format(major=major, minor=minor) + create_new_newsfile_if_necessary(new_newsfile, major, minor, args.dry_run) + patch_towncrier_toml(new_newsfile, new_version, args.dry_run) + build_changelog(suffix, args.dry_run) + + +def create_new_newsfile_if_necessary( + new_newsfile: str, major: str, minor: str, dry_run: bool +) -> None: + new_newsfile_path = Path(new_newsfile) + if new_newsfile_path.exists(): + return + + # create new file and add boiler plate content + if dry_run: + print( + f"Dry run enabled - would create file {new_newsfile} " + "and intermediate folders" + ) + return + + print("Creating new newsfile:", new_newsfile) + new_newsfile_path.parent.mkdir(parents=True, exist_ok=True) + new_newsfile_path.touch() + new_newsfile_path.write_text( + NEWSFILE_CONTENT_TEMPLATE.format(major=major, minor=minor), + encoding="utf8", ) - # There is no release notes for the next version - assert content.count(wn_next_version) == 0, NEXT_VERSION_ERROR_MSG.format( - version=next_version + + # tbump does not add and commit new files, so we add it ourselves + print("Adding new newsfile to git") + check_call(["git", "add", new_newsfile]) + + +def patch_towncrier_toml(new_newsfile: str, version: str, dry_run: bool) -> None: + file_content = TOWNCRIER_CONFIG_FILE.read_text(encoding="utf-8") + patched_newsfile_path = NEWSFILE_PATTERN.sub(new_newsfile, file_content) + new_file_content = TOWNCRIER_VERSION_PATTERN.sub( + f'version = "{version}"', patched_newsfile_path ) + if dry_run: + print("Dry run enabled - this is what I would write:\n") + print(new_file_content) + return + TOWNCRIER_CONFIG_FILE.write_text(new_file_content, encoding="utf-8") + + +def build_changelog(suffix: str | None, dry_run: bool) -> None: + if suffix: + print("Not a release version, skipping changelog generation") + return + + if dry_run: + print("Dry run enabled - not building changelog") + return + + print("Building changelog") + check_call(["towncrier", "build", "--yes"]) if __name__ == "__main__": -- cgit v1.2.1