From 7977f169fe06aada14142ac83aae7365c180fb23 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 16 Aug 2024 16:34:08 +0200 Subject: [PATCH] Update versionadded:: next (and similar directives) on release --- release.py | 10 ++++ run_release.py | 9 ++++ tests/test_update_version_next.py | 87 +++++++++++++++++++++++++++++++ update_version_next.py | 86 ++++++++++++++++++++++++++++++ 4 files changed, 192 insertions(+) create mode 100644 tests/test_update_version_next.py create mode 100644 update_version_next.py diff --git a/release.py b/release.py index d8e8552..45d3e31 100755 --- a/release.py +++ b/release.py @@ -209,6 +209,16 @@ def committed_at(self) -> datetime.datetime: int(proc.stdout.decode().strip()), tz=datetime.timezone.utc ) + @property + def doc_version(self) -> str: + """Text used for notes in docs like 'Added in x.y'""" + # - ignore levels (alpha/beta/rc are preparatiomn for the full release) + # - use just X.Y for patch 0 + if self.patch == 0: + return f"{self.major}.{self.minor}" + else: + return f"{self.major}.{self.minor}.{self.patch}" + def error(*msgs: str) -> None: print("**ERROR**", file=sys.stderr) diff --git a/run_release.py b/run_release.py index b2b3b08..69b5b85 100755 --- a/run_release.py +++ b/run_release.py @@ -33,6 +33,7 @@ import sbom from buildbotapi import BuildBotAPI, Builder from release import ReleaseShelf, Tag, Task +import update_version_next API_KEY_REGEXP = re.compile(r"(?P\w+):(?P\w+)") RELEASE_REGEXP = re.compile( @@ -497,6 +498,13 @@ def bump_version(db: ReleaseShelf) -> None: ) +def bump_version_in_docs(db: ReleaseShelf) -> None: + update_version_next.main([db['release'].doc_version, str(db["git_repo"])]) + subprocess.check_call( + ["git", "commit", "-a", "--amend", "--no-edit"], cwd=db["git_repo"] + ) + + def create_tag(db: ReleaseShelf) -> None: with cd(db["git_repo"]): if not release_mod.make_tag(db["release"], sign_gpg=db["sign_gpg"]): @@ -1251,6 +1259,7 @@ def _api_key(api_key: str) -> str: Task(check_cpython_repo_is_clean, "Checking Git repository is clean"), Task(prepare_pydoc_topics, "Preparing pydoc topics"), Task(bump_version, "Bump version"), + Task(bump_version_in_docs, "Bump version in docs"), Task(check_cpython_repo_is_clean, "Checking Git repository is clean"), Task(run_autoconf, "Running autoconf"), Task(check_cpython_repo_is_clean, "Checking Git repository is clean"), diff --git a/tests/test_update_version_next.py b/tests/test_update_version_next.py new file mode 100644 index 0000000..37bdc32 --- /dev/null +++ b/tests/test_update_version_next.py @@ -0,0 +1,87 @@ +"""Tests for the update_version_next tool.""" + +from pathlib import Path +import unittest + +from test.support import os_helper + +import update_version_next + +TO_CHANGE = """ +Directives to change +-------------------- + +Here, all occurences of NEXT (lowercase) should be changed: + +.. versionadded:: next + +.. versionchanged:: next + +.. deprecated:: next + +.. deprecated-removed:: next 4.0 + +whitespace: + +.. versionchanged:: next + +.. versionchanged :: next + + .. versionadded:: next + +arguments: + +.. versionadded:: next + Foo bar + +.. versionadded:: next as ``previousname`` +""" + +UNCHANGED = """ +Unchanged +--------- + +Here, the word "next" should NOT be changed: + +.. versionchanged:: NEXT + +..versionchanged:: NEXT + +... versionchanged:: next + +foo .. versionchanged:: next + +.. otherdirective:: next + +.. VERSIONCHANGED: next + +.. deprecated-removed: 3.0 next +""" + +EXPECTED_CHANGED = TO_CHANGE.replace('next', 'VER') + + +class TestVersionNext(unittest.TestCase): + maxDiff = len(TO_CHANGE + UNCHANGED) * 10 + + def test_freeze_simple_script(self): + with os_helper.temp_dir() as testdir: + path = Path(testdir) + path.joinpath('source.rst').write_text(TO_CHANGE + UNCHANGED) + path.joinpath('subdir').mkdir() + path.joinpath('subdir/change.rst').write_text( + '.. versionadded:: next') + path.joinpath('subdir/keep.not-rst').write_text( + '.. versionadded:: next') + path.joinpath('subdir/keep.rst').write_text( + 'nothing to see here') + args = ['VER', testdir] + update_version_next.main(args) + self.assertEqual(path.joinpath('source.rst').read_text(), + EXPECTED_CHANGED + UNCHANGED) + self.assertEqual(path.joinpath('subdir/change.rst').read_text(), + '.. versionadded:: VER') + self.assertEqual(path.joinpath('subdir/keep.not-rst').read_text(), + '.. versionadded:: next') + self.assertEqual(path.joinpath('subdir/keep.rst').read_text(), + 'nothing to see here') diff --git a/update_version_next.py b/update_version_next.py new file mode 100644 index 0000000..b3d53a3 --- /dev/null +++ b/update_version_next.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +""" +Replace `.. versionchanged:: next` lines in docs files by the given version. + +Run this at release time to replace `next` with the just-released version +in the sources. + +No backups are made; add/commit to Git before running the script. + +Applies to all the VersionChange directives. For deprecated-removed, only +handle the first argument (deprecation version, not the removal version). + +""" + +import argparse +import re +import sys +from pathlib import Path + +DIRECTIVE_RE = re.compile( + r''' + (?P + \s*\.\.\s+ + (version(added|changed|removed)|deprecated(-removed)?) + \s*::\s* + ) + next + (?P + .* + ) + ''', + re.VERBOSE | re.DOTALL, +) + +doc_dir = (Path(__file__) + .parent # cpython/Doc/tools + .parent # cpython/Doc + .resolve() + ) + +parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) +parser.add_argument('version', + help='String to replace "next" with. Usually `x.y`, ' + + 'but can be anything.') +parser.add_argument('directory', type=Path, nargs='?', + help=f'Directory to process. Default: {doc_dir}', + default=doc_dir) +parser.add_argument('--verbose', '-v', action='count', default=0, + help='Increase verbosity. Can be repeated (`-vv`).') + + +def main(argv): + args = parser.parse_args(argv) + version = args.version + if args.verbose: + print( + f'Updating "next" versions in {args.directory} to {version!r}', + file=sys.stderr) + for path in Path(args.directory).glob('**/*.rst'): + num_changed_lines = 0 + lines = [] + with open(path, encoding='utf-8') as file: + for lineno, line in enumerate(file, start=1): + try: + if match := DIRECTIVE_RE.fullmatch(line): + line = match['before'] + version + match['after'] + num_changed_lines += 1 + lines.append(line) + except Exception as exc: + exc.add_note(f'processing line {path}:{lineno}') + raise + if num_changed_lines: + if args.verbose: + print(f'Updating file {path} ({num_changed_lines} changes)', + file=sys.stderr) + with open(path, 'w', encoding='utf-8') as file: + file.writelines(lines) + else: + if args.verbose > 1: + print(f'Unchanged file {path}', file=sys.stderr) + + +if __name__ == '__main__': + main(sys.argv[1:])