Expand Up @@ -72,7 +72,7 @@ TYPE_CHECKING = False if TYPE_CHECKING: from collections.abc import Iterator, Sequence, Set from collections.abc importCollection, Iterator, Sequence, Set from typing import Literal try: Expand Down Expand Up @@ -101,7 +101,7 @@ def __reversed__(self) -> Iterator[Version]: return reversed(self._seq) @classmethod def from_json(cls, data) -> Versions: def from_json(cls, data: dict ) -> Versions: versions = sorted( [Version.from_json(name, release) for name, release in data.items()], key=Version.as_tuple, Expand Down Expand Up @@ -158,7 +158,9 @@ class Version: "prerelease": "pre-release", } def __init__(self, name, *, status, branch_or_tag=None): def __init__( self, name: str, *, status: str, branch_or_tag: str | None = None ) -> None: status = self.SYNONYMS.get(status, status) if status not in self.STATUSES: raise ValueError( Expand All @@ -169,22 +171,22 @@ def __init__(self, name, *, status, branch_or_tag=None): self.branch_or_tag = branch_or_tag self.status = status def __repr__(self): def __repr__(self) -> str : return f"Version({self.name})" def __eq__(self, other) : def __eq__(self, other: Version) -> bool : return self.name == other.name def __gt__(self, other) : def __gt__(self, other: Version) -> bool : return self.as_tuple() > other.as_tuple() @classmethod def from_json(cls, name, values) : def from_json(cls, name: str , values: dict) -> Version : """Loads a version from devguide's json representation.""" return cls(name, status=values["status"], branch_or_tag=values["branch"]) @property def requirements(self): def requirements(self) -> list[str] : """Generate the right requirements for this version. Since CPython 3.8 a Doc/requirements.txt file can be used. Expand Down Expand Up @@ -213,9 +215,10 @@ def requirements(self): return reqs + ["sphinx==2.3.1"] if self.name == "3.5": return reqs + ["sphinx==1.8.4"] raise ValueError("unreachable") @property def changefreq(self): def changefreq(self) -> str : """Estimate this version change frequency, for the sitemap.""" return {"EOL": "never", "security-fixes": "yearly"}.get(self.status, "daily") Expand All @@ -224,17 +227,17 @@ def as_tuple(self) -> tuple[int, ...]: return version_to_tuple(self.name) @property def url(self): def url(self) -> str : """The doc URL of this version in production.""" return f"https://docs.python.org/{self.name}/" @property def title(self): def title(self) -> str : """The title of this version's doc, for the sidebar.""" return f"Python {self.name} ({self.status})" @property def picker_label(self): def picker_label(self) -> str : """Forge the label of a version picker.""" if self.status == "in development": return f"dev ({self.name})" Expand All @@ -254,7 +257,7 @@ def __reversed__(self) -> Iterator[Language]: return reversed(self._seq) @classmethod def from_json(cls, defaults, languages) -> Languages: def from_json(cls, defaults: dict , languages: dict ) -> Languages: default_translated_name = defaults.get("translated_name", "") default_in_prod = defaults.get("in_prod", True) default_sphinxopts = defaults.get("sphinxopts", []) Expand Down Expand Up @@ -290,17 +293,19 @@ class Language: html_only: bool = False @property def tag(self): def tag(self) -> str : return self.iso639_tag.replace("_", "-").lower() @property def switcher_label(self): def switcher_label(self) -> str : if self.translated_name: return f"{self.name} | {self.translated_name}" return self.name def run(cmd, cwd=None) -> subprocess.CompletedProcess: def run( cmd: Sequence[str | Path], cwd: Path | None = None ) -> subprocess.CompletedProcess: """Like subprocess.run, with logging before and after the command execution.""" cmd = list(map(str, cmd)) cmdstring = shlex.join(cmd) Expand All @@ -326,7 +331,7 @@ def run(cmd, cwd=None) -> subprocess.CompletedProcess: return result def run_with_logging(cmd, cwd= None): def run_with_logging(cmd: Sequence[str | Path] , cwd: Path | None = None) -> None : """Like subprocess.check_call, with logging before the command execution.""" cmd = list(map(str, cmd)) logging.debug("Run: '%s'", shlex.join(cmd)) Expand All @@ -348,13 +353,13 @@ def run_with_logging(cmd, cwd=None): raise subprocess.CalledProcessError(return_code, cmd[0]) def changed_files(left, right) : def changed_files(left: Path , right: Path) -> list[str] : """Compute a list of different files between left and right, recursively. Resulting paths are relative to left. """ changed = [] def traverse(dircmp_result) : def traverse(dircmp_result: filecmp.dircmp) -> None : base = Path(dircmp_result.left).relative_to(left) for file in dircmp_result.diff_files: changed.append(str(base / file)) Expand All @@ -374,11 +379,11 @@ class Repository: remote: str directory: Path def run(self, *args) : def run(self, *args: str) -> subprocess.CompletedProcess : """Run git command in the clone repository.""" return run(("git", "-C", self.directory) + args) def get_ref(self, pattern) : def get_ref(self, pattern: str) -> str : """Return the reference of a given tag or branch.""" try: # Maybe it's a branch Expand All @@ -387,7 +392,7 @@ def get_ref(self, pattern): # Maybe it's a tag return self.run("show-ref", "-s", "tags/" + pattern).stdout.strip() def fetch(self): def fetch(self) -> subprocess.CompletedProcess : """Try (and retry) to run git fetch.""" try: return self.run("fetch") Expand All @@ -396,12 +401,12 @@ def fetch(self): sleep(5) return self.run("fetch") def switch(self, branch_or_tag) : def switch(self, branch_or_tag: str) -> None : """Reset and cleans the repository to the given branch or tag.""" self.run("reset", "--hard", self.get_ref(branch_or_tag), "--") self.run("clean", "-dfqx") def clone(self): def clone(self) -> bool : """Maybe clone the repository, if not already cloned.""" if (self.directory / ".git").is_dir(): return False # Already cloned Expand All @@ -410,21 +415,23 @@ def clone(self): run(["git", "clone", self.remote, self.directory]) return True def update(self): def update(self) -> None : self.clone() or self.fetch() def version_to_tuple(version) -> tuple[int, ...]: def version_to_tuple(version: str ) -> tuple[int, ...]: """Transform a version string to a tuple, for easy comparisons.""" return tuple(int(part) for part in version.split(".")) def tuple_to_version(version_tuple) : def tuple_to_version(version_tuple: tuple[int, ...]) -> str : """Reverse version_to_tuple.""" return ".".join(str(part) for part in version_tuple) def locate_nearest_version(available_versions, target_version): def locate_nearest_version( available_versions: Collection[str], target_version: str ) -> str: """Look for the nearest version of target_version in available_versions. Versions are to be given as tuples, like (3, 7) for 3.7. Expand Down Expand Up @@ -468,7 +475,7 @@ def edit(file: Path): temporary.rename(file) def setup_switchers(versions: Versions, languages: Languages, html_root: Path): def setup_switchers(versions: Versions, languages: Languages, html_root: Path) -> None : """Setup cross-links between CPython versions: - Cross-link various languages in a language switcher - Cross-link various versions in a version switcher Expand Down Expand Up @@ -499,12 +506,12 @@ def setup_switchers(versions: Versions, languages: Languages, html_root: Path): ofile.write(line) def head(text, lines= 10): def head(text: str , lines: int = 10) -> str : """Return the first *lines* lines from the given text.""" return "\n".join(text.split("\n")[:lines]) def version_info(): def version_info() -> None : """Handler for --version.""" try: platex_version = head( Expand Down Expand Up @@ -554,15 +561,15 @@ class DocBuilder: theme: Path @property def html_only(self): def html_only(self) -> bool : return ( self.select_output in {"only-html", "only-html-en"} or self.quick or self.language.html_only ) @property def includes_html(self): def includes_html(self) -> bool : """Does the build we are running include HTML output?""" return self.select_output != "no-html" Expand Down Expand Up @@ -601,12 +608,12 @@ def checkout(self) -> Path: """Path to CPython git clone.""" return self.build_root / _checkout_name(self.select_output) def clone_translation(self): def clone_translation(self) -> None : self.translation_repo.update() self.translation_repo.switch(self.translation_branch) @property def translation_repo(self): def translation_repo(self) -> Repository : """See PEP 545 for translations repository naming convention.""" locale_repo = f"https://github.com/python/python-docs-{self.language.tag}.git" Expand All @@ -620,7 +627,7 @@ def translation_repo(self): return Repository(locale_repo, locale_clone_dir) @property def translation_branch(self): def translation_branch(self) -> str : """Some CPython versions may be untranslated, being either too old or too new. Expand All @@ -633,7 +640,7 @@ def translation_branch(self): branches = re.findall(r"/([0-9]+\.[0-9]+)$", remote_branches, re.M) return locate_nearest_version(branches, self.version.name) def build(self): def build(self) -> None : """Build this version/language doc.""" logging.info("Build start.") start_time = perf_counter() Expand Down Expand Up @@ -702,7 +709,7 @@ def build(self): ) logging.info("Build done (%s).", format_seconds(perf_counter() - start_time)) def build_venv(self): def build_venv(self) -> None : """Build a venv for the specific Python version. So we can reuse them from builds to builds, while they contain Expand Down Expand Up @@ -819,7 +826,7 @@ def copy_build_to_webroot(self, http: urllib3.PoolManager) -> None: "Publishing done (%s).", format_seconds(perf_counter() - start_time) ) def should_rebuild(self, force: bool): def should_rebuild(self, force: bool) -> str | Literal[False] : state = self.load_state() if not state: logging.info("Should rebuild: no previous state found.") Expand Down Expand Up @@ -865,7 +872,9 @@ def load_state(self) -> dict: except (KeyError, FileNotFoundError): return {} def save_state(self, build_start: dt.datetime, build_duration: float, trigger: str): def save_state( self, build_start: dt.datetime, build_duration: float, trigger: str ) -> None: """Save current CPython sha1 and current translation sha1. Using this we can deduce if a rebuild is needed or not. Expand Down Expand Up @@ -911,14 +920,16 @@ def format_seconds(seconds: float) -> str: case h, m, s: return f"{h}h {m}m {s}s" raise ValueError("unreachable") def _checkout_name(select_output: str | None) -> str: if select_output is not None: return f"cpython-{select_output}" return "cpython" def main(): def main() -> None : """Script entry point.""" args = parse_args() setup_logging(args.log_directory, args.select_output) Expand All @@ -934,7 +945,7 @@ def main(): build_docs_with_lock(args, "build_docs_html_en.lock") def parse_args(): def parse_args() -> argparse.Namespace : """Parse command-line arguments.""" parser = argparse.ArgumentParser( Expand Down Expand Up @@ -1028,7 +1039,7 @@ def parse_args(): return args def setup_logging(log_directory: Path, select_output: str | None): def setup_logging(log_directory: Path, select_output: str | None) -> None : """Setup logging to stderr if run by a human, or to a file if run from a cron.""" log_format = "%(asctime)s %(levelname)s: %(message)s" if sys.stderr.isatty(): Expand Down Expand Up @@ -1174,7 +1185,9 @@ def parse_languages_from_config() -> Languages: return Languages.from_json(config["defaults"], config["languages"]) def build_sitemap(versions: Versions, languages: Languages, www_root: Path, group): def build_sitemap( versions: Versions, languages: Languages, www_root: Path, group: str ) -> None: """Build a sitemap with all live versions and translations.""" if not www_root.exists(): logging.info("Skipping sitemap generation (www root does not even exist).") Expand All @@ -1189,7 +1202,7 @@ def build_sitemap(versions: Versions, languages: Languages, www_root: Path, grou run(["chgrp", group, sitemap_path]) def build_404(www_root: Path, group) : def build_404(www_root: Path, group: str) -> None : """Build a nice 404 error page to display in case PDFs are not built yet.""" if not www_root.exists(): logging.info("Skipping 404 page generation (www root does not even exist).") Expand All @@ -1203,8 +1216,8 @@ def build_404(www_root: Path, group): def copy_robots_txt( www_root: Path, group, skip_cache_invalidation, group: str , skip_cache_invalidation: bool , http: urllib3.PoolManager, ) -> None: """Copy robots.txt to www_root.""" Expand Down Expand Up @@ -1322,7 +1335,7 @@ def proofread_canonicals( ) def _check_canonical_rel(file: Path, www_root: Path): def _check_canonical_rel(file: Path, www_root: Path) -> Path | None : # Check for a canonical relation link in the HTML. # If one exists, ensure that the target exists # or otherwise remove the canonical link element. Expand Down