diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..1003a92 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,27 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: monthly + assignees: + - "ezio-melotti" + groups: + actions: + patterns: + - "*" + cooldown: + default-days: 7 + + - package-ecosystem: pip + directory: "/" + schedule: + interval: monthly + assignees: + - "ezio-melotti" + groups: + pip: + patterns: + - "*" + cooldown: + default-days: 7 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index cd9d07e..2068239 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,17 +7,14 @@ permissions: {} env: FORCE_COLOR: 1 PIP_DISABLE_PIP_VERSION_CHECK: 1 + RUFF_OUTPUT_FORMAT: github jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: persist-credentials: false - - uses: actions/setup-python@v5 - with: - python-version: "3.x" - cache: pip - - uses: pre-commit/action@v3.0.1 + - uses: j178/prek-action@v1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b64e8a3..d00a215 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,9 +1,6 @@ name: Test -on: - push: - pull_request: - workflow_dispatch: +on: [push, pull_request, workflow_dispatch] permissions: {} @@ -15,10 +12,10 @@ jobs: name: Integration test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: persist-credentials: false - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.13" @@ -40,7 +37,7 @@ jobs: --branches 3.14 - name: Upload documentation - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: www-root path: ./www @@ -52,16 +49,16 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.13", "3.14"] + python-version: ["3.13", "3.14", "3.15"] os: [windows-latest, macos-latest, ubuntu-latest] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} allow-prereleases: true diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 0000000..1000265 --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,6 @@ +# https://docs.zizmor.sh/configuration/ +rules: + unpinned-uses: + config: + policies: + "*": ref-pin diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 869a979..d4d9fdf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: check-added-large-files - id: check-case-conflict @@ -14,44 +14,45 @@ repos: - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.5 + rev: v0.14.10 hooks: - - id: ruff + - id: ruff-check args: [--fix] - id: ruff-format - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.31.1 + rev: 0.36.0 hooks: + - id: check-dependabot - id: check-github-workflows - repo: https://github.com/rhysd/actionlint - rev: v1.7.7 + rev: v1.7.10 hooks: - id: actionlint - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.3.1 + rev: v1.19.0 hooks: - id: zizmor - repo: https://github.com/tox-dev/pyproject-fmt - rev: v2.5.0 + rev: v2.11.1 hooks: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.23 + rev: v0.24.1 hooks: - id: validate-pyproject - repo: https://github.com/tox-dev/tox-ini-fmt - rev: 1.5.0 + rev: 1.7.1 hooks: - id: tox-ini-fmt - repo: https://github.com/rbubley/mirrors-prettier - rev: v3.5.1 + rev: v3.7.4 hooks: - id: prettier files: templates/switchers.js diff --git a/README.md b/README.md index 8f914c6..407d0f2 100644 --- a/README.md +++ b/README.md @@ -20,42 +20,64 @@ If you don't need to build all translations of all branches, add `--languages en --branches main`. -## Check current version +## Sphinx versions + + +Sphinx configuration in various branches: + +| version | requirements.txt | conf.py | +|-----------|--------------------|----------------------| +| 2.6 | ø | ø | +| 2.7 | ø | needs_sphinx='1.2' | +| 3.0 | ø | ø | +| 3.1 | ø | ø | +| 3.2 | ø | ø | +| 3.3 | ø | ø | +| 3.4 | ø | needs_sphinx='1.2' | +| 3.5 | ø | needs_sphinx='1.8' | +| 3.6 | ø | needs_sphinx='1.2' | +| 3.7 | sphinx==2.3.1 | needs_sphinx='1.6.6' | +| 3.8 | sphinx==2.4.4 | needs_sphinx='1.8' | +| 3.9 | sphinx==2.4.4 | needs_sphinx='1.8' | +| 3.10 | sphinx==3.4.3 | needs_sphinx='3.2' | +| 3.11 | sphinx~=7.2.0 | needs_sphinx='4.2' | +| 3.12 | sphinx~=8.2.0 | needs_sphinx='8.2.0' | +| 3.13 | sphinx~=8.2.0 | needs_sphinx='8.2.0' | +| 3.14 | sphinx~=8.2.0 | needs_sphinx='8.2.0' | +| 3.15 | sphinx~=8.2.0 | needs_sphinx='8.2.0' | + +Sphinx build as seen on docs.python.org: + +| version | el | en | es | fr | bn-in | id | it | ja | ko | pl | pt-br | ro | sv | tr | uk | zh-cn | zh-tw | +|-----------|-------|-------|-------|-------|---------|-------|-------|-------|-------|-------|---------|-------|-------|-------|-------|---------|---------| +| 2.6 | ø | 0.6.5 | ø | ø | ø | ø | ø | ø | ø | ø | ø | ø | ø | ø | ø | ø | ø | +| 2.7 | ø | 2.3.1 | ø | 2.3.1 | ø | 2.3.1 | ø | 2.3.1 | 2.3.1 | ø | 2.3.1 | ø | ø | ø | ø | 2.3.1 | 2.3.1 | +| 3.0 | ø | 0.6 | ø | ø | ø | ø | ø | ø | ø | ø | ø | ø | ø | ø | ø | ø | ø | +| 3.1 | ø | 0.6.5 | ø | ø | ø | ø | ø | ø | ø | ø | ø | ø | ø | ø | ø | ø | ø | +| 3.2 | ø | 1.0.7 | ø | ø | ø | ø | ø | ø | ø | ø | ø | ø | ø | ø | ø | ø | ø | +| 3.3 | ø | 1.2 | ø | ø | ø | ø | ø | ø | ø | ø | ø | ø | ø | ø | ø | ø | ø | +| 3.4 | ø | 1.2.3 | ø | ø | ø | ø | ø | ø | ø | ø | ø | ø | ø | ø | ø | ø | ø | +| 3.5 | ø | 1.8.4 | 1.8.4 | 1.8.4 | ø | 1.8.4 | ø | 1.8.4 | 1.8.4 | 1.8.4 | 1.8.4 | ø | ø | ø | ø | 1.8.4 | 1.8.4 | +| 3.6 | ø | 2.3.1 | 2.3.1 | 2.3.1 | ø | 2.3.1 | ø | 2.3.1 | 2.3.1 | 2.3.1 | 2.3.1 | ø | ø | ø | ø | 2.3.1 | 2.3.1 | +| 3.7 | ø | 2.3.1 | 2.3.1 | 2.3.1 | ø | 2.3.1 | 2.3.1 | 2.3.1 | 2.3.1 | 2.3.1 | 2.3.1 | ø | ø | 2.3.1 | 2.3.1 | 2.3.1 | 2.3.1 | +| 3.8 | ø | 2.4.4 | 2.4.4 | 2.4.4 | ø | 2.4.4 | 2.4.4 | 2.4.4 | 2.4.4 | 2.4.4 | 2.4.4 | ø | ø | 2.4.4 | 2.4.4 | 2.4.4 | 2.4.4 | +| 3.9 | 2.4.4 | 2.4.4 | 2.4.4 | 2.4.4 | 2.4.4 | 2.4.4 | 2.4.4 | 2.4.4 | 2.4.4 | 2.4.4 | 2.4.4 | 2.4.4 | ø | 2.4.4 | 2.4.4 | 2.4.4 | 2.4.4 | +| 3.10 | 3.4.3 | 3.4.3 | 3.4.3 | 3.4.3 | 3.4.3 | 3.4.3 | 3.4.3 | 3.4.3 | 3.4.3 | 3.4.3 | 3.4.3 | 3.4.3 | ø | 3.4.3 | 3.4.3 | 3.4.3 | 3.4.3 | +| 3.11 | 7.2.6 | 7.2.6 | 7.2.6 | 7.2.6 | 7.2.6 | 7.2.6 | 7.2.6 | 7.2.6 | 7.2.6 | 7.2.6 | 7.2.6 | 7.2.6 | ø | 7.2.6 | 7.2.6 | 7.2.6 | 7.2.6 | +| 3.12 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | ø | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | +| 3.13 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | +| 3.14 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | +| 3.15 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | 8.2.3 | + Install `tools_requirements.txt` then run `python check_versions.py -../cpython/` (pointing to a real CPython clone) to see which version -of Sphinx we're using where: - - Sphinx configuration in various branches: - - ========= ============= ================== ==================== - version travis requirements.txt conf.py - ========= ============= ================== ==================== - 2.7 sphinx~=2.0.1 ø needs_sphinx='1.2' - 3.5 sphinx==1.8.2 ø needs_sphinx='1.8' - 3.6 sphinx==1.8.2 ø needs_sphinx='1.2' - 3.7 sphinx==1.8.2 sphinx==2.3.1 needs_sphinx="1.6.6" - 3.8 ø sphinx==2.4.4 needs_sphinx='1.8' - 3.9 ø sphinx==2.4.4 needs_sphinx='1.8' - 3.10 ø sphinx==3.4.3 needs_sphinx='3.2' - 3.11 ø sphinx~=7.2.0 needs_sphinx='4.2' - 3.12 ø sphinx~=8.2.0 needs_sphinx='8.2.0' - 3.13 ø sphinx~=8.2.0 needs_sphinx='8.2.0' - 3.14 ø sphinx~=8.2.0 needs_sphinx='8.2.0' - ========= ============= ================== ==================== - - Sphinx build as seen on docs.python.org: - - ========= ===== ===== ===== ===== ===== ===== ===== ===== ======= ===== ===== ======= ======= - version en es fr id it ja ko pl pt-br tr uk zh-cn zh-tw - ========= ===== ===== ===== ===== ===== ===== ===== ===== ======= ===== ===== ======= ======= - 3.9 2.4.4 2.4.4 2.4.4 2.4.4 2.4.4 2.4.4 2.4.4 2.4.4 2.4.4 2.4.4 2.4.4 2.4.4 2.4.4 - 3.10 3.4.3 3.4.3 3.4.3 3.4.3 3.4.3 3.4.3 3.4.3 3.4.3 3.4.3 3.4.3 3.4.3 3.4.3 3.4.3 - 3.11 7.2.6 7.2.6 7.2.6 7.2.6 7.2.6 7.2.6 7.2.6 7.2.6 7.2.6 7.2.6 7.2.6 7.2.6 7.2.6 - 3.12 8.2.3 8.2.3 8.2.3 8.2.3 8.2.3 8.2.3 8.2.3 8.2.3 8.2.3 8.2.3 8.2.3 8.2.3 8.2.3 - 3.13 8.2.3 8.2.3 8.2.3 8.2.3 8.2.3 8.2.3 8.2.3 8.2.3 8.2.3 8.2.3 8.2.3 8.2.3 8.2.3 - 3.14 8.2.3 8.2.3 8.2.3 8.2.3 8.2.3 8.2.3 8.2.3 8.2.3 8.2.3 8.2.3 8.2.3 8.2.3 8.2.3 - ========= ===== ===== ===== ===== ===== ===== ===== ===== ======= ===== ===== ======= ======= +../cpython/` (pointing to a real CPython clone) to see which versions +of Sphinx we're using. + +Or run `tox -e cog` (with a clone at `../cpython`) to directly update these tables. ## Manually rebuild a branch @@ -71,5 +93,5 @@ To manually rebuild a branch, for example 3.11: ssh docs.nyc1.psf.io sudo su --shell=/bin/bash docsbuild screen -DUR # Rejoin screen session if it exists, otherwise create a new one -/srv/docsbuild/venv/bin/python /srv/docsbuild/scripts/build_docs.py --force --branch 3.11 +/srv/docsbuild/venv/bin/python /srv/docsbuild/scripts/build_docs.py --force --branches 3.11 ``` diff --git a/build_docs.py b/build_docs.py index c75f096..e44ffa0 100755 --- a/build_docs.py +++ b/build_docs.py @@ -26,7 +26,7 @@ ``` Languages are stored in `config.toml` while versions are discovered -from the devguide. +from peps.python.org (generated by `python-releases.toml`). -q selects "quick build", which means to build only HTML. @@ -86,8 +86,6 @@ import sentry_sdk except ImportError: sentry_sdk = None -else: - sentry_sdk.init() HERE = Path(__file__).resolve().parent @@ -111,13 +109,18 @@ def from_json(cls, data: dict) -> Versions: for name, release in data.items(): branch = release["branch"] status = release["status"] + if status in Version.SKIP_STATUSES: + logging.info("Skipping %s with status %r", name, status) + continue status = Version.SYNONYMS.get(status, status) if status not in Version.STATUSES: - msg = ( - f"Saw invalid version status {status!r}, " - f"expected to be one of {permitted}." + logging.warning( + "Saw invalid version status %r, expected to be one of %s. Context: %s", + status, + permitted, + release, ) - raise ValueError(msg) + continue versions.append(Version(name=name, status=status, branch_or_tag=branch)) return cls(sorted(versions, key=Version.as_tuple)) @@ -151,10 +154,25 @@ class Version: """Represents a CPython version and its documentation build dependencies.""" name: str - status: Literal["EOL", "security-fixes", "stable", "pre-release", "in development"] + status: Literal[ + "in development", + "pre-release", + "stable", + "security-fixes", + "EOL", + ] branch_or_tag: str - STATUSES = {"EOL", "security-fixes", "stable", "pre-release", "in development"} + STATUSES = { + "in development", + "pre-release", + "stable", + "security-fixes", + "EOL", + } + + # Statuses for versions we don't build docs for at all. + SKIP_STATUSES = {"planned"} # Those synonyms map branch status vocabulary found in the devguide # with our vocabulary. @@ -294,14 +312,6 @@ class Language: def tag(self) -> str: return self.iso639_tag.replace("_", "-").lower() - @property - def is_translation(self) -> bool: - return self.tag != "en" - - @property - def locale_repo_url(self) -> str: - return f"https://github.com/python/python-docs-{self.tag}.git" - @property def switcher_label(self) -> str: if self.translated_name: @@ -309,6 +319,75 @@ def switcher_label(self) -> str: return self.name +@dataclasses.dataclass(frozen=True, kw_only=True, slots=True) +class BuildMetadata: + _version: Version + _language: Language + + @property + def sphinxopts(self) -> Sequence[str]: + return self._language.sphinxopts + + @property + def iso639_tag(self) -> str: + return self._language.iso639_tag + + @property + def html_only(self) -> bool: + return self._language.html_only or not self._language.in_prod + + @property + def url(self): + """The URL of this version in production.""" + if self.is_translation: + return f"https://docs.python.org/{self.version}/{self.language}/" + return f"https://docs.python.org/{self.version}/" + + @property + def branch_or_tag(self) -> str: + return self._version.branch_or_tag + + @property + def status(self) -> str: + return self._version.status + + @property + def is_eol(self) -> bool: + return self._version.status == "EOL" + + @property + def dependencies(self) -> list[str]: + return self._version.requirements + + @property + def version(self): + return self._version.name + + @property + def version_tuple(self): + return self._version.as_tuple() + + @property + def language(self): + return self._language.tag + + @property + def is_translation(self): + return self.language != "en" + + @property + def slug(self) -> str: + return f"{self.language}/{self.version}" + + @property + def venv_name(self) -> str: + return f"venv-{self.version}" + + @property + def locale_repo_url(self) -> str: + return f"https://github.com/python/python-docs-{self.language}.git" + + def run( cmd: Sequence[str | Path], cwd: Path | None = None ) -> subprocess.CompletedProcess: @@ -534,8 +613,7 @@ def version_info() -> None: class DocBuilder: """Builder for a CPython version and a language.""" - version: Version - language: Language + build_meta: BuildMetadata cpython_repo: Repository docs_by_version_content: bytes switchers_content: bytes @@ -553,7 +631,7 @@ def html_only(self) -> bool: return ( self.select_output in {"only-html", "only-html-en"} or self.quick - or self.language.html_only + or self.build_meta.html_only ) @property @@ -567,11 +645,11 @@ def run(self, http: urllib3.PoolManager, force_build: bool) -> bool | None: start_timestamp = dt.datetime.now(tz=dt.UTC).replace(microsecond=0) logging.info("Running.") try: - if self.language.html_only and not self.includes_html: + if self.build_meta.html_only and not self.includes_html: logging.info("Skipping non-HTML build (language is HTML-only).") return None # skipped - self.cpython_repo.switch(self.version.branch_or_tag) - if self.language.is_translation: + self.cpython_repo.switch(self.build_meta.branch_or_tag) + if self.build_meta.is_translation: self.clone_translation() if trigger_reason := self.should_rebuild(force_build): self.build_venv() @@ -593,7 +671,7 @@ def run(self, http: urllib3.PoolManager, force_build: bool) -> bool | None: @property def locale_dir(self) -> Path: - return self.build_root / self.version.name / "locale" + return self.build_root / self.build_meta.version / "locale" @property def checkout(self) -> Path: @@ -608,8 +686,8 @@ def clone_translation(self) -> None: def translation_repo(self) -> Repository: """See PEP 545 for translations repository naming convention.""" - locale_clone_dir = self.locale_dir / self.language.iso639_tag / "LC_MESSAGES" - return Repository(self.language.locale_repo_url, locale_clone_dir) + locale_clone_dir = self.locale_dir / self.build_meta.iso639_tag / "LC_MESSAGES" + return Repository(self.build_meta.locale_repo_url, locale_clone_dir) @property def translation_branch(self) -> str: @@ -623,25 +701,25 @@ def translation_branch(self) -> str: """ remote_branches = self.translation_repo.run("branch", "-r").stdout branches = re.findall(r"/([0-9]+\.[0-9]+)$", remote_branches, re.M) - return locate_nearest_version(branches, self.version.name) + return locate_nearest_version(branches, self.build_meta.version) def build(self) -> None: """Build this version/language doc.""" logging.info("Build start.") start_time = perf_counter() - sphinxopts = list(self.language.sphinxopts) - if self.language.is_translation: + sphinxopts = list(self.build_meta.sphinxopts) + if self.build_meta.is_translation: sphinxopts.extend(( f"-D locale_dirs={self.locale_dir}", - f"-D language={self.language.iso639_tag}", + f"-D language={self.build_meta.iso639_tag}", "-D gettext_compact=0", "-D translation_progress_classes=1", )) - if self.version.status == "EOL": + if self.build_meta.is_eol: sphinxopts.append("-D html_context.outdated=1") - if self.version.status in ("in development", "pre-release"): + if self.build_meta.status in ("in development", "pre-release"): maketarget = "autobuild-dev" else: maketarget = "autobuild-stable" @@ -653,9 +731,7 @@ def build(self) -> None: blurb = self.venv / "bin" / "blurb" if self.includes_html: - site_url = self.version.url - if self.language.is_translation: - site_url += f"{self.language.tag}/" + site_url = self.build_meta.url # Define a tag to enable opengraph socialcards previews # (used in Doc/conf.py and requires matplotlib) sphinxopts += ( @@ -663,7 +739,7 @@ def build(self) -> None: f"-D ogp_site_url={site_url}", ) - if self.version.as_tuple() < (3, 8): + if self.build_meta.version_tuple < (3, 8): # Disable CPython switchers, we handle them now: text = (self.checkout / "Doc" / "Makefile").read_text(encoding="utf-8") text = text.replace(" -A switchers=1", "") @@ -696,12 +772,12 @@ def build_venv(self) -> None: So we can reuse them from builds to builds, while they contain different Sphinx versions. """ - requirements = list(self.version.requirements) + requirements = list(self.build_meta.dependencies) if self.includes_html: # opengraph previews requirements.append("matplotlib>=3") - venv_path = self.build_root / f"venv-{self.version.name}" + venv_path = self.build_root / self.build_meta.venv_name venv.create(venv_path, symlinks=os.name != "nt", with_pip=True) run( ( @@ -726,7 +802,7 @@ def setup_indexsidebar(self) -> None: dbv_path = tmpl_dst / "_docs_by_version.html" shutil.copy(tmpl_src / "indexsidebar.html", tmpl_dst / "indexsidebar.html") - if self.version.status != "EOL": + if not self.build_meta.is_eol: dbv_path.write_bytes(self.docs_by_version_content) else: shutil.copy(tmpl_src / "_docs_by_version.html", dbv_path) @@ -736,14 +812,14 @@ def copy_build_to_webroot(self, http: urllib3.PoolManager) -> None: logging.info("Publishing start.") start_time = perf_counter() self.www_root.mkdir(parents=True, exist_ok=True) - if not self.language.is_translation: - target = self.www_root / self.version.name + if not self.build_meta.is_translation: + target = self.www_root / self.build_meta.version else: - language_dir = self.www_root / self.language.tag + language_dir = self.www_root / self.build_meta.language language_dir.mkdir(parents=True, exist_ok=True) chgrp(language_dir, group=self.group, recursive=True) language_dir.chmod(0o775) - target = language_dir / self.version.name + target = language_dir / self.build_meta.version target.mkdir(parents=True, exist_ok=True) try: @@ -792,8 +868,7 @@ def copy_build_to_webroot(self, http: urllib3.PoolManager) -> None: logging.info("%s files changed", changed) if changed and not self.skip_cache_invalidation: - surrogate_key = f"{self.language.tag}/{self.version.name}" - purge_surrogate_key(http, surrogate_key) + purge_surrogate_key(http, self.build_meta.slug) logging.info( "Publishing done (%s).", format_seconds(perf_counter() - start_time) ) @@ -804,7 +879,7 @@ def should_rebuild(self, force: bool) -> str | Literal[False]: logging.info("Should rebuild: no previous state found.") return "no previous state" cpython_sha = self.cpython_repo.run("rev-parse", "HEAD").stdout.strip() - if self.language.is_translation: + if self.build_meta.is_translation: translation_sha = self.translation_repo.run( "rev-parse", "HEAD" ).stdout.strip() @@ -839,7 +914,7 @@ def load_state(self) -> dict: state_file = self.build_root / "state.toml" try: return tomlkit.loads(state_file.read_text(encoding="UTF-8"))[ - f"/{self.language.tag}/{self.version.name}/" + f"/{self.build_meta.slug}/" ] except (KeyError, FileNotFoundError): return {} @@ -860,14 +935,14 @@ def save_state( except FileNotFoundError: states = tomlkit.document() - key = f"/{self.language.tag}/{self.version.name}/" + key = f"/{self.build_meta.slug}/" state = { "last_build_start": build_start, "last_build_duration": round(build_duration, 0), "triggered_by": trigger, "cpython_sha": self.cpython_repo.run("rev-parse", "HEAD").stdout.strip(), } - if self.language.is_translation: + if self.build_meta.is_translation: state["translation_sha"] = self.translation_repo.run( "rev-parse", "HEAD" ).stdout.strip() @@ -948,10 +1023,14 @@ def main() -> int: args = parse_args() setup_logging(args.log_directory, args.select_output) load_environment_variables() + if sentry_sdk: + sentry_sdk.init() if args.select_output is None: return build_docs_with_lock(args, "build_docs.lock") if args.select_output == "no-html": + # Disable Plausible analytics for download copies + os.environ.pop("PYTHON_DOCS_ENABLE_ANALYTICS", None) return build_docs_with_lock(args, "build_docs_archives.lock") if args.select_output == "only-html": return build_docs_with_lock(args, "build_docs_html.lock") @@ -967,6 +1046,7 @@ def parse_args() -> argparse.Namespace: description="Runs a build of the Python docs for various branches.", allow_abbrev=False, ) + parser.suggest_on_error = True parser.add_argument( "--select-output", choices=("no-html", "only-html", "only-html-en"), @@ -1116,13 +1196,13 @@ def build_docs(args: argparse.Namespace) -> int: logging.info("Full build start.") start_time = perf_counter() http = urllib3.PoolManager() - versions = parse_versions_from_devguide(http) + versions = parse_versions_from_peps_site(http) languages = parse_languages_from_config() # Reverse languages but not versions, because we take version-language # pairs from the end of the list, effectively reversing it. # This runs languages in config.toml order and versions newest first. todo = [ - (version, language) + BuildMetadata(_version=version, _language=language) for version in versions.filter(args.branches) for language in reversed(languages.filter(args.languages)) ] @@ -1141,20 +1221,19 @@ def build_docs(args: argparse.Namespace) -> int: args.build_root / _checkout_name(args.select_output), ) while todo: - version, language = todo.pop() + build_props = todo.pop() logging.root.handlers[0].setFormatter( logging.Formatter( - f"%(asctime)s %(levelname)s {language.tag}/{version.name}: %(message)s" + f"%(asctime)s %(levelname)s {build_props.slug}: %(message)s" ) ) if sentry_sdk: scope = sentry_sdk.get_isolation_scope() - scope.set_tag("version", version.name) - scope.set_tag("language", language.tag) + scope.set_tag("version", build_props.version) + scope.set_tag("language", build_props.language) cpython_repo.update() builder = DocBuilder( - version, - language, + build_props, cpython_repo, docs_by_version_content, switchers_content, @@ -1162,7 +1241,7 @@ def build_docs(args: argparse.Namespace) -> int: ) built_successfully = builder.run(http, force_build=force_build) if built_successfully: - build_succeeded.add((version.name, language.tag)) + build_succeeded.add(build_props.slug) elif built_successfully is not None: any_build_failed = True @@ -1196,11 +1275,10 @@ def build_docs(args: argparse.Namespace) -> int: return EX_FAILURE if any_build_failed else EX_OK -def parse_versions_from_devguide(http: urllib3.PoolManager) -> Versions: +def parse_versions_from_peps_site(http: urllib3.PoolManager) -> Versions: releases = http.request( "GET", - "https://raw.githubusercontent.com/" - "python/devguide/main/include/release-cycle.json", + "https://peps.python.org/api/release-cycle.json", timeout=30, ).json() return Versions.from_json(releases) @@ -1285,7 +1363,7 @@ def make_symlinks( group: str, versions: Versions, languages: Languages, - successful_builds: Set[tuple[str, str]], + successful_builds: Set[str], skip_cache_invalidation: bool, http: urllib3.PoolManager, ) -> None: @@ -1305,7 +1383,7 @@ def make_symlinks( ("dev", versions.current_dev.name), ): for language in languages: - if (symlink_target, language.tag) in successful_builds: + if f"{language.tag}/{symlink_target}" in successful_builds: symlink( www_root, language.tag, diff --git a/check_times.py b/check_times.py index 2b3d2f9..e3bbaea 100644 --- a/check_times.py +++ b/check_times.py @@ -10,6 +10,7 @@ $ python check_times.py """ +import argparse import gzip import tomllib from pathlib import Path @@ -78,17 +79,36 @@ def calc_time(lines: list[str]) -> None: if __name__ == "__main__": - print("Build times (HTML only; English)") - print("=======================") - print() - calc_time(get_lines("docsbuild-only-html-en.log")) - - print("Build times (HTML only)") - print("=======================") - print() - calc_time(get_lines("docsbuild-only-html.log")) - - print("Build times (no HTML)") - print("=====================") - print() - calc_time(get_lines("docsbuild-no-html.log")) + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter + ) + ALL_BUILDS = ("no-html", "only-html", "only-html-en") + parser.add_argument( + "--select-output", + choices=ALL_BUILDS, + nargs="*", + help="Choose what builds to show (default: all).", + ) + args = parser.parse_args() + parser.suggest_on_error = True + + if not args.select_output: + args.select_output = ALL_BUILDS + + if "only-html-en" in args.select_output: + print("Build times (HTML only; English)") + print("=======================") + print() + calc_time(get_lines("docsbuild-only-html-en.log")) + + if "only-html" in args.select_output: + print("Build times (HTML only)") + print("=======================") + print() + calc_time(get_lines("docsbuild-only-html.log")) + + if "no-html" in args.select_output: + print("Build times (no HTML)") + print("=====================") + print() + calc_time(get_lines("docsbuild-no-html.log")) diff --git a/check_versions.py b/check_versions.py index 1a1016f..b50d69d 100644 --- a/check_versions.py +++ b/check_versions.py @@ -15,7 +15,7 @@ logger = logging.getLogger(__name__) http = urllib3.PoolManager() -VERSIONS = build_docs.parse_versions_from_devguide(http) +VERSIONS = build_docs.parse_versions_from_peps_site(http) LANGUAGES = build_docs.parse_languages_from_config() @@ -36,13 +36,13 @@ def find_upstream_remote_name(repo: git.Repo) -> str: return f"{remote.name}/" -def find_sphinx_spec(text: str): +def find_sphinx_spec(text: str) -> str: if found := re.search( """sphinx[=<>~]{1,2}[0-9.]{3,}|needs_sphinx = [0-9.'"]*""", text, flags=re.IGNORECASE, ): - return found.group(0).replace(" ", "") + return found.group(0).replace(" ", "").replace('"', "'") return "ø" @@ -62,7 +62,6 @@ def find_sphinx_in_files(repo: git.Repo, branch_or_tag, filenames): CONF_FILES = { - "travis": ".travis.yml", "requirements.txt": "Doc/requirements.txt", "conf.py": "Doc/conf.py", } @@ -85,7 +84,7 @@ def search_sphinx_versions_in_cpython(repo: git.Repo): for version in VERSIONS ] headers = ["version", *CONF_FILES.keys()] - print(tabulate(table, headers=headers, tablefmt="rst", disable_numparse=True)) + print(tabulate(table, headers=headers, tablefmt="github", disable_numparse=True)) async def get_version_in_prod(language: str, version: str) -> str: @@ -119,16 +118,15 @@ async def which_sphinx_is_used_in_production(): for version in VERSIONS ] headers = ["version", *[language.tag for language in LANGUAGES]] - print(tabulate(table, headers=headers, tablefmt="rst", disable_numparse=True)) + print(tabulate(table, headers=headers, tablefmt="github", disable_numparse=True)) -def main(): +def check_versions(cpython_clone: str) -> None: logging.basicConfig(level=logging.INFO) logging.getLogger("charset_normalizer").setLevel(logging.WARNING) logging.getLogger("asyncio").setLevel(logging.WARNING) logging.getLogger("httpx").setLevel(logging.WARNING) - args = parse_args() - repo = git.Repo(args.cpython_clone) + repo = git.Repo(cpython_clone) print("Sphinx configuration in various branches:", end="\n\n") search_sphinx_versions_in_cpython(repo) print() @@ -136,5 +134,10 @@ def main(): asyncio.run(which_sphinx_is_used_in_production()) +def main(): + args = parse_args() + check_versions(args.cpython_clone) + + if __name__ == "__main__": main() diff --git a/config.toml b/config.toml index 489c774..7ef45b0 100644 --- a/config.toml +++ b/config.toml @@ -4,6 +4,9 @@ # html_only: If true, only create HTML files. # sphinxopts: Extra options to pass to SPHINXOPTS in the Makefile. +# Remember to update the Salt config with redirects for new translations! +# For example: https://github.com/python/psf-salt/commit/14bdc3ae054468092e5a8c3cdacfd02f43c32e19 + [defaults] # name has no default, it is mandatory. translated_name = "" @@ -15,6 +18,10 @@ sphinxopts = [ "-D latex_elements.fontenc=", ] +[languages.el] +name = "Greek" +translated_name = "Ελληνικά" + [languages.en] name = "English" @@ -36,6 +43,11 @@ sphinxopts = [ '-D latex_elements.fontenc=\\usepackage{fontspec}', ] +[languages.bn_IN] +name = "Bengali" +translated_name = "বাংলা" +in_prod = false + [languages.id] name = "Indonesian" translated_name = "Indonesia" @@ -87,6 +99,15 @@ translated_name = "polski" name = "Brazilian Portuguese" translated_name = "Português brasileiro" +[languages.ro] +name = "Romanian" +translated_name = "Românește" + +[languages.sv] +name = "Swedish" +translated_name = "Svenska" +in_prod = false + [languages.tr] name = "Turkish" translated_name = "Türkçe" diff --git a/templates/switchers.js b/templates/switchers.js index e54a278..0a042fd 100644 --- a/templates/switchers.js +++ b/templates/switchers.js @@ -63,6 +63,7 @@ const _create_placeholders_if_missing = () => { const _create_version_select = (versions) => { const select = document.createElement("select"); select.className = "version-select"; + select.setAttribute("aria-label", "Python version"); if (_IS_LOCAL) { select.disabled = true; select.title = "Version switching is disabled in local builds"; @@ -96,6 +97,7 @@ const _create_language_select = (languages) => { const select = document.createElement("select"); select.className = "language-select"; + select.setAttribute("aria-label", "Language"); if (_IS_LOCAL) { select.disabled = true; select.title = "Language switching is disabled in local builds"; diff --git a/tests/test_build_docs_versions.py b/tests/test_build_docs_versions.py index 1d8f6dc..4af2a08 100644 --- a/tests/test_build_docs_versions.py +++ b/tests/test_build_docs_versions.py @@ -1,5 +1,7 @@ from __future__ import annotations +import logging + import pytest from build_docs import Version, Versions @@ -57,16 +59,28 @@ def test_from_json() -> None: ] -def test_from_json_error() -> None: +def test_from_json_warning(caplog) -> None: # Arrange - json_data = {"2.8": {"branch": "2.8", "pep": 404, "status": "ex-release"}} - - # Act / Assert - with pytest.raises( - ValueError, - match="Saw invalid version status 'ex-release', expected to be one of", - ): - Versions.from_json(json_data) + json_data = { + "2.8": {"branch": "2.8", "pep": 404, "status": "ex-release"}, + "3.16": { + "branch": "", + "pep": 826, + "status": "planned", + "first_release": "2027-10-06", + "end_of_life": "2032-10", + "release_manager": "Savannah Ostrowski", + }, + } + + # Act + with caplog.at_level(logging.INFO): + versions = list(Versions.from_json(json_data)) + + # Assert: both should be skipped + assert versions == [] + assert "Saw invalid version status 'ex-release'" in caplog.text + assert "Skipping 3.16 with status 'planned'" in caplog.text def test_current_stable(versions) -> None: diff --git a/tox.ini b/tox.ini index 12efcdf..9047969 100644 --- a/tox.ini +++ b/tox.ini @@ -2,8 +2,9 @@ requires = tox>=4.2 env_list = + cog lint - py{314, 313} + py{315, 314, 313} [testenv] package = wheel @@ -26,6 +27,16 @@ commands = --cov-report xml \ {posargs} +[testenv:cog] +base_python = python3.13 +skip_install = true +deps = + -r requirements.txt + -r tools_requirements.txt + cogapp +commands = + cog -Pr README.md + [testenv:lint] skip_install = true deps =