diff options
Diffstat (limited to 'utils')
-rw-r--r-- | utils/CHANGES_template | 20 | ||||
-rw-r--r-- | utils/__init__.py | 0 | ||||
-rw-r--r-- | utils/babel_runner.py | 171 | ||||
-rw-r--r-- | utils/bump_docker.sh | 16 | ||||
-rw-r--r-- | utils/bump_version.py | 183 | ||||
-rw-r--r-- | utils/release-checklist | 108 |
6 files changed, 498 insertions, 0 deletions
diff --git a/utils/CHANGES_template b/utils/CHANGES_template new file mode 100644 index 0000000..991925c --- /dev/null +++ b/utils/CHANGES_template @@ -0,0 +1,20 @@ +Release x.y.z (in development) +============================== + +Dependencies +------------ + +Incompatible changes +-------------------- + +Deprecated +---------- + +Features added +-------------- + +Bugs fixed +---------- + +Testing +-------- diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/utils/__init__.py diff --git a/utils/babel_runner.py b/utils/babel_runner.py new file mode 100644 index 0000000..32e6fe6 --- /dev/null +++ b/utils/babel_runner.py @@ -0,0 +1,171 @@ +"""Run babel for translations. + +Usage: + +babel_runner.py extract + Extract messages from the source code and update the ".pot" template file. + +babel_runner.py update + Update all language catalogues in "sphinx/locale/<language>/LC_MESSAGES" + with the current messages in the template file. + +babel_runner.py compile + Compile the ".po" catalogue files to ".mo" and ".js" files. +""" + +import json +import logging +import os +import sys + +from babel.messages.frontend import compile_catalog, extract_messages, update_catalog +from babel.messages.pofile import read_po + +import sphinx + +ROOT = os.path.realpath(os.path.join(os.path.abspath(__file__), "..", "..")) + + +class compile_catalog_plusjs(compile_catalog): + """ + An extended command that writes all message strings that occur in + JavaScript files to a JavaScript file along with the .mo file. + + Unfortunately, babel's setup command isn't built very extensible, so + most of the run() code is duplicated here. + """ + + def run(self): + if super().run(): + print("Compiling failed.", file=sys.stderr) + raise SystemExit(2) + + for domain in self.domain: + self._run_domain_js(domain) + + def _run_domain_js(self, domain): + po_files = [] + js_files = [] + + if not self.input_file: + if self.locale: + po_files.append((self.locale, + os.path.join(self.directory, self.locale, + 'LC_MESSAGES', + domain + '.po'))) + js_files.append(os.path.join(self.directory, self.locale, + 'LC_MESSAGES', + domain + '.js')) + else: + for locale in os.listdir(self.directory): + po_file = os.path.join(self.directory, locale, + 'LC_MESSAGES', + domain + '.po') + if os.path.exists(po_file): + po_files.append((locale, po_file)) + js_files.append(os.path.join(self.directory, locale, + 'LC_MESSAGES', + domain + '.js')) + else: + po_files.append((self.locale, self.input_file)) + if self.output_file: + js_files.append(self.output_file) + else: + js_files.append(os.path.join(self.directory, self.locale, + 'LC_MESSAGES', + domain + '.js')) + + for js_file, (locale, po_file) in zip(js_files, po_files): + with open(po_file, encoding='utf8') as infile: + catalog = read_po(infile, locale) + + if catalog.fuzzy and not self.use_fuzzy: + continue + + self.log.info('writing JavaScript strings in catalog %s to %s', + po_file, js_file) + + jscatalog = {} + for message in catalog: + if any(x[0].endswith(('.js', '.js_t', '.html')) + for x in message.locations): + msgid = message.id + if isinstance(msgid, (list, tuple)): + msgid = msgid[0] + jscatalog[msgid] = message.string + + obj = json.dumps({ + 'messages': jscatalog, + 'plural_expr': catalog.plural_expr, + 'locale': f'{catalog.locale!s}' + }, sort_keys=True, indent=4) + with open(js_file, 'w', encoding='utf8') as outfile: + outfile.write(f'Documentation.addTranslations({obj});') + + +def _get_logger(): + log = logging.getLogger('babel') + log.setLevel(logging.INFO) + handler = logging.StreamHandler() + handler.setFormatter(logging.Formatter('%(message)s')) + log.addHandler(handler) + return log + + +def run_extract(): + os.chdir(ROOT) + command = extract_messages() + command.log = _get_logger() + command.initialize_options() + + command.keywords = "_ __ l_ lazy_gettext" + command.mapping_file = "babel.cfg" + command.output_file = os.path.join("sphinx", "locale", "sphinx.pot") + command.project = "Sphinx" + command.version = sphinx.__version__ + command.input_paths = "sphinx" + + command.finalize_options() + return command.run() + + +def run_update(): + os.chdir(ROOT) + command = update_catalog() + command.log = _get_logger() + command.initialize_options() + + command.domain = "sphinx" + command.input_file = os.path.join("sphinx", "locale", "sphinx.pot") + command.output_dir = os.path.join("sphinx", "locale") + + command.finalize_options() + return command.run() + + +def run_compile(): + os.chdir(ROOT) + command = compile_catalog_plusjs() + command.log = _get_logger() + command.initialize_options() + + command.domain = "sphinx" + command.directory = os.path.join("sphinx", "locale") + + command.finalize_options() + return command.run() + + +if __name__ == '__main__': + try: + action = sys.argv[1].lower() + except IndexError: + print(__doc__, file=sys.stderr) + raise SystemExit(2) + + if action == "extract": + raise SystemExit(run_extract()) + if action == "update": + raise SystemExit(run_update()) + if action == "compile": + raise SystemExit(run_compile()) diff --git a/utils/bump_docker.sh b/utils/bump_docker.sh new file mode 100644 index 0000000..436aaef --- /dev/null +++ b/utils/bump_docker.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +set -ex + +if [ -z "$1" ]; then + echo "bump_docker.sh [VERSION]" + exit +fi + +cd docker +git checkout . +sed -i "" -e "s/==[0-9.]\{1,\}/==$1/" base/Dockerfile +sed -i "" -e "s/==[0-9.]\{1,\}/==$1/" latexpdf/Dockerfile +git commit -am "Bump to $1" +git tag $1 +git push origin master --tags diff --git a/utils/bump_version.py b/utils/bump_version.py new file mode 100644 index 0000000..e2aefa8 --- /dev/null +++ b/utils/bump_version.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 + +import argparse +import os +import re +import sys +from contextlib import contextmanager +from datetime import datetime + +script_dir = os.path.dirname(__file__) +package_dir = os.path.abspath(os.path.join(script_dir, '..')) + +RELEASE_TYPE = {'a': 'alpha', 'b': 'beta'} + + +def stringify_version(version_info, in_develop=True): + version = '.'.join(str(v) for v in version_info[:3]) + if not in_develop and version_info[3] != 'final': + version += version_info[3][0] + str(version_info[4]) + + return version + + +def bump_version(path, version_info, in_develop=True): + version = stringify_version(version_info, in_develop) + + with open(path, 'r', encoding='utf-8') as f: + lines = f.read().splitlines() + + for i, line in enumerate(lines): + if line.startswith('__version__ = '): + lines[i] = f"__version__ = '{version}'" + continue + if line.startswith('version_info = '): + lines[i] = f'version_info = {version_info}' + continue + if line.startswith('_in_development = '): + lines[i] = f'_in_development = {in_develop}' + continue + + with open(path, 'w', encoding='utf-8') as f: + f.write('\n'.join(lines) + '\n') + + +def parse_version(version): + matched = re.search(r'^(\d+)\.(\d+)$', version) + if matched: + major, minor = matched.groups() + return (int(major), int(minor), 0, 'final', 0) + + matched = re.search(r'^(\d+)\.(\d+)\.(\d+)$', version) + if matched: + major, minor, rev = matched.groups() + return (int(major), int(minor), int(rev), 'final', 0) + + matched = re.search(r'^(\d+)\.(\d+)\s*(a|b|alpha|beta)(\d+)$', version) + if matched: + major, minor, typ, relver = matched.groups() + release = RELEASE_TYPE.get(typ, typ) + return (int(major), int(minor), 0, release, int(relver)) + + matched = re.search(r'^(\d+)\.(\d+)\.(\d+)\s*(a|b|alpha|beta)(\d+)$', version) + if matched: + major, minor, rev, typ, relver = matched.groups() + release = RELEASE_TYPE.get(typ, typ) + return (int(major), int(minor), int(rev), release, int(relver)) + + raise RuntimeError('Unknown version: %s' % version) + + +class Skip(Exception): + pass + + +@contextmanager +def processing(message): + try: + print(message + ' ... ', end='') + yield + except Skip as exc: + print('skip: %s' % exc) + except Exception: + print('error') + raise + else: + print('done') + + +class Changes: + def __init__(self, path): + self.path = path + self.fetch_version() + + def fetch_version(self): + with open(self.path, encoding='utf-8') as f: + version = f.readline().strip() + matched = re.search(r'^Release (.*) \((.*)\)$', version) + if matched is None: + raise RuntimeError('Unknown CHANGES format: %s' % version) + + self.version, self.release_date = matched.groups() + self.version_info = parse_version(self.version) + if self.release_date == 'in development': + self.in_development = True + else: + self.in_development = False + + def finalize_release_date(self): + release_date = datetime.now().strftime('%b %d, %Y') + heading = 'Release %s (released %s)' % (self.version, release_date) + + with open(self.path, 'r+', encoding='utf-8') as f: + f.readline() # skip first two lines + f.readline() + body = f.read() + + f.seek(0) + f.truncate(0) + f.write(heading + '\n') + f.write('=' * len(heading) + '\n') + f.write(self.filter_empty_sections(body)) + + def add_release(self, version_info): + if version_info[-2:] in (('beta', 0), ('final', 0)): + version = stringify_version(version_info) + else: + reltype = version_info[3] + version = '%s %s%s' % (stringify_version(version_info), + RELEASE_TYPE.get(reltype, reltype), + version_info[4] or '') + heading = 'Release %s (in development)' % version + + with open(os.path.join(script_dir, 'CHANGES_template'), encoding='utf-8') as f: + f.readline() # skip first two lines + f.readline() + tmpl = f.read() + + with open(self.path, 'r+', encoding='utf-8') as f: + body = f.read() + + f.seek(0) + f.truncate(0) + f.write(heading + '\n') + f.write('=' * len(heading) + '\n') + f.write(tmpl) + f.write('\n') + f.write(body) + + def filter_empty_sections(self, body): + return re.sub('^\n.+\n-{3,}\n+(?=\n.+\n[-=]{3,}\n)', '', body, flags=re.M) + + +def parse_options(argv): + parser = argparse.ArgumentParser() + parser.add_argument('version', help='A version number (cf. 1.6b0)') + parser.add_argument('--in-develop', action='store_true') + options = parser.parse_args(argv) + options.version = parse_version(options.version) + return options + + +def main(): + options = parse_options(sys.argv[1:]) + + with processing("Rewriting sphinx/__init__.py"): + bump_version(os.path.join(package_dir, 'sphinx/__init__.py'), + options.version, options.in_develop) + + with processing('Rewriting CHANGES'): + changes = Changes(os.path.join(package_dir, 'CHANGES')) + if changes.version_info == options.version: + if changes.in_development: + changes.finalize_release_date() + else: + raise Skip('version not changed') + else: + if changes.in_development: + print('WARNING: last version is not released yet: %s' % changes.version) + changes.add_release(options.version) + + +if __name__ == '__main__': + main() diff --git a/utils/release-checklist b/utils/release-checklist new file mode 100644 index 0000000..febbbae --- /dev/null +++ b/utils/release-checklist @@ -0,0 +1,108 @@ +Release checklist +================= + +for stable releases +------------------- + +* open "https://github.com/sphinx-doc/sphinx/actions?query=branch:X.Y.x" and all tests has passed +* Run ``git fetch; git status`` and check nothing changed +* ``python utils/bump_version.py X.Y.Z`` +* Check diff by ``git diff`` +* ``git commit -am 'Bump to X.Y.Z final'`` +* ``make clean`` +* ``python -m build .`` +* ``twine upload dist/Sphinx-* --sign --identity [your GPG key]`` +* open https://pypi.org/project/Sphinx/ and check there are no obvious errors +* ``sh utils/bump_docker.sh X.Y.Z`` +* ``git tag vX.Y.Z -m "Sphinx X.Y.Z"`` +* ``python utils/bump_version.py --in-develop X.Y.Zb0`` (ex. 1.5.3b0) +* Check diff by ``git diff`` +* ``git commit -am 'Bump version'`` +* ``git push origin X.Y.x --tags`` +* ``git checkout X.x`` +* ``git merge X.Y.x`` +* ``git push origin X.x`` +* Add new version/milestone to tracker categories +* Write announcement and send to sphinx-dev, sphinx-users and python-announce + +for first beta releases +----------------------- + +* open "https://github.com/sphinx-doc/sphinx/actions?query=branch:master" and all tests has passed +* Run ``git fetch; git status`` and check nothing changed +* ``python utils/bump_version.py X.Y.0b1`` +* Check diff by ``git diff`` +* ``git commit -am 'Bump to X.Y.0 beta1'`` +* ``make clean`` +* ``python -m build .`` +* ``twine upload dist/Sphinx-* --sign --identity [your GPG key]`` +* open https://pypi.org/project/Sphinx/ and check there are no obvious errors +* ``git tag vX.Y.0b1 -m "Sphinx X.Y.0b1"`` +* ``python utils/bump_version.py --in-develop X.Y.0b2`` (ex. 1.6.0b2) +* Check diff by ``git diff`` +* ``git commit -am 'Bump version'`` +* ``git checkout -b X.x`` +* ``git push origin X.x --tags`` +* ``git checkout master`` +* ``git merge X.x`` +* ``python utils/bump_version.py --in-develop A.B.0b0`` (ex. 1.7.0b0) +* Check diff by ``git diff`` +* ``git commit -am 'Bump version'`` +* ``git push origin master`` +* open https://github.com/sphinx-doc/sphinx/settings/branches and make ``X.Y`` branch protected +* Add new version/milestone to tracker categories +* Write announcement and send to sphinx-dev, sphinx-users and python-announce + +for other beta releases +----------------------- + +* open "https://github.com/sphinx-doc/sphinx/actions?query=branch:X.x" and all tests has passed +* Run ``git fetch; git status`` and check nothing changed +* ``python utils/bump_version.py X.Y.0bN`` +* Check diff by ``git diff`` +* ``git commit -am 'Bump to X.Y.0 betaN'`` +* ``make clean`` +* ``python -m build .`` +* ``twine upload dist/Sphinx-* --sign --identity [your GPG key]`` +* open https://pypi.org/project/Sphinx/ and check there are no obvious errors +* ``git tag vX.Y.0bN -m "Sphinx X.Y.0bN"`` +* ``python utils/bump_version.py --in-develop X.Y.0bM`` (ex. 1.6.0b3) +* Check diff by `git diff`` +* ``git commit -am 'Bump version'`` +* ``git push origin X.x --tags`` +* ``git checkout master`` +* ``git merge X.x`` +* ``git push origin master`` +* Add new version/milestone to tracker categories +* Write announcement and send to sphinx-dev, sphinx-users and python-announce + +for major releases +------------------ + +* open "https://github.com/sphinx-doc/sphinx/actions?query=branch:X.x" and all tests has passed +* Run ``git fetch; git status`` and check nothing changed +* Run ``git add sphinx`` +* Run ``git commit -am 'Update message catalogs'`` +* ``python utils/bump_version.py X.Y.0`` +* Check diff by ``git diff`` +* ``git commit -am 'Bump to X.Y.0 final'`` +* ``make clean`` +* ``python -m build .`` +* ``twine upload dist/Sphinx-* --sign --identity [your GPG key]`` +* open https://pypi.org/project/Sphinx/ and check there are no obvious errors +* ``sh utils/bump_docker.sh X.Y.Z`` +* ``git tag vX.Y.0 -m "Sphinx X.Y.0"`` +* ``python utils/bump_version.py --in-develop X.Y.1b0`` (ex. 1.6.1b0) +* Check diff by ``git diff`` +* ``git commit -am 'Bump version'`` +* ``git push origin X.x --tags`` +* ``git checkout master`` +* ``git merge X.x`` +* ``git push origin master`` +* open https://github.com/sphinx-doc/sphinx/settings/branches and make ``A.B`` branch *not* protected +* ``git checkout A.B`` (checkout old stable) +* Run ``git tag A.B -m "Sphinx A.B"`` to paste a tag instead branch +* Run ``git push origin :A.B --tags`` to remove old stable branch +* open https://readthedocs.org/dashboard/sphinx/versions/ and enable the released version +* Add new version/milestone to tracker categories +* Write announcement and send to sphinx-dev, sphinx-users and python-announce |