summaryrefslogtreecommitdiff
path: root/script/bump_changelog.py
blob: a08a1aef10e0bd06a565520cf026d22e4ae1a5a3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# 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()