Skip to content

Commit bf1f636

Browse files
Merge pull request #73 from PlotPyStack/ci/rtd-dev-deps
Install development dependencies for Read the Docs dev builds
2 parents 43e7210 + fe7f9d9 commit bf1f636

2 files changed

Lines changed: 138 additions & 0 deletions

File tree

‎.readthedocs.yaml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ build:
1010
post_create_environment:
1111
- pip install PyQt5 setuptools numpy Cython
1212
- python setup.py build_ext --inplace
13+
pre_install:
14+
# Development builds may require dependency versions not yet released on PyPI
15+
- python scripts/rtd_install_dev_deps.py
1316
sphinx:
1417
configuration: doc/conf.py
1518
formats:

‎scripts/rtd_install_dev_deps.py‎

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Install development versions of dependencies for Read the Docs builds.
4+
5+
This project is developed alongside other PlotPyStack and DataLab Platform
6+
projects: development branches and pull requests may require dependency versions
7+
which are not yet published on PyPI, so installing the documentation requirements
8+
from PyPI alone would fail.
9+
10+
This script mirrors the GitHub Actions test workflows:
11+
12+
- tag builds and release branches: nothing is done, dependencies are installed
13+
from PyPI by the regular ``python.install`` step;
14+
- other branches and pull requests: the dependencies listed in
15+
``DEV_DEPENDENCIES`` are installed from GitHub. The regular ``python.install``
16+
step then keeps these versions as they already satisfy the project
17+
requirements.
18+
19+
It is intended to be run as a ``build.jobs.pre_install`` step and only relies on
20+
the standard library and ``git``.
21+
22+
The same script is used by several projects: only the project-specific settings
23+
below differ between copies.
24+
"""
25+
26+
from __future__ import annotations
27+
28+
import os
29+
import subprocess
30+
import sys
31+
32+
# ---- Project-specific settings ----------------------------------------------
33+
PROJECT_REPO = "https://github.com/PlotPyStack/PlotPy.git"
34+
#: Placeholder for the name of the branch being built
35+
SAME_BRANCH = "{branch}"
36+
#: Dependencies to install from GitHub for development builds:
37+
#: package name -> (repository URL, candidate branches, first existing one is used)
38+
DEV_DEPENDENCIES = {
39+
"PythonQwt": ("https://github.com/PlotPyStack/PythonQwt.git", ("master",)),
40+
"guidata": ("https://github.com/PlotPyStack/guidata.git", ("develop",)),
41+
}
42+
# -----------------------------------------------------------------------------
43+
44+
RELEASE_BRANCHES = ("main", "master", "release")
45+
46+
47+
def get_remote_branches(url: str) -> dict[str, str]:
48+
"""Return the branches of a remote Git repository.
49+
50+
Args:
51+
url: Remote repository URL
52+
53+
Returns:
54+
Mapping of branch names to the commit hash of their head
55+
"""
56+
output = subprocess.run(
57+
["git", "ls-remote", "--heads", url],
58+
capture_output=True,
59+
text=True,
60+
check=True,
61+
).stdout
62+
branches = {}
63+
for line in output.splitlines():
64+
commit, ref = line.split("\t", 1)
65+
branches[ref.removeprefix("refs/heads/")] = commit
66+
return branches
67+
68+
69+
def get_built_branch() -> str | None:
70+
"""Return the branch being built, if it can be determined.
71+
72+
For pull request builds, Read the Docs only provides the pull request number,
73+
so the branch is the one of the project repository whose head is the commit
74+
being built.
75+
76+
Returns:
77+
Branch name, or None for tag builds and pull requests from forks
78+
"""
79+
version_type = os.environ.get("READTHEDOCS_VERSION_TYPE")
80+
if version_type == "branch":
81+
return os.environ["READTHEDOCS_GIT_IDENTIFIER"]
82+
if version_type == "external":
83+
commit = os.environ["READTHEDOCS_GIT_COMMIT_HASH"]
84+
for branch, head in get_remote_branches(PROJECT_REPO).items():
85+
if head == commit:
86+
return branch
87+
return None
88+
89+
90+
def select_branch(url: str, candidates: tuple[str, ...], built: str | None) -> str:
91+
"""Return the first candidate branch which exists in a remote repository.
92+
93+
Args:
94+
url: Remote repository URL
95+
candidates: Candidate branch names, ``SAME_BRANCH`` standing for the
96+
branch being built
97+
built: Branch being built, or None if unknown
98+
99+
Returns:
100+
Selected branch name
101+
"""
102+
names = [built if name == SAME_BRANCH else name for name in candidates]
103+
existing = get_remote_branches(url)
104+
for name in names:
105+
if name is not None and name in existing:
106+
return name
107+
raise SystemExit(f"None of the branches {names} exists in {url}")
108+
109+
110+
def main() -> None:
111+
"""Install dependencies from GitHub for development builds."""
112+
version_type = os.environ.get("READTHEDOCS_VERSION_TYPE")
113+
if version_type not in ("branch", "external"):
114+
print(f"Version type {version_type!r}: dependencies installed from PyPI")
115+
return
116+
built = get_built_branch()
117+
if built in RELEASE_BRANCHES:
118+
print(f"Branch {built!r}: dependencies installed from PyPI")
119+
return
120+
print(f"Development build (branch: {built or 'unknown'})")
121+
requirements = []
122+
for name, (url, candidates) in DEV_DEPENDENCIES.items():
123+
branch = select_branch(url, candidates, built)
124+
print(f" {name}: {branch!r} branch")
125+
requirements.append(f"{name} @ git+{url}@{branch}")
126+
# Single pip call, so that dependencies between these packages are resolved
127+
# against their development versions rather than against PyPI releases
128+
subprocess.run(
129+
[sys.executable, "-m", "pip", "install", "--no-cache-dir", *requirements],
130+
check=True,
131+
)
132+
133+
134+
if __name__ == "__main__":
135+
main()

0 commit comments

Comments
 (0)