release.py: simplify bumping the version

Additionally, update the 'in progress' header in the changelog: instead
of using a specific version number, simply say 'Unreleased' since we do
not know in advance what version will the changes be eventually
released.

This also simplifies latest changelog retrieval.
This commit is contained in:
Šarūnas Nejus 2024-06-04 13:02:57 +01:00
parent be778b8da0
commit 3e5e1eca87
No known key found for this signature in database
GPG key ID: DD28F6704DBE3435
2 changed files with 67 additions and 138 deletions

View file

@ -1,8 +1,8 @@
Changelog Changelog
========= =========
2.0.1 (in development) Unreleased
---------------------- ----------
Changelog goes here! Please add your entry to the bottom of one of the lists below! Changelog goes here! Please add your entry to the bottom of one of the lists below!

View file

@ -2,156 +2,86 @@
"""A utility script for automating the beets release process. """A utility script for automating the beets release process.
""" """
import datetime from __future__ import annotations
import os
import re import re
from datetime import datetime, timezone
from pathlib import Path
from typing import Callable
import click import click
from packaging.version import Version, parse
BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE = Path(__file__).parent.parent.absolute()
CHANGELOG = os.path.join(BASE, "docs", "changelog.rst") BEETS_INIT = BASE / "beets" / "__init__.py"
# Locations (filenames and patterns) of the version number.
VERSION_LOCS = [ def update_docs_config(text: str, new: Version) -> str:
new_major_minor = f"{new.major}.{new.minor}"
text = re.sub(r"(?<=version = )[^\n]+", f'"{new_major_minor}"', text)
return re.sub(r"(?<=release = )[^\n]+", f'"{new}"', text)
def update_changelog(text: str, new: Version) -> str:
new_header = f"{new} ({datetime.now(timezone.utc).date():%B %d, %Y})"
return re.sub(
# do not match if the new version is already present
r"\nUnreleased\n--+\n",
rf"""
Unreleased
----------
Changelog goes here! Please add your entry to the bottom of one of the lists below!
{new_header}
{'-' * len(new_header)}
""",
text,
)
UpdateVersionCallable = Callable[[str, Version], str]
FILENAME_AND_UPDATE_TEXT: list[tuple[Path, UpdateVersionCallable]] = [
( (
os.path.join(BASE, "beets", "__init__.py"), BEETS_INIT,
[ lambda text, new: re.sub(
( r"(?<=__version__ = )[^\n]+", f'"{new}"', text
r'__version__\s*=\s*[\'"]([0-9\.]+)[\'"]', ),
'__version__ = "{version}"',
)
],
),
(
os.path.join(BASE, "docs", "conf.py"),
[
(
r'version\s*=\s*[\'"]([0-9\.]+)[\'"]',
'version = "{minor}"',
),
(
r'release\s*=\s*[\'"]([0-9\.]+)[\'"]',
'release = "{version}"',
),
],
),
(
os.path.join(BASE, "setup.py"),
[
(
r'\s*version\s*=\s*[\'"]([0-9\.]+)[\'"]',
' version="{version}",',
)
],
), ),
(BASE / "docs" / "changelog.rst", update_changelog),
(BASE / "docs" / "conf.py", update_docs_config),
] ]
GITHUB_USER = "beetbox" GITHUB_USER = "beetbox"
GITHUB_REPO = "beets" GITHUB_REPO = "beets"
def bump_version(version: str): def validate_new_version(
"""Update the version number in setup.py, docs config, changelog, ctx: click.Context, param: click.Argument, value: Version
and root module. ) -> Version:
""" """Validate the version is newer than the current one."""
version_parts = [int(p) for p in version.split(".")] with BEETS_INIT.open() as f:
assert len(version_parts) == 3, "invalid version number" contents = f.read()
minor = "{}.{}".format(*version_parts)
major = "{}".format(*version_parts)
# Replace the version each place where it lives. m = re.search(r'(?<=__version__ = ")[^"]+', contents)
for filename, locations in VERSION_LOCS: assert m, "Current version not found in __init__.py"
# Read and transform the file. current = parse(m.group())
out_lines = []
with open(filename) as f:
found = False
for line in f:
for pattern, template in locations:
match = re.match(pattern, line)
if match:
# Check that this version is actually newer.
old_version = match.group(1)
old_parts = [int(p) for p in old_version.split(".")]
assert (
version_parts > old_parts
), "version must be newer than {}".format(old_version)
# Insert the new version. if not value > current:
out_lines.append( msg = f"version must be newer than {current}"
template.format( raise click.BadParameter(msg)
version=version,
major=major,
minor=minor,
)
+ "\n"
)
found = True return value
break
else:
# Normal line.
out_lines.append(line)
if not found:
print(f"No pattern found in {filename}")
# Write the file back.
with open(filename, "w") as f:
f.write("".join(out_lines))
update_changelog(version)
def update_changelog(version: str): def bump_version(new: Version) -> None:
# Generate bits to insert into changelog. """Update the version number in specified files."""
header_line = f"In Development" for path, perform_update in FILENAME_AND_UPDATE_TEXT:
header = "\n\n" + header_line + "\n" + "-" * len(header_line) + "\n\n" with path.open("r+") as f:
header += ( contents = f.read()
"Changelog goes here! Please add your entry to the bottom of" f.seek(0)
" one of the lists below!\n" f.write(perform_update(contents, new))
) f.truncate()
# Insert into the right place.
with open(CHANGELOG) as f:
contents = f.readlines()
contents = [
line
for line in contents
if not re.match(r"Changelog goes here!.*", line)
]
contents = "".join(contents)
contents = re.sub("\n{3,}", "\n\n", contents)
location = contents.find("\n\n") # First blank line.
contents = contents[:location] + header + contents[location:]
# Write back.
with open(CHANGELOG, "w") as f:
f.write(contents)
def datestamp():
"""Enter today's date as the release date in the changelog."""
dt = datetime.datetime.now()
stamp = "({} {}, {})".format(dt.strftime("%B"), dt.day, dt.year)
marker = "(in development)"
lines = []
underline_length = None
with open(CHANGELOG) as f:
for line in f:
if marker in line:
# The header line.
line = line.replace(marker, stamp)
lines.append(line)
underline_length = len(line.strip())
elif underline_length:
# This is the line after the header. Rewrite the dashes.
lines.append("-" * underline_length + "\n")
underline_length = None
else:
lines.append(line)
with open(CHANGELOG, "w") as f:
for line in lines:
f.write(line)
@click.group() @click.group()
@ -160,10 +90,9 @@ def cli():
@cli.command() @cli.command()
@click.argument("version") @click.argument("version", type=Version, callback=validate_new_version)
def bump(version: str) -> None: def bump(version: Version) -> None:
# Version number bump. """Bump the version in project files."""
datestamp()
bump_version(version) bump_version(version)