diff --git a/build_docs.py b/build_docs.py index 0781464..ecb135c 100755 --- a/build_docs.py +++ b/build_docs.py @@ -10,9 +10,9 @@ [--languages [fr [fr ...]]] -Without any arguments builds docs for all branches configured in the -global BRANCHES value and all languages configured in LANGUAGES, -ignoring the -d flag as it's given in the BRANCHES configuration. +Without any arguments builds docs for all active versions configured in the +global VERSIONS list and all languages configured in the LANGUAGES list, +ignoring the -d flag as it's given in the VERSIONS configuration. -q selects "quick build", which means to build only HTML. @@ -32,17 +32,23 @@ """ from bisect import bisect_left as bisect +from collections import namedtuple, OrderedDict +from contextlib import contextmanager, suppress import filecmp +import json import logging import logging.handlers import os -import pathlib +from pathlib import Path import re +from shlex import quote import shutil +from string import Template import subprocess import sys from datetime import datetime +HERE = Path(__file__).resolve().parent try: import sentry_sdk @@ -53,66 +59,89 @@ VERSION = "19.0" -BRANCHES = [ - # version, git branch, isdev - ("3.6", "3.6", False), - ("3.7", "3.7", False), - ("3.8", "3.8", False), - ("3.9", "3.9", True), - ("3.10", "master", True), + +class Version: + STATUSES = {"EOL", "security-fixes", "stable", "pre-release", "in development"} + + def __init__(self, name, branch, status): + if status not in self.STATUSES: + raise ValueError( + "Version status expected to be in {}".format(", ".join(self.STATUSES)) + ) + self.name = name + self.branch = branch + self.status = status + + @property + def url(self): + return "https://docs.python.org/{}/".format(self.name) + + @property + def title(self): + return "Python {} ({})".format(self.name, self.status) + + +Language = namedtuple( + "Language", ["tag", "iso639_tag", "name", "in_prod", "sphinxopts"] +) + +# EOL and security-fixes are not automatically built, no need to remove them +# from the list. +VERSIONS = [ + Version("2.7", "2.7", "EOL"), + Version("3.5", "3.5", "security-fixes"), + Version("3.6", "3.6", "security-fixes"), + Version("3.7", "3.7", "stable"), + Version("3.8", "3.8", "stable"), + Version("3.9", "3.9", "pre-release"), + Version("3.10", "master", "in development"), ] -LANGUAGES = ["en", "es", "fr", "id", "ja", "ko", "pt-br", "zh-cn", "zh-tw"] - -SPHINXOPTS = { - "ja": [ - "-D latex_engine=platex", - "-D latex_elements.inputenc=", - "-D latex_elements.fontenc=", - ], - "ko": [ - "-D latex_engine=xelatex", - "-D latex_elements.inputenc=", - "-D latex_elements.fontenc=", - r"-D latex_elements.preamble=\\usepackage{kotex}\\setmainhangulfont{UnBatang}\\setsanshangulfont{UnDotum}\\setmonohangulfont{UnTaza}", - ], - "pt-br": [ - "-D latex_engine=xelatex", - "-D latex_elements.inputenc=", - "-D latex_elements.fontenc=", - ], - "fr": [ - "-D latex_engine=xelatex", - "-D latex_elements.inputenc=", - r"-D latex_elements.fontenc=\\usepackage{fontspec}", - ], - "en": [ - "-D latex_engine=xelatex", - "-D latex_elements.inputenc=", - "-D latex_elements.fontenc=", - ], - "es": [ - "-D latex_engine=xelatex", - "-D latex_elements.inputenc=", - r"-D latex_elements.fontenc=\\usepackage{fontspec}", - ], - "zh-cn": [ - "-D latex_engine=xelatex", - "-D latex_elements.inputenc=", - r"-D latex_elements.fontenc=\\usepackage{xeCJK}", - ], - "zh-tw": [ - "-D latex_engine=xelatex", - "-D latex_elements.inputenc=", - r"-D latex_elements.fontenc=\\usepackage{xeCJK}", - ], - "id": [ - "-D latex_engine=xelatex", - "-D latex_elements.inputenc=", - "-D latex_elements.fontenc=", - ], +XELATEX_DEFAULT = ( + "-D latex_engine=xelatex", + "-D latex_elements.inputenc=", + "-D latex_elements.fontenc=", +) + +PLATEX_DEFAULT = ( + "-D latex_engine=platex", + "-D latex_elements.inputenc=", + "-D latex_elements.fontenc=", +) + +XELATEX_WITH_FONTSPEC = ( + "-D latex_engine=xelatex", + "-D latex_elements.inputenc=", + r"-D latex_elements.fontenc=\\usepackage{fontspec}", +) + +XELATEX_FOR_KOREAN = ( + "-D latex_engine=xelatex", + "-D latex_elements.inputenc=", + "-D latex_elements.fontenc=", + r"-D latex_elements.preamble=\\usepackage{kotex}\\setmainhangulfont{UnBatang}\\setsanshangulfont{UnDotum}\\setmonohangulfont{UnTaza}", +) + +XELATEX_WITH_CJK = ( + "-D latex_engine=xelatex", + "-D latex_elements.inputenc=", + r"-D latex_elements.fontenc=\\usepackage{xeCJK}", +) + +LANGUAGES = { + Language("en", "en", "English", True, XELATEX_DEFAULT), + Language("es", "es", "Spanish", False, XELATEX_WITH_FONTSPEC), + Language("fr", "fr", "French", True, XELATEX_WITH_FONTSPEC), + Language("id", "id", "Indonesian", False, XELATEX_DEFAULT), + Language("ja", "ja", "Japanese", True, PLATEX_DEFAULT), + Language("ko", "ko", "Korean", True, XELATEX_FOR_KOREAN), + Language("pt-br", "pt_BR", "Brazilian Portuguese", True, XELATEX_DEFAULT), + Language("zh-cn", "zh_CN", "Simplified Chinese", True, XELATEX_WITH_CJK), + Language("zh-tw", "zh_TW", "Traditional Chinese", True, XELATEX_WITH_CJK), } +DEFAULT_LANGUAGES_SET = {language.tag for language in LANGUAGES if language.in_prod} + def shell_out(cmd, shell=False, logfile=None): logging.debug("Running command %r", cmd) @@ -156,7 +185,7 @@ def changed_files(left, right): changed = [] def traverse(dircmp_result): - base = pathlib.Path(dircmp_result.left).relative_to(left) + base = Path(dircmp_result.left).relative_to(left) changed.extend(str(base / file) for file in dircmp_result.diff_files) for dircomp in dircmp_result.subdirs.values(): traverse(dircomp) @@ -189,15 +218,12 @@ def git_clone(repository, directory, branch=None): shell_out(["git", "-C", directory, "checkout", branch]) -def pep_545_tag_to_gettext_tag(tag): - """Transforms PEP 545 language tags like "pt-br" to gettext language - tags like "pt_BR". (Note that none of those are IETF language tags - like "pt-BR"). - """ - if "-" not in tag: - return tag - language, region = tag.split("-") - return language + "_" + region.upper() +def version_to_tuple(version): + return tuple(int(part) for part in version.split(".")) + + +def tuple_to_version(version_tuple): + return ".".join(str(part) for part in version_tuple) def locate_nearest_version(available_versions, target_version): @@ -216,12 +242,6 @@ def locate_nearest_version(available_versions, target_version): '3.7' """ - def version_to_tuple(version): - return tuple(int(part) for part in version.split(".")) - - def tuple_to_version(version_tuple): - return ".".join(str(part) for part in version_tuple) - available_versions_tuples = sorted( [ version_to_tuple(available_version) @@ -254,55 +274,154 @@ def translation_branch(locale_repo, locale_clone_dir, needed_version): return locate_nearest_version(branches, needed_version) +@contextmanager +def edit(file): + """Context manager to edit a file "in place", use it as: + with edit("/etc/hosts") as i, o: + for line in i: + o.write(line.replace("localhoat", "localhost")) + """ + temporary = file.with_name(file.name + ".tmp") + with suppress(OSError): + os.unlink(temporary) + with open(file) as input_file: + with open(temporary, "w") as output_file: + yield input_file, output_file + os.rename(temporary, file) + + +def picker_label(version): + if version.status == "in development": + return "dev ({})".format(version.name) + if version.status == "pre-release": + return "pre ({})".format(version.name) + return version.name + + +def setup_indexsidebar(dest_path): + versions_li = [] + for version in sorted( + VERSIONS, key=lambda v: version_to_tuple(v.name), reverse=True, + ): + versions_li.append( + '