release.py (4498B)
1 # mypy: disallow-untyped-defs 2 """Invoke development tasks.""" 3 4 import argparse 5 import os 6 from pathlib import Path 7 from subprocess import call 8 from subprocess import check_call 9 from subprocess import check_output 10 11 from colorama import Fore 12 from colorama import init 13 14 15 def announce(version: str, template_name: str, doc_version: str) -> None: 16 """Generates a new release announcement entry in the docs.""" 17 # Get our list of authors 18 stdout = check_output(["git", "describe", "--abbrev=0", "--tags"], encoding="UTF-8") 19 last_version = stdout.strip() 20 21 stdout = check_output( 22 ["git", "log", f"{last_version}..HEAD", "--format=%aN"], encoding="UTF-8" 23 ) 24 25 contributors = { 26 name 27 for name in stdout.splitlines() 28 if not name.endswith("[bot]") and name != "pytest bot" 29 } 30 31 template_text = ( 32 Path(__file__).parent.joinpath(template_name).read_text(encoding="UTF-8") 33 ) 34 35 contributors_text = "\n".join(f"* {name}" for name in sorted(contributors)) + "\n" 36 text = template_text.format( 37 version=version, contributors=contributors_text, doc_version=doc_version 38 ) 39 40 target = Path(__file__).parent.joinpath(f"../doc/en/announce/release-{version}.rst") 41 target.write_text(text, encoding="UTF-8") 42 print(f"{Fore.CYAN}[generate.announce] {Fore.RESET}Generated {target.name}") 43 44 # Update index with the new release entry 45 index_path = Path(__file__).parent.joinpath("../doc/en/announce/index.rst") 46 lines = index_path.read_text(encoding="UTF-8").splitlines() 47 indent = " " 48 for index, line in enumerate(lines): 49 if line.startswith(f"{indent}release-"): 50 new_line = indent + target.stem 51 if line != new_line: 52 lines.insert(index, new_line) 53 index_path.write_text("\n".join(lines) + "\n", encoding="UTF-8") 54 print( 55 f"{Fore.CYAN}[generate.announce] {Fore.RESET}Updated {index_path.name}" 56 ) 57 else: 58 print( 59 f"{Fore.CYAN}[generate.announce] {Fore.RESET}Skip {index_path.name} (already contains release)" 60 ) 61 break 62 63 check_call(["git", "add", str(target)]) 64 65 66 def regen(version: str) -> None: 67 """Call regendoc tool to update examples and pytest output in the docs.""" 68 print(f"{Fore.CYAN}[generate.regen] {Fore.RESET}Updating docs") 69 check_call( 70 ["tox", "-e", "regen"], 71 env={**os.environ, "SETUPTOOLS_SCM_PRETEND_VERSION_FOR_PYTEST": version}, 72 ) 73 74 75 def fix_formatting() -> None: 76 """Runs pre-commit in all files to ensure they are formatted correctly""" 77 print( 78 f"{Fore.CYAN}[generate.fix linting] {Fore.RESET}Fixing formatting using pre-commit" 79 ) 80 call(["pre-commit", "run", "--all-files"]) 81 82 83 def check_links() -> None: 84 """Runs sphinx-build to check links""" 85 print(f"{Fore.CYAN}[generate.check_links] {Fore.RESET}Checking links") 86 check_call(["tox", "-e", "docs-checklinks"]) 87 88 89 def pre_release( 90 version: str, template_name: str, doc_version: str, *, skip_check_links: bool 91 ) -> None: 92 """Generates new docs, release announcements and creates a local tag.""" 93 announce(version, template_name, doc_version) 94 regen(version) 95 changelog(version, write_out=True) 96 fix_formatting() 97 if not skip_check_links: 98 check_links() 99 100 msg = f"Prepare release version {version}" 101 check_call(["git", "commit", "-a", "-m", msg]) 102 103 print() 104 print(f"{Fore.CYAN}[generate.pre_release] {Fore.GREEN}All done!") 105 print() 106 print("Please push your branch and open a PR.") 107 108 109 def changelog(version: str, write_out: bool = False) -> None: 110 addopts = [] if write_out else ["--draft"] 111 check_call(["towncrier", "--yes", "--version", version, *addopts]) 112 113 114 def main() -> None: 115 init(autoreset=True) 116 parser = argparse.ArgumentParser() 117 parser.add_argument("version", help="Release version") 118 parser.add_argument( 119 "template_name", help="Name of template file to use for release announcement" 120 ) 121 parser.add_argument( 122 "doc_version", help="For prereleases, the version to link to in the docs" 123 ) 124 parser.add_argument("--skip-check-links", action="store_true", default=False) 125 options = parser.parse_args() 126 pre_release( 127 options.version, 128 options.template_name, 129 options.doc_version, 130 skip_check_links=options.skip_check_links, 131 ) 132 133 134 if __name__ == "__main__": 135 main()