From 97471a0b4aab81523f1d44996797a343a62365ca Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Fri, 23 Oct 2026 11:27:16 +0200 Subject: [PATCH] ci(rtd): install all development dependencies for doc builds Replace rtd_install_sigima.py with rtd_install_dev_deps.py, shared with Sigima and PlotPy. Besides Sigima (same-named branch, falling back to develop), branch and pull request builds now also install PythonQwt from master and guidata and PlotPy from develop, as the GitHub Actions test workflows do. Assisted-by: Claude Opus 5.5 --- .readthedocs.yaml | 4 +- scripts/rtd_install_dev_deps.py | 140 ++++++++++++++++++++++++++++++++ scripts/rtd_install_sigima.py | 106 ------------------------ 3 files changed, 142 insertions(+), 108 deletions(-) create mode 100644 scripts/rtd_install_dev_deps.py delete mode 100644 scripts/rtd_install_sigima.py diff --git a/.readthedocs.yaml b/.readthedocs.yaml index b35710b..ce86a0a 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -18,8 +18,8 @@ build: - libxcb-xfixes0 jobs: pre_install: - # Development builds may require a Sigima version not yet released on PyPI - - "python scripts/rtd_install_sigima.py" + # Development builds may require dependency versions not yet released on PyPI + - "python scripts/rtd_install_dev_deps.py" pre_build: # Start Xvfb before building docs - "Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &" diff --git a/scripts/rtd_install_dev_deps.py b/scripts/rtd_install_dev_deps.py new file mode 100644 index 0000000..91cedf1 --- /dev/null +++ b/scripts/rtd_install_dev_deps.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +""" +Install development versions of dependencies for Read the Docs builds. + +This project is developed alongside other PlotPyStack and DataLab Platform +projects: development branches and pull requests may require dependency versions +which are not yet published on PyPI, so installing the documentation requirements +from PyPI alone would fail. + +This script mirrors the GitHub Actions test workflows: + +- tag builds and release branches: nothing is done, dependencies are installed + from PyPI by the regular ``python.install`` step; +- other branches and pull requests: the dependencies listed in + ``DEV_DEPENDENCIES`` are installed from GitHub. The regular ``python.install`` + step then keeps these versions as they already satisfy the project + requirements. + +It is intended to be run as a ``build.jobs.pre_install`` step and only relies on +the standard library and ``git``. + +The same script is used by several projects: only the project-specific settings +below differ between copies. +""" + +from __future__ import annotations + +import os +import subprocess +import sys + +# ---- Project-specific settings ---------------------------------------------- +PROJECT_REPO = "https://github.com/DataLab-Platform/SigimaX.git" +#: Placeholder for the name of the branch being built +SAME_BRANCH = "{branch}" +#: Dependencies to install from GitHub for development builds: +#: package name -> (repository URL, candidate branches, first existing one is used) +DEV_DEPENDENCIES = { + "PythonQwt": ("https://github.com/PlotPyStack/PythonQwt.git", ("master",)), + "guidata": ("https://github.com/PlotPyStack/guidata.git", ("develop",)), + "PlotPy": ("https://github.com/PlotPyStack/PlotPy.git", ("develop",)), + "Sigima": ( + "https://github.com/DataLab-Platform/Sigima.git", + (SAME_BRANCH, "develop"), + ), +} +# ----------------------------------------------------------------------------- + +RELEASE_BRANCHES = ("main", "master", "release") + + +def get_remote_branches(url: str) -> dict[str, str]: + """Return the branches of a remote Git repository. + + Args: + url: Remote repository URL + + Returns: + Mapping of branch names to the commit hash of their head + """ + output = subprocess.run( + ["git", "ls-remote", "--heads", url], + capture_output=True, + text=True, + check=True, + ).stdout + branches = {} + for line in output.splitlines(): + commit, ref = line.split("\t", 1) + branches[ref.removeprefix("refs/heads/")] = commit + return branches + + +def get_built_branch() -> str | None: + """Return the branch being built, if it can be determined. + + For pull request builds, Read the Docs only provides the pull request number, + so the branch is the one of the project repository whose head is the commit + being built. + + Returns: + Branch name, or None for tag builds and pull requests from forks + """ + version_type = os.environ.get("READTHEDOCS_VERSION_TYPE") + if version_type == "branch": + return os.environ["READTHEDOCS_GIT_IDENTIFIER"] + if version_type == "external": + commit = os.environ["READTHEDOCS_GIT_COMMIT_HASH"] + for branch, head in get_remote_branches(PROJECT_REPO).items(): + if head == commit: + return branch + return None + + +def select_branch(url: str, candidates: tuple[str, ...], built: str | None) -> str: + """Return the first candidate branch which exists in a remote repository. + + Args: + url: Remote repository URL + candidates: Candidate branch names, ``SAME_BRANCH`` standing for the + branch being built + built: Branch being built, or None if unknown + + Returns: + Selected branch name + """ + names = [built if name == SAME_BRANCH else name for name in candidates] + existing = get_remote_branches(url) + for name in names: + if name is not None and name in existing: + return name + raise SystemExit(f"None of the branches {names} exists in {url}") + + +def main() -> None: + """Install dependencies from GitHub for development builds.""" + version_type = os.environ.get("READTHEDOCS_VERSION_TYPE") + if version_type not in ("branch", "external"): + print(f"Version type {version_type!r}: dependencies installed from PyPI") + return + built = get_built_branch() + if built in RELEASE_BRANCHES: + print(f"Branch {built!r}: dependencies installed from PyPI") + return + print(f"Development build (branch: {built or 'unknown'})") + requirements = [] + for name, (url, candidates) in DEV_DEPENDENCIES.items(): + branch = select_branch(url, candidates, built) + print(f" {name}: {branch!r} branch") + requirements.append(f"{name} @ git+{url}@{branch}") + # Single pip call, so that dependencies between these packages are resolved + # against their development versions rather than against PyPI releases + subprocess.run( + [sys.executable, "-m", "pip", "install", "--no-cache-dir", *requirements], + check=True, + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/rtd_install_sigima.py b/scripts/rtd_install_sigima.py deleted file mode 100644 index 2f76be8..0000000 --- a/scripts/rtd_install_sigima.py +++ /dev/null @@ -1,106 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Install the development version of Sigima for Read the Docs builds. - -SigimaX is developed alongside Sigima: development branches and pull requests -may require a Sigima version which is not yet published on PyPI, so installing -``sigimax[doc]`` from PyPI alone would fail. - -This script mirrors the GitHub Actions test workflows: - -- tag builds and the ``main`` branch: nothing is done, Sigima is installed from - PyPI by the regular ``python.install`` step; -- other branches and pull requests: Sigima is installed from the GitHub branch - with the same name as the SigimaX branch if it exists, otherwise from - ``develop``. The regular ``python.install`` step then keeps this version as it - already satisfies SigimaX requirements. - -It is intended to be run as a ``build.jobs.pre_install`` step and only relies -on the standard library and ``git``. -""" - -from __future__ import annotations - -import os -import subprocess -import sys - -SIGIMA_REPO = "https://github.com/DataLab-Platform/Sigima.git" -SIGIMAX_REPO = "https://github.com/DataLab-Platform/SigimaX.git" -DEFAULT_BRANCH = "develop" -RELEASE_BRANCH = "main" - - -def get_remote_branches(url: str) -> dict[str, str]: - """Return the branches of a remote Git repository. - - Args: - url: Remote repository URL - - Returns: - Mapping of branch names to the commit hash of their head - """ - output = subprocess.run( - ["git", "ls-remote", "--heads", url], - capture_output=True, - text=True, - check=True, - ).stdout - branches = {} - for line in output.splitlines(): - commit, ref = line.split("\t", 1) - branches[ref.removeprefix("refs/heads/")] = commit - return branches - - -def get_sigimax_branch() -> str | None: - """Return the SigimaX branch being built, if it can be determined. - - For pull request builds, Read the Docs only provides the pull request number, - so the branch is the one of the SigimaX repository whose head is the commit - being built. - - Returns: - Branch name, or None for tag builds and pull requests from forks - """ - version_type = os.environ.get("READTHEDOCS_VERSION_TYPE") - if version_type == "branch": - return os.environ["READTHEDOCS_GIT_IDENTIFIER"] - if version_type == "external": - commit = os.environ["READTHEDOCS_GIT_COMMIT_HASH"] - for branch, head in get_remote_branches(SIGIMAX_REPO).items(): - if head == commit: - return branch - return None - - -def main() -> None: - """Install Sigima from GitHub for development builds.""" - version_type = os.environ.get("READTHEDOCS_VERSION_TYPE") - if version_type not in ("branch", "external"): - print(f"Version type {version_type!r}: Sigima will be installed from PyPI") - return - sigimax_branch = get_sigimax_branch() - if sigimax_branch == RELEASE_BRANCH: - print(f"Branch {RELEASE_BRANCH!r}: Sigima will be installed from PyPI") - return - if sigimax_branch in get_remote_branches(SIGIMA_REPO): - sigima_branch = sigimax_branch - else: - sigima_branch = DEFAULT_BRANCH - print(f"SigimaX branch: {sigimax_branch!r} -> Sigima branch: {sigima_branch!r}") - subprocess.run( - [ - sys.executable, - "-m", - "pip", - "install", - "--no-cache-dir", - f"sigima @ git+{SIGIMA_REPO}@{sigima_branch}", - ], - check=True, - ) - - -if __name__ == "__main__": - main()