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
26from pathlib import Path
37
48from sphinx_polyversion .api import apply_overrides
9+ from sphinx_polyversion .builder import BuildError
510from 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
813from 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`
2339SPHINX_ARGS = "-a -v"
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):
6790root = 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
76235ENVIRONMENT = {
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 )
84249DefaultDriver (
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