scripts/prepare_release.py PYTHON 217 lines View on github.com → Search inside
1"""Prepare a release by updating the package version and release notes."""23import re4from datetime import date5from pathlib import Path6from typing import Annotated, Literal78import typer910VERSION_PATTERN = re.compile(r'(?m)^__version__ = "(\d+\.\d+\.\d+)"$')11VERSION_HEADING_PATTERN = re.compile(r"(?m)^## (\d+\.\d+\.\d+)(?: \([^)]+\))?$")12RELEASE_NOTES_HEADER = """---13hide:14  - navigation15---1617# Release Notes1819"""20LATEST_CHANGES_HEADER = "## Latest Changes"21BumpType = Literal["major", "minor", "patch"]2223app = typer.Typer()242526def parse_version(version: str) -> tuple[int, int, int]:27    match = re.fullmatch(r"\d+\.\d+\.\d+", version)28    if not match:29        raise ValueError(f"Invalid version: {version!r}. Expected format: X.Y.Z")30    major, minor, patch = version.split(".")31    return int(major), int(minor), int(patch)323334def get_current_version(content: str, version_file: Path) -> str:35    matches = list(VERSION_PATTERN.finditer(content))36    if len(matches) != 1:37        raise RuntimeError(38            f"Expected exactly one __version__ assignment in {version_file}, "39            f"found {len(matches)}"40        )41    return matches[0].group(1)424344def bump_version(version: str, bump: BumpType) -> str:45    major, minor, patch = parse_version(version)46    if bump == "major":47        return f"{major + 1}.0.0"48    if bump == "minor":49        return f"{major}.{minor + 1}.0"50    return f"{major}.{minor}.{patch + 1}"515253def update_version_file(content: str, version: str, version_file: Path) -> str:54    current_version = get_current_version(content, version_file)55    if parse_version(version) <= parse_version(current_version):56        raise RuntimeError(57            f"New version {version} must be greater than current version {current_version}"58        )59    return VERSION_PATTERN.sub(f'__version__ = "{version}"', content, count=1)606162def update_release_notes(63    content: str, version: str, release_date: date, release_notes_file: Path64) -> str:65    if not content.startswith(RELEASE_NOTES_HEADER):66        raise RuntimeError(67            f"{release_notes_file} must start with {RELEASE_NOTES_HEADER!r}"68        )69    if re.search(rf"^## {re.escape(version)}(?: \([^)]+\))?$", content, re.M):70        raise RuntimeError(f"Release notes already contain a section for {version}")7172    latest_header = f"{RELEASE_NOTES_HEADER}{LATEST_CHANGES_HEADER}\n"73    if not content.startswith(latest_header):74        raise RuntimeError(f"{release_notes_file} must start with {latest_header!r}")7576    release_header = f"## {version} ({release_date.isoformat()})"77    return content.replace(78        latest_header,79        f"{RELEASE_NOTES_HEADER}{LATEST_CHANGES_HEADER}\n\n{release_header}\n",80        1,81    )828384def get_release_notes_body(content: str, version: str, release_notes_file: Path) -> str:85    version_heading = re.compile(rf"(?m)^## {re.escape(version)}(?: \([^)]+\))?$")86    match = version_heading.search(content)87    if not match:88        raise RuntimeError(89            f"Could not find release notes section for {version} in {release_notes_file}"90        )9192    next_match = VERSION_HEADING_PATTERN.search(content, match.end())93    end = next_match.start() if next_match else len(content)94    body = content[match.end() : end].strip()95    if not body:96        raise RuntimeError(97            f"Release notes section for {version} in {release_notes_file} is empty"98        )99    return f"{body}\n"100101102@app.command()103def prepare(104    bump: Annotated[105        BumpType,106        typer.Argument(107            envvar="PREPARE_RELEASE_BUMP",108            help="The release bump to make: major, minor, or patch.",109        ),110    ],111    version_file: Annotated[112        Path,113        typer.Option(114            envvar="PREPARE_RELEASE_VERSION_FILE",115            exists=True,116            file_okay=True,117            dir_okay=False,118            readable=True,119            writable=True,120            help="Path to the Python file containing the __version__ assignment.",121        ),122    ],123    release_notes_file: Annotated[124        Path,125        typer.Option(126            envvar="PREPARE_RELEASE_RELEASE_NOTES_FILE",127            exists=True,128            file_okay=True,129            dir_okay=False,130            readable=True,131            writable=True,132            help="Path to the release notes Markdown file.",133        ),134    ],135    release_date: Annotated[136        str,137        typer.Option(138            "--date",139            envvar="PREPARE_RELEASE_DATE",140            help="Release date in YYYY-MM-DD format. Defaults to today.",141        ),142    ] = date.today().isoformat(),143) -> None:144    parsed_release_date = date.fromisoformat(release_date or date.today().isoformat())145146    version_file_content = version_file.read_text()147    release_notes_content = release_notes_file.read_text()148    version = bump_version(149        get_current_version(version_file_content, version_file), bump150    )151152    version_file.write_text(153        update_version_file(version_file_content, version, version_file)154    )155    release_notes_file.write_text(156        update_release_notes(157            release_notes_content, version, parsed_release_date, release_notes_file158        )159    )160161    typer.echo(f"Prepared release {version} ({parsed_release_date.isoformat()})")162163164@app.command()165def current_version(166    version_file: Annotated[167        Path,168        typer.Option(169            envvar="PREPARE_RELEASE_VERSION_FILE",170            exists=True,171            file_okay=True,172            dir_okay=False,173            readable=True,174            help="Path to the Python file containing the __version__ assignment.",175        ),176    ],177) -> None:178    typer.echo(get_current_version(version_file.read_text(), version_file))179180181@app.command()182def release_notes(183    version_file: Annotated[184        Path,185        typer.Option(186            envvar="PREPARE_RELEASE_VERSION_FILE",187            exists=True,188            file_okay=True,189            dir_okay=False,190            readable=True,191            help="Path to the Python file containing the __version__ assignment.",192        ),193    ],194    release_notes_file: Annotated[195        Path,196        typer.Option(197            envvar="PREPARE_RELEASE_RELEASE_NOTES_FILE",198            exists=True,199            file_okay=True,200            dir_okay=False,201            readable=True,202            help="Path to the release notes Markdown file.",203        ),204    ],205) -> None:206    version = get_current_version(version_file.read_text(), version_file)207    typer.echo(208        get_release_notes_body(209            release_notes_file.read_text(), version, release_notes_file210        ),211        nl=False,212    )213214215if __name__ == "__main__":216    app()

Code quality findings 6

Ensure functions have docstrings for documentation
info maintainability missing-docstring
def parse_version(version: str) -> tuple[int, int, int]:
Ensure functions have docstrings for documentation
info maintainability missing-docstring
def get_current_version(content: str, version_file: Path) -> str:
Ensure functions have docstrings for documentation
info maintainability missing-docstring
def bump_version(version: str, bump: BumpType) -> str:
Ensure functions have docstrings for documentation
info maintainability missing-docstring
def update_version_file(content: str, version: str, version_file: Path) -> str:
Ensure functions have docstrings for documentation
info maintainability missing-docstring
def update_release_notes(
Ensure functions have docstrings for documentation
info maintainability missing-docstring
def get_release_notes_body(content: str, version: str, release_notes_file: Path) -> str:

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.

Generated by searchcode.com · powered by code_get_file + code_get_findings