Source code for batools.changelog

# Released under the MIT License. See LICENSE for details.
#
"""Generates a pretty html changelog from our markdown."""

from __future__ import annotations  # Docs-generation hack.

import os
import subprocess


[docs] def changelog_version(version: str) -> str: """Map a build version to its changelog section version. Pre-release suffixes (``1.8.0a1``, ``1.8.0b3``) collapse to their base version (``1.8.0``) so all alphas/betas of a release accumulate in a single changelog section rather than each getting its own. """ import re return re.sub(r'(a|b)\d+$', '', version)
[docs] def get_version_changelog(version: str, projroot: str) -> list[str]: """Get changelog text for a given version from CHANGELOG.md.""" import re from efro.error import CleanError version = changelog_version(version) changelog_path = os.path.join(projroot, 'CHANGELOG.md') if not os.path.exists(changelog_path): raise CleanError(f'CHANGELOG.md not found at {changelog_path}') with open(changelog_path, 'r', encoding='utf-8') as infile: changelog_content = infile.read() # Regex to find the section for the given version pattern = rf'^###\s+{re.escape(version)}\b.*?\n(.*?)(?=^###\s+|\Z)' match = re.search(pattern, changelog_content, re.DOTALL | re.MULTILINE) if not match: raise CleanError(f'Changelog entry for version {version} not found.') section_text = match.group(1).rstrip() # Convert changelog section into a list of bullet entries, # preserving internal newlines and indentation. lines = section_text.splitlines() entries: list[str] = [] current_entry: list[str] = [] for line in lines: if line.startswith('- '): # Save previous entry if present if current_entry: entries.append('\n'.join(current_entry).rstrip()) current_entry = [] # Strip "- " but preserve rest exactly current_entry.append(line[2:]) else: # Continuation line (including indentation or blank lines) if current_entry: current_entry.append(line) # Add final entry if current_entry: entries.append('\n'.join(current_entry).rstrip()) changelog_list = entries return changelog_list
[docs] def get_changelog_releases(projroot: str) -> list[tuple[str, str, str]]: """Get the versions CHANGELOG.md has sections for, newest first. Returns (version, date, heading) tuples, where date is the section's ISO date and heading is the section's heading text (what GitHub derives its anchor from). Old sections whose headings lack a date are skipped. """ import re from efro.error import CleanError changelog_path = os.path.join(projroot, 'CHANGELOG.md') if not os.path.exists(changelog_path): raise CleanError(f'CHANGELOG.md not found at {changelog_path}') with open(changelog_path, 'r', encoding='utf-8') as infile: changelog_content = infile.read() return [ (match.group(2), match.group(3), match.group(1)) for match in re.finditer( r'^###\s+((\d+\.\d+\.\d+)\s+\(.*?(\d{4}-\d{2}-\d{2})\))\s*$', changelog_content, re.MULTILINE, ) ]
[docs] def generate(projroot: str) -> None: """Main script entry point.""" # Make sure we start from root dir (one above this script). os.chdir(projroot) out_path = 'build/changelog.html' out_path_tmp = out_path + '.md' # Do some filtering of our raw changelog. with open('CHANGELOG.md', encoding='utf-8') as infile: lines = infile.read().splitlines() # Strip out anything marked internal. lines = [ line for line in lines if not line.strip().startswith('- (internal)') ] with open(out_path_tmp, 'w', encoding='utf-8') as outfile: outfile.write('\n'.join(lines)) subprocess.run( f'pandoc -f markdown {out_path_tmp} > {out_path}', shell=True, check=True, ) print(f'Generated changelog at \'{out_path}\'.') os.unlink(out_path_tmp)
# Docs-generation hack; import some stuff that we likely only forward-declared # in our actual source code so that docs tools can find it. from typing import (Coroutine, Any, Literal, Callable, Generator, Awaitable, Sequence, Self, TypeIs) import asyncio from concurrent.futures import Future from pathlib import Path from enum import Enum