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
|
"""Internal tool to update the changelog."""
import subprocess
from dataclasses import dataclass
from datetime import datetime
from typing import List
@dataclass(frozen=True)
class Change:
"""Capture the data of a git commit."""
commit_hash: str
prefix: str
message: str
def main(changelog_path: str):
"""Create a changelog."""
changelog = get_changelog(changelog_path)
git_tag = get_most_recent_git_tag()
changes = get_formatted_changes(git_tag)
print("-" * 80)
print(changes)
new_version = version_bump(git_tag)
today = datetime.now()
header = f"Version {new_version}, {today:%Y-%m-%d}\n"
header = header + "-" * (len(header) - 1) + "\n"
trailer = f"\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/{git_tag}...{new_version})\n\n"
new_entry = header + changes + trailer
print(new_entry)
# TODO: Make idempotent - multiple calls to this script
# should not change the changelog
new_changelog = new_entry + changelog
write_changelog(new_changelog, changelog_path)
def version_bump(git_tag: str) -> str:
# just assume a patch version change
major, minor, patch = git_tag.split(".")
return f"{major}.{minor}.{int(patch) + 1}"
def get_changelog(changelog_path: str) -> str:
with open(changelog_path) as fh:
changelog = fh.read()
return changelog
def write_changelog(new_changelog: str, changelog_path: str) -> None:
with open(changelog_path, "w") as fh:
fh.write(new_changelog)
def get_formatted_changes(git_tag: str) -> str:
commits = get_git_commits_since_tag(git_tag)
# Group by prefix
grouped = {}
for commit in commits:
if commit.prefix not in grouped:
grouped[commit.prefix] = []
grouped[commit.prefix].append({"msg": commit.message})
# Order prefixes
order = ["DEP", "ENH", "PI", "BUG", "ROB", "DOC", "DEV", "MAINT", "TST", "STY"]
abbrev2long = {
"DEP": "Deprecations",
"ENH": "New Features",
"BUG": "Bug Fixes",
"ROB": "Robustness",
"DOC": "Documentation",
"DEV": "Developer Experience",
"MAINT": "Maintenance",
"TST": "Testing",
"STY": "Code Style",
"PI": "Performance Improvements",
}
# Create output
output = ""
for prefix in order:
if prefix not in grouped:
continue
output += f"\n{abbrev2long[prefix]} ({prefix}):\n" # header
for commit in grouped[prefix]:
output += f"- {commit['msg']}\n"
del grouped[prefix]
if grouped:
print("@" * 80)
output += "\nYou forgot something!:\n"
for prefix in grouped:
output += f"- {prefix}: {grouped[prefix]}\n"
print("@" * 80)
return output
def get_most_recent_git_tag():
git_tag = str(
subprocess.check_output(
["git", "describe", "--abbrev=0"], stderr=subprocess.STDOUT
)
).strip("'b\\n")
return git_tag
def get_git_commits_since_tag(git_tag) -> List[Change]:
commits = str(
subprocess.check_output(
[
"git",
"--no-pager",
"log",
f"{git_tag}..HEAD",
'--pretty=format:"%h%x09%s"',
],
stderr=subprocess.STDOUT,
)
).strip("'b\\n")
return [parse_commit_line(line) for line in commits.split("\\n")]
def parse_commit_line(line) -> Change:
if "\\t" not in line:
raise ValueError(f"Invalid commit line: {line}")
commit_hash, rest = line.split("\\t", 1)
if ":" in rest:
prefix, message = rest.split(":", 1)
else:
prefix = ""
message = rest
# Standardize
message.strip()
if message.endswith('"'):
message = message[:-1]
prefix = prefix.strip()
if prefix == "DOCS":
prefix = "DOC"
return Change(commit_hash=commit_hash, prefix=prefix, message=message)
if __name__ == "__main__":
main("CHANGELOG.md")
|