From f0005e70efb8c35129938dbae8231ebf47b744f9 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 2 Jul 2024 16:52:22 +0200 Subject: [PATCH 01/16] Make `versionchanged:: next`` expand to current (unreleased) version - versionadded, versionchanged, and similar directives expand "next" to e.g. "3.14.0a0 (unreleased)". - A tool is provided for release managers to replace all such occurences of "next" with the given string. --- Doc/tools/extensions/pyspecific.py | 31 +++++- Doc/tools/version_next.py | 76 ++++++++++++++ Lib/test/test_tools/test_docs_version_next.py | 99 +++++++++++++++++++ 3 files changed, 201 insertions(+), 5 deletions(-) create mode 100755 Doc/tools/version_next.py create mode 100644 Lib/test/test_tools/test_docs_version_next.py diff --git a/Doc/tools/extensions/pyspecific.py b/Doc/tools/extensions/pyspecific.py index 8b592d4b4adcea..159e3e7b64ee0e 100644 --- a/Doc/tools/extensions/pyspecific.py +++ b/Doc/tools/extensions/pyspecific.py @@ -28,6 +28,7 @@ from sphinx.util.docutils import SphinxDirective from sphinx.writers.text import TextWriter, TextTranslator from sphinx.util.display import status_iterator +import sphinx.directives.other ISSUE_URI = 'https://bugs.python.org/issue?@action=redirect&bpo=%s' @@ -393,7 +394,23 @@ def run(self): return PyMethod.run(self) -# Support for documenting version of removal in deprecations +# Support for documenting version of changes, additions, deprecations + +def expand_version_arg(argument, env): + """Expand "next" to the current version""" + if argument == 'next': + return sphinx_gettext('{} (unreleased)').format(env.config.release) + return argument + + +class PyVersionChange(sphinx.directives.other.VersionChange): + def run(self): + env = self.state.document.settings.env + self.arguments = ( + expand_version_arg(self.arguments[0], env), + *self.arguments[1:]) + return super().run() + class DeprecatedRemoved(Directive): has_content = True @@ -409,9 +426,10 @@ def run(self): node = addnodes.versionmodified() node.document = self.state.document node['type'] = 'deprecated-removed' - version = (self.arguments[0], self.arguments[1]) - node['version'] = version env = self.state.document.settings.env + deprecated = expand_version_arg(self.arguments[0], env) + version = (deprecated, self.arguments[1]) + node['version'] = version current_version = tuple(int(e) for e in env.config.version.split('.')) removed_version = tuple(int(e) for e in self.arguments[1].split('.')) if current_version < removed_version: @@ -419,7 +437,7 @@ def run(self): else: label = self._removed_label - text = label.format(deprecated=self.arguments[0], removed=self.arguments[1]) + text = label.format(deprecated=deprecated, removed=self.arguments[1]) if len(self.arguments) == 3: inodes, messages = self.state.inline_text(self.arguments[2], self.lineno+1) @@ -448,7 +466,6 @@ def run(self): env.get_domain('changeset').note_changeset(node) return [node] + messages - # Support for including Misc/NEWS issue_re = re.compile('(?:[Ii]ssue #|bpo-)([0-9]+)', re.I) @@ -698,6 +715,10 @@ def setup(app): app.add_directive('availability', Availability) app.add_directive('audit-event', AuditEvent) app.add_directive('audit-event-table', AuditEventListDirective) + app.add_directive('versionadded', PyVersionChange, override=True) + app.add_directive('versionchanged', PyVersionChange, override=True) + app.add_directive('versionremoved', PyVersionChange, override=True) + app.add_directive('deprecated', PyVersionChange, override=True) app.add_directive('deprecated-removed', DeprecatedRemoved) app.add_builder(PydocTopicsBuilder) app.add_object_type('opcode', 'opcode', '%s (opcode)', parse_opcode_signature) diff --git a/Doc/tools/version_next.py b/Doc/tools/version_next.py new file mode 100755 index 00000000000000..38b7c2cd6b7196 --- /dev/null +++ b/Doc/tools/version_next.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +""" +Replace `.. versionchanged:: next` lines in docs files by the given version. + +Applies to all the VersionChange directives. For deprecated-removed, only +handle the first argument (deprecation version, not the removal version). + +""" + +import re +import sys +import argparse +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, +) + +basedir = (Path(__file__) + .parent # cpython/Tools/doc + .parent # cpython/Tools + .parent # cpython + .resolve() + ) +docdir = basedir / 'Doc' + +parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) +parser.add_argument('version', + help='String to replace "next" with.') +parser.add_argument('directory', type=Path, nargs='?', + help=f'Directory to process. Default: {docdir}', + default=docdir) +parser.add_argument('--verbose', '-v', action='count', default=0) + +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) as file: + for lineno, line in enumerate(file, start=1): + if match := DIRECTIVE_RE.fullmatch(line): + line = match['before'] + version + match['after'] + num_changed_lines += 1 + lines.append(line) + if num_changed_lines: + if args.verbose: + print(f'Updating file {path} ({num_changed_lines} changes)', + file=sys.stderr) + with open(path, 'w') 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:]) diff --git a/Lib/test/test_tools/test_docs_version_next.py b/Lib/test/test_tools/test_docs_version_next.py new file mode 100644 index 00000000000000..e8b208a5d812fe --- /dev/null +++ b/Lib/test/test_tools/test_docs_version_next.py @@ -0,0 +1,99 @@ +"""Sanity-check tests for the "docs/replace_versoin_next" tool.""" + +from pathlib import Path +import unittest + +from test.test_tools import basepath +from test.support.import_helper import DirsOnSysPath, import_module +from test.support import os_helper +from test import support + +print(basepath) +tooldir = Path(basepath) / 'Doc' / 'tools' +print(tooldir) + +with DirsOnSysPath(str(tooldir)): + import sys + print(sys.path) + version_next = import_module('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] + if support.verbose: + args.append('-vv') + 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') From 0c86239ab786b706ff2ada38709b2fc01610027e Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 2 Jul 2024 16:59:16 +0200 Subject: [PATCH 02/16] Use in existing docs --- Doc/c-api/long.rst | 2 +- Doc/c-api/unicode.rst | 2 +- Doc/library/ast.rst | 2 +- Doc/library/ctypes.rst | 2 +- Doc/library/dis.rst | 4 ++-- Doc/library/pathlib.rst | 4 ++-- Doc/library/symtable.rst | 8 ++++---- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Doc/c-api/long.rst b/Doc/c-api/long.rst index 42162914c0aec8..e4684e8c746932 100644 --- a/Doc/c-api/long.rst +++ b/Doc/c-api/long.rst @@ -511,7 +511,7 @@ distinguished from a number. Use :c:func:`PyErr_Occurred` to disambiguate. On failure, return -1 with an exception set. This function always succeeds if *obj* is a :c:type:`PyLongObject` or its subtype. - .. versionadded:: 3.14 + .. versionadded:: next .. c:function:: int PyUnstable_Long_IsCompact(const PyLongObject* op) diff --git a/Doc/c-api/unicode.rst b/Doc/c-api/unicode.rst index 246cf47df62e78..2425b6ceb70329 100644 --- a/Doc/c-api/unicode.rst +++ b/Doc/c-api/unicode.rst @@ -1509,7 +1509,7 @@ PyUnicodeWriter The :c:type:`PyUnicodeWriter` API can be used to create a Python :class:`str` object. -.. versionadded:: 3.14 +.. versionadded:: next .. c:type:: PyUnicodeWriter diff --git a/Doc/library/ast.rst b/Doc/library/ast.rst index f7e8afa7000392..48a7d30604bab5 100644 --- a/Doc/library/ast.rst +++ b/Doc/library/ast.rst @@ -2487,7 +2487,7 @@ effects on the compilation of a program: differ in whitespace or similar details. Attributes include line numbers and column offsets. - .. versionadded:: 3.14 + .. versionadded:: next .. _ast-cli: diff --git a/Doc/library/ctypes.rst b/Doc/library/ctypes.rst index a56e0eef5d11b1..537564beb50c3b 100644 --- a/Doc/library/ctypes.rst +++ b/Doc/library/ctypes.rst @@ -2299,7 +2299,7 @@ These are the fundamental ctypes data types: Represents the C :c:expr:`double complex` datatype, if available. The constructor accepts an optional :class:`complex` initializer. - .. versionadded:: 3.14 + .. versionadded:: next .. class:: c_int diff --git a/Doc/library/dis.rst b/Doc/library/dis.rst index e932b865a825a0..5905be7754be63 100644 --- a/Doc/library/dis.rst +++ b/Doc/library/dis.rst @@ -926,7 +926,7 @@ iterations of the loop. list of constants supported by this instruction. Used by the :keyword:`assert` statement to load :exc:`AssertionError`. - .. versionadded:: 3.14 + .. versionadded:: next .. opcode:: LOAD_BUILD_CLASS @@ -1798,7 +1798,7 @@ iterations of the loop. If ``type(STACK[-1]).__xxx__`` is not a method, leave ``STACK[-1].__xxx__; NULL`` on the stack. - .. versionadded:: 3.14 + .. versionadded:: next **Pseudo-instructions** diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 0918bbb47e9ea6..cfd821eb600484 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -1559,7 +1559,7 @@ Copying, renaming and deleting raises :exc:`OSError` when a symlink to a directory is encountered and *follow_symlinks* is false. - .. versionadded:: 3.14 + .. versionadded:: next .. method:: Path.copytree(target, *, follow_symlinks=True, dirs_exist_ok=False, \ @@ -1586,7 +1586,7 @@ Copying, renaming and deleting nothing, in which case the copying operation continues. If *on_error* isn't given, exceptions are propagated to the caller. - .. versionadded:: 3.14 + .. versionadded:: next .. method:: Path.rename(target) diff --git a/Doc/library/symtable.rst b/Doc/library/symtable.rst index aa5f8d95925ada..bc6836224df9cc 100644 --- a/Doc/library/symtable.rst +++ b/Doc/library/symtable.rst @@ -239,7 +239,7 @@ Examining Symbol Tables Return ``True`` if the symbol is a type parameter. - .. versionadded:: 3.14 + .. versionadded:: next .. method:: is_global() @@ -286,7 +286,7 @@ Examining Symbol Tables be free from the perspective of ``C.method``, thereby allowing the latter to return *1* at runtime and not *2*. - .. versionadded:: 3.14 + .. versionadded:: next .. method:: is_assigned() @@ -296,13 +296,13 @@ Examining Symbol Tables Return ``True`` if the symbol is a comprehension iteration variable. - .. versionadded:: 3.14 + .. versionadded:: next .. method:: is_comp_cell() Return ``True`` if the symbol is a cell in an inlined comprehension. - .. versionadded:: 3.14 + .. versionadded:: next .. method:: is_namespace() From e270ddc4ce2914e4d077c5c2b490423a77aef7f9 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 19 Jul 2024 11:54:42 +0200 Subject: [PATCH 03/16] Apply extension suggestions from code review Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com> --- Doc/tools/extensions/pyspecific.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Doc/tools/extensions/pyspecific.py b/Doc/tools/extensions/pyspecific.py index 159e3e7b64ee0e..a807371aec817c 100644 --- a/Doc/tools/extensions/pyspecific.py +++ b/Doc/tools/extensions/pyspecific.py @@ -396,19 +396,17 @@ def run(self): # Support for documenting version of changes, additions, deprecations -def expand_version_arg(argument, env): +def expand_version_arg(argument, release): """Expand "next" to the current version""" if argument == 'next': - return sphinx_gettext('{} (unreleased)').format(env.config.release) + return sphinx_gettext('{} (unreleased)').format(release) return argument -class PyVersionChange(sphinx.directives.other.VersionChange): +class PyVersionChange(sphinx.domains.changeset.VersionChange): def run(self): - env = self.state.document.settings.env - self.arguments = ( - expand_version_arg(self.arguments[0], env), - *self.arguments[1:]) + # Replace the 'next' special token with the current development version + self.arguments[0] = expand_version_arg(self.arguments[0], self.config.release) return super().run() @@ -426,8 +424,8 @@ def run(self): node = addnodes.versionmodified() node.document = self.state.document node['type'] = 'deprecated-removed' - env = self.state.document.settings.env - deprecated = expand_version_arg(self.arguments[0], env) + release = self.state.document.settings.env.config.release + deprecated = expand_version_arg(self.arguments[0], release) version = (deprecated, self.arguments[1]) node['version'] = version current_version = tuple(int(e) for e in env.config.version.split('.')) From 0022e1ae0478bfcf27a1ff7f0d04aaf63a664bf4 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 19 Jul 2024 12:17:12 +0200 Subject: [PATCH 04/16] Update Doc/tools/extensions/pyspecific.py Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com> --- Doc/tools/extensions/pyspecific.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/tools/extensions/pyspecific.py b/Doc/tools/extensions/pyspecific.py index a807371aec817c..e59d4d1ee3c3b3 100644 --- a/Doc/tools/extensions/pyspecific.py +++ b/Doc/tools/extensions/pyspecific.py @@ -28,7 +28,7 @@ from sphinx.util.docutils import SphinxDirective from sphinx.writers.text import TextWriter, TextTranslator from sphinx.util.display import status_iterator -import sphinx.directives.other +import sphinx.domains.changeset ISSUE_URI = 'https://bugs.python.org/issue?@action=redirect&bpo=%s' From 342838d26a612be758ace246e0f3d4a5de73e598 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 19 Jul 2024 11:40:06 +0200 Subject: [PATCH 05/16] Fix location of the the Doc dir Co-Authored-By: Adam Turner <9087854+AA-Turner@users.noreply.github.com> --- Doc/tools/version_next.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Doc/tools/version_next.py b/Doc/tools/version_next.py index 38b7c2cd6b7196..4eb2294187457e 100755 --- a/Doc/tools/version_next.py +++ b/Doc/tools/version_next.py @@ -27,13 +27,11 @@ re.VERBOSE | re.DOTALL, ) -basedir = (Path(__file__) - .parent # cpython/Tools/doc - .parent # cpython/Tools - .parent # cpython - .resolve() - ) -docdir = basedir / 'Doc' +docdir = (Path(__file__) + .parent # cpython/Doc/tools + .parent # cpython/Doc + .resolve() + ) parser = argparse.ArgumentParser( description=__doc__, From 1d9a61879d7d54eb9448c097903451a31cc5eab5 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 19 Jul 2024 11:48:01 +0200 Subject: [PATCH 06/16] Specify encoding --- Doc/tools/version_next.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/tools/version_next.py b/Doc/tools/version_next.py index 4eb2294187457e..dcb7f01d9aa119 100755 --- a/Doc/tools/version_next.py +++ b/Doc/tools/version_next.py @@ -53,7 +53,7 @@ def main(argv): for path in Path(args.directory).glob('**/*.rst'): num_changed_lines = 0 lines = [] - with open(path) as file: + with open(path, encoding='utf-8') as file: for lineno, line in enumerate(file, start=1): if match := DIRECTIVE_RE.fullmatch(line): line = match['before'] + version + match['after'] @@ -63,7 +63,7 @@ def main(argv): if args.verbose: print(f'Updating file {path} ({num_changed_lines} changes)', file=sys.stderr) - with open(path, 'w') as file: + with open(path, 'w', encoding='utf-8') as file: file.writelines(lines) else: if args.verbose > 1: From 311bceb73dd09398e7320cd279359e4cdf5d6f46 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 19 Jul 2024 11:52:17 +0200 Subject: [PATCH 07/16] Improve the docstring/usage --- Doc/tools/version_next.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Doc/tools/version_next.py b/Doc/tools/version_next.py index dcb7f01d9aa119..c64fce78ba9248 100755 --- a/Doc/tools/version_next.py +++ b/Doc/tools/version_next.py @@ -2,6 +2,11 @@ """ 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). From 72a3162fb2692b0377425f553af2a037300faa62 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 19 Jul 2024 11:54:03 +0200 Subject: [PATCH 08/16] Re-add stray removed line --- Doc/tools/extensions/pyspecific.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Doc/tools/extensions/pyspecific.py b/Doc/tools/extensions/pyspecific.py index e59d4d1ee3c3b3..f36590393e4e29 100644 --- a/Doc/tools/extensions/pyspecific.py +++ b/Doc/tools/extensions/pyspecific.py @@ -464,6 +464,7 @@ def run(self): env.get_domain('changeset').note_changeset(node) return [node] + messages + # Support for including Misc/NEWS issue_re = re.compile('(?:[Ii]ssue #|bpo-)([0-9]+)', re.I) From 91bd51186f588f948d9d803ab55c923e900a1044 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 19 Jul 2024 12:18:39 +0200 Subject: [PATCH 09/16] Split long line --- Doc/tools/extensions/pyspecific.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Doc/tools/extensions/pyspecific.py b/Doc/tools/extensions/pyspecific.py index f36590393e4e29..8b7b4920abddac 100644 --- a/Doc/tools/extensions/pyspecific.py +++ b/Doc/tools/extensions/pyspecific.py @@ -406,7 +406,8 @@ def expand_version_arg(argument, release): class PyVersionChange(sphinx.domains.changeset.VersionChange): def run(self): # Replace the 'next' special token with the current development version - self.arguments[0] = expand_version_arg(self.arguments[0], self.config.release) + self.arguments[0] = expand_version_arg(self.arguments[0], + self.config.release) return super().run() From 46ae8143a786b841901ef94768b7f73cb5617234 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 19 Jul 2024 12:19:43 +0200 Subject: [PATCH 10/16] Set `env` before using it --- Doc/tools/extensions/pyspecific.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/tools/extensions/pyspecific.py b/Doc/tools/extensions/pyspecific.py index 8b7b4920abddac..ac9341f9e8f618 100644 --- a/Doc/tools/extensions/pyspecific.py +++ b/Doc/tools/extensions/pyspecific.py @@ -425,7 +425,8 @@ def run(self): node = addnodes.versionmodified() node.document = self.state.document node['type'] = 'deprecated-removed' - release = self.state.document.settings.env.config.release + env = self.state.document.settings.env + release = env.config.release deprecated = expand_version_arg(self.arguments[0], release) version = (deprecated, self.arguments[1]) node['version'] = version @@ -461,7 +462,6 @@ def run(self): classes=['versionmodified']), translatable=False) node.append(para) - env = self.state.document.settings.env env.get_domain('changeset').note_changeset(node) return [node] + messages From 9c4502c621e5a30563c7253672a33a5e74f89e17 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 19 Jul 2024 12:22:51 +0200 Subject: [PATCH 11/16] Add a blurb --- .../2024-07-19-12-22-48.gh-issue-121277.wF_zKd.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Documentation/2024-07-19-12-22-48.gh-issue-121277.wF_zKd.rst diff --git a/Misc/NEWS.d/next/Documentation/2024-07-19-12-22-48.gh-issue-121277.wF_zKd.rst b/Misc/NEWS.d/next/Documentation/2024-07-19-12-22-48.gh-issue-121277.wF_zKd.rst new file mode 100644 index 00000000000000..60f75ae0c21326 --- /dev/null +++ b/Misc/NEWS.d/next/Documentation/2024-07-19-12-22-48.gh-issue-121277.wF_zKd.rst @@ -0,0 +1,2 @@ +Writers of CPython's documentation can now use ``next`` as the version for +the ``versionchanged``, ``versionadded``, ``deprecated`` directives. From 993dd5cfc8cb91c4f72a018bc4f8c03d948a6675 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 19 Jul 2024 13:23:31 +0200 Subject: [PATCH 12/16] Consolidate VersionChange imports Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com> --- Doc/tools/extensions/pyspecific.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Doc/tools/extensions/pyspecific.py b/Doc/tools/extensions/pyspecific.py index 35faf5d9985878..7a62da0db2f348 100644 --- a/Doc/tools/extensions/pyspecific.py +++ b/Doc/tools/extensions/pyspecific.py @@ -29,7 +29,6 @@ from sphinx.util.docutils import SphinxDirective from sphinx.writers.text import TextWriter, TextTranslator from sphinx.util.display import status_iterator -import sphinx.domains.changeset ISSUE_URI = 'https://bugs.python.org/issue?@action=redirect&bpo=%s' @@ -403,7 +402,7 @@ def expand_version_arg(argument, release): return argument -class PyVersionChange(sphinx.domains.changeset.VersionChange): +class PyVersionChange(VersionChange): def run(self): # Replace the 'next' special token with the current development version self.arguments[0] = expand_version_arg(self.arguments[0], From 5f2b6127f7b7b75cd66e0dbb9274ddf6eb8af097 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Mon, 22 Jul 2024 11:03:28 +0200 Subject: [PATCH 13/16] Apply suggestions from code review Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Doc/tools/version_next.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Doc/tools/version_next.py b/Doc/tools/version_next.py index c64fce78ba9248..e7c56f660dac27 100755 --- a/Doc/tools/version_next.py +++ b/Doc/tools/version_next.py @@ -32,11 +32,11 @@ re.VERBOSE | re.DOTALL, ) -docdir = (Path(__file__) - .parent # cpython/Doc/tools - .parent # cpython/Doc - .resolve() - ) +doc_dir = (Path(__file__) + .parent # cpython/Doc/tools + .parent # cpython/Doc + .resolve() + ) parser = argparse.ArgumentParser( description=__doc__, @@ -48,6 +48,7 @@ default=docdir) parser.add_argument('--verbose', '-v', action='count', default=0) + def main(argv): args = parser.parse_args(argv) version = args.version @@ -67,7 +68,7 @@ def main(argv): if num_changed_lines: if args.verbose: print(f'Updating file {path} ({num_changed_lines} changes)', - file=sys.stderr) + file=sys.stderr) with open(path, 'w', encoding='utf-8') as file: file.writelines(lines) else: From 8b8a2f92512b3c598bbeb68a5a8693e8e31fc6a5 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Mon, 22 Jul 2024 11:09:20 +0200 Subject: [PATCH 14/16] Expand help --- Doc/tools/version_next.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Doc/tools/version_next.py b/Doc/tools/version_next.py index e7c56f660dac27..04fb5aaeb92272 100755 --- a/Doc/tools/version_next.py +++ b/Doc/tools/version_next.py @@ -42,11 +42,13 @@ description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument('version', - help='String to replace "next" with.') + 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: {docdir}', - default=docdir) -parser.add_argument('--verbose', '-v', action='count', default=0) + 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): From d163647ea500874a1d79831c73aee29b2bbe620a Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Mon, 22 Jul 2024 11:11:16 +0200 Subject: [PATCH 15/16] Add lineno to exceptions Thanks to Ruff for reminding me --- Doc/tools/version_next.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Doc/tools/version_next.py b/Doc/tools/version_next.py index 04fb5aaeb92272..351cb16a315ae1 100755 --- a/Doc/tools/version_next.py +++ b/Doc/tools/version_next.py @@ -63,10 +63,14 @@ def main(argv): lines = [] with open(path, encoding='utf-8') as file: for lineno, line in enumerate(file, start=1): - if match := DIRECTIVE_RE.fullmatch(line): - line = match['before'] + version + match['after'] - num_changed_lines += 1 - lines.append(line) + 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)', From 757cf6b04d02509a9f0e7e650493a292bc2c8cd5 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 24 Sep 2024 11:11:12 -0700 Subject: [PATCH 16/16] Remove the tool; that now lives in the release-tools repo --- Doc/tools/version_next.py | 86 ---------------- Lib/test/test_tools/test_docs_version_next.py | 99 ------------------- 2 files changed, 185 deletions(-) delete mode 100755 Doc/tools/version_next.py delete mode 100644 Lib/test/test_tools/test_docs_version_next.py diff --git a/Doc/tools/version_next.py b/Doc/tools/version_next.py deleted file mode 100755 index 351cb16a315ae1..00000000000000 --- a/Doc/tools/version_next.py +++ /dev/null @@ -1,86 +0,0 @@ -#!/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 re -import sys -import argparse -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:]) diff --git a/Lib/test/test_tools/test_docs_version_next.py b/Lib/test/test_tools/test_docs_version_next.py deleted file mode 100644 index e8b208a5d812fe..00000000000000 --- a/Lib/test/test_tools/test_docs_version_next.py +++ /dev/null @@ -1,99 +0,0 @@ -"""Sanity-check tests for the "docs/replace_versoin_next" tool.""" - -from pathlib import Path -import unittest - -from test.test_tools import basepath -from test.support.import_helper import DirsOnSysPath, import_module -from test.support import os_helper -from test import support - -print(basepath) -tooldir = Path(basepath) / 'Doc' / 'tools' -print(tooldir) - -with DirsOnSysPath(str(tooldir)): - import sys - print(sys.path) - version_next = import_module('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] - if support.verbose: - args.append('-vv') - 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')