Skip to content

Commit e12aeef

Browse files
committed
fix configuration for using setup-scm
1 parent ac7609b commit e12aeef

File tree

1 file changed

+183
-23
lines changed

1 file changed

+183
-23
lines changed

docsrc/poly.py

Lines changed: 183 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,39 @@
1-
from functools import partial
1+
import asyncio
2+
from asyncio.subprocess import PIPE
3+
from logging import getLogger
4+
from subprocess import CalledProcessError
5+
import os
26
from pathlib import Path
37

48
from sphinx_polyversion.api import apply_overrides
9+
from sphinx_polyversion.builder import BuildError
510
from sphinx_polyversion.driver import DefaultDriver
6-
from sphinx_polyversion.git import Git, file_predicate, refs_by_type, closest_tag
7-
from sphinx_polyversion.pyvenv import Pip
11+
from sphinx_polyversion.git import Git, file_predicate, refs_by_type
12+
from sphinx_polyversion.pyvenv import VirtualPythonEnvironment
813
from sphinx_polyversion.sphinx import SphinxBuilder, Placeholder
14+
from sphinx_polyversion.utils import to_thread
15+
16+
from typing import (
17+
Any,
18+
Callable,
19+
Dict,
20+
Iterable,
21+
cast,
22+
)
23+
24+
logger = getLogger(__name__)
925

1026
#: CodeRegex matching the branches to build docs for
11-
BRANCH_REGEX = r"(doc-polyversion)"
27+
BRANCH_REGEX = r"^(master|dev)$"
1228

1329
#: Regex matching the tags to build docs for
14-
TAG_REGEX = r"v1.1.6.1"
30+
TAG_REGEX = r"^v[\.0-9]*$"
1531

1632
#: Output dir relative to project root
17-
OUTPUT_DIR = "_polybuild"
33+
OUTPUT_DIR = "docsrc/_build_polyversion"
1834

1935
#: Source directory
20-
SOURCE_DIR = "docsrc/"
36+
SOURCE_DIR = "docsrc"
2137

2238
#: Arguments to pass to `sphinx-build`
2339
SPHINX_ARGS = "-a -v"
@@ -33,7 +49,13 @@
3349
"sphinx-polyversion==1.0.0",
3450
]
3551

36-
BACKEND_DEPS = [
52+
#: Extra dependencies to iinstall for version 1
53+
V1_BACKEND_DEPS = [
54+
"tf-keras",
55+
]
56+
57+
#: Extra dependencies to install for version 2
58+
V2_BACKEND_DEPS = [
3759
"jax",
3860
"torch",
3961
"tensorflow",
@@ -45,10 +67,11 @@ def data(driver, rev, env):
4567
revisions = driver.targets
4668
branches, tags = refs_by_type(revisions)
4769
latest = max(tags or branches)
70+
# sort tags and branches by date, newest first
4871
return {
4972
"current": rev,
50-
"tags": tags,
51-
"branches": branches,
73+
"tags": sorted(tags, reverse=True),
74+
"branches": sorted(branches, reverse=True),
5275
"revisions": revisions,
5376
"latest": latest,
5477
}
@@ -67,36 +90,173 @@ def root_data(driver):
6790
root = Git.root(Path(__file__).parent)
6891

6992

70-
async def selector(f, a, b):
71-
return a.name
93+
async def selector(rev, keys):
94+
"""Select configuration based on revision"""
95+
# map all v1 revisions to one configuration
96+
if rev.name.startswith("v1."):
97+
return "v1"
98+
elif rev.name in ["master"]:
99+
# special configuration for v1 master branch
100+
return rev.name
101+
# common config for everything else
102+
return None
103+
104+
105+
# adapted from Pip
106+
class DynamicPip(VirtualPythonEnvironment):
107+
"""
108+
Build Environment for using a venv and installing deps with pip.
109+
The name is added to the path to allow distinct virtual environments
110+
for each revision.
111+
112+
Use this to run the build commands in a python virtual environment
113+
and install dependencies with pip into the venv before the build.
114+
115+
116+
Parameters
117+
----------
118+
path : Path
119+
The path of the current revision.
120+
name : str
121+
The name of the environment (usually the name of the revision).
122+
venv : Path
123+
The path of the python venv.
124+
args : Iterable[str]
125+
The cmd arguments to pass to `pip install`.
126+
creator : Callable[[Path], Any] | None, optional
127+
A callable for creating the venv, by default None
128+
env_vars: Dict[str, str], optional, default []
129+
A dictionary of environment variables passed to `pip install`
130+
"""
131+
132+
def __init__(
133+
self,
134+
path: Path,
135+
name: str,
136+
venv: str | Path,
137+
*,
138+
args: Iterable[str],
139+
creator: Callable[[Path], Any] | None = None,
140+
env_vars: Dict[str, str] = {},
141+
):
142+
"""
143+
Build Environment for using a venv and pip.
144+
145+
Parameters
146+
----------
147+
path : Path
148+
The path of the current revision.
149+
name : str
150+
The name of the environment (usually the name of the revision).
151+
venv : Path
152+
The path of the python venv.
153+
args : Iterable[str]
154+
The cmd arguments to pass to `pip install`.
155+
creator : Callable[[Path], Any], optional
156+
A callable for creating the venv, by default None
157+
158+
"""
159+
logger.info("Setting dynamic venv name: " + str(Path(venv) / name))
160+
self.env_vars = env_vars
161+
self.args = args.copy()
162+
if name.startswith("v1."):
163+
# required, as setup-scm cannot determine the version without
164+
# the .git directory, which is not copied for the build.
165+
logger.info("Setting setuptools version 'SETUPTOOLS_SCM_PRETEND_VERSION_FOR_BAYESFLOW=" + name[1:] + "'")
166+
self.env_vars["SETUPTOOLS_SCM_PRETEND_VERSION_FOR_BAYESFLOW"] = name[1:]
167+
168+
super().__init__(path, name, Path(venv) / name, creator=creator)
169+
170+
async def __aenter__(self):
171+
"""
172+
Set the venv up.
173+
174+
Raises
175+
------
176+
BuildError
177+
Running `pip install` failed.
178+
"""
179+
await super().__aenter__()
180+
181+
logger.info("Running `pip install`...")
182+
183+
cmd: list[str] = ["pip", "install"]
184+
cmd += self.args
185+
186+
env = self.activate(os.environ.copy())
187+
# add environment variables to environment
188+
for key, value in self.env_vars.items():
189+
env[key] = value
190+
191+
process = await asyncio.create_subprocess_exec(
192+
*cmd,
193+
cwd=self.path,
194+
env=env,
195+
stdout=PIPE,
196+
stderr=PIPE,
197+
)
198+
out, err = await process.communicate()
199+
out = out.decode(errors="ignore")
200+
err = err.decode(errors="ignore")
201+
202+
self.logger.debug("Installation output:\n %s", out)
203+
if process.returncode != 0:
204+
raise BuildError from CalledProcessError(cast(int, process.returncode), " ".join(cmd), out, err)
205+
return self
206+
207+
208+
# for some reason, VenvWrapper did not work for me (probably something specific
209+
# to my system, so we use subprocess.call directly to use the system utilities
210+
# to create the environment.
211+
class LocalVenvCreator:
212+
def _create(self, venv_path):
213+
if not os.path.exists(venv_path):
214+
import subprocess
215+
216+
print(f"Creating venv '{venv_path}'...")
217+
subprocess.call(f"python -m venv {venv_path}", shell=True)
218+
219+
async def __call__(self, path: Path) -> None:
220+
await to_thread(self._create, path)
72221

73222

74223
# Setup environments for the different versions
224+
src = Path(SOURCE_DIR)
225+
vcs = Git(
226+
branch_regex=BRANCH_REGEX,
227+
tag_regex=TAG_REGEX,
228+
buffer_size=1 * 10**9, # 1 GB
229+
predicate=file_predicate([src]), # exclude refs without source dir
230+
)
231+
232+
233+
creator = LocalVenvCreator()
75234

76235
ENVIRONMENT = {
77-
None: Pip.factory(venv=Path(".venv"), args=["-vv", "bayesflow==1.1.6"] + SPHINX_DEPS),
78-
"doc-polyversion": Pip.factory(venv=Path(".venv/dev"), args=["-vv", "-e", "."] + SPHINX_DEPS + BACKEND_DEPS),
79-
"v1.1.6": Pip.factory(venv=Path(".venv/v1.1.6"), args=["-vv", "bayesflow==1.1.6"] + SPHINX_DEPS),
236+
# configuration for v2 and dev
237+
None: DynamicPip.factory(venv=Path(".venv"), args=["-e", "."] + SPHINX_DEPS + V2_BACKEND_DEPS, creator=creator),
238+
# configuration for v1 and master (remove master here and in selector when it moves to v2)
239+
"v1": DynamicPip.factory(venv=Path(".venv"), args=["-e", "."] + SPHINX_DEPS + V1_BACKEND_DEPS, creator=creator),
240+
"master": DynamicPip.factory(
241+
venv=Path(".venv"),
242+
args=["-vv", "-e", "."] + V1_BACKEND_DEPS + SPHINX_DEPS,
243+
creator=creator,
244+
env_vars={"SETUPTOOLS_SCM_PRETEND_VERSION_FOR_BAYESFLOW": "1.1.6dev"},
245+
),
80246
}
81247

82248
# Setup driver and run it
83-
src = Path(SOURCE_DIR)
84249
DefaultDriver(
85250
root,
86251
OUTPUT_DIR,
87-
vcs=Git(
88-
branch_regex=BRANCH_REGEX,
89-
tag_regex=TAG_REGEX,
90-
buffer_size=1 * 10**9, # 1 GB
91-
predicate=file_predicate([src]), # exclude refs without source dir
92-
),
252+
vcs=vcs,
93253
builder=SphinxBuilder(
94254
src / "source",
95255
args=SPHINX_ARGS.split(),
96256
pre_cmd=["python", root / src / "pre-build.py", Placeholder.SOURCE_DIR],
97257
),
98258
env=ENVIRONMENT,
99-
selector=partial(selector, partial(closest_tag, root)),
259+
selector=selector,
100260
template_dir=root / src / "polyversion/templates",
101261
static_dir=root / src / "polyversion/static",
102262
data_factory=data,

0 commit comments

Comments
 (0)