Skip to content

Commit 1010c99

Browse files
authored
Add option to use temporary venv to Pip (#20)
Until now the `Pip` build environment only supported a pre-existing python venv which would be the same for all versions of the docs. This is certainly not intended. This is now fixed by introducing the `temporary` keyword. When specified a new python venv will be created for each version in a subdirectory of the temporary build directory. * README.md : Add example. * sphinx_polyversion/pyvenv.py (Pip): Implement new `temporary` keyword. * tests/test_pyvenv.py : Test new behaviour.
1 parent e50fc2c commit 1010c99

File tree

3 files changed

+142
-0
lines changed

3 files changed

+142
-0
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,10 @@ ENVIRONMENT = {
186186
None: Poetry.factory(args="--sync".split()), # first version
187187
"v1.5.7": Poetry.factory(args="--only sphinx --sync".split()),
188188
"v1.8.2": Poetry.factory(args="--only dev --sync".split(), env={"MY_VAR": "value"}),
189+
# use a pre-existing environment at the location ./.venv
189190
"v3.0.0": Pip.factory(venv=Path(".venv"), args="-e . -r requirements.txt".split()),
191+
# dynamically create an environment in the temporary build directory
192+
"v4.*.*": Pip.factory(venv=Path(".venv"), args="-e . -r requirements.txt".split(), creator=VenvWrapper(), temporary=True),
190193
}
191194

192195
# ...

sphinx_polyversion/pyvenv.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,10 @@ class Pip(VirtualPythonEnvironment):
412412
The cmd arguments to pass to `pip install`.
413413
creator : Callable[[Path], Any] | None, optional
414414
A callable for creating the venv, by default None
415+
temporary : bool, optional
416+
A flag to specify whether the environment should be created in the
417+
temporary directory, by default False. If this is True, `creator`
418+
must not be None and `venv` will be treated relative to `path`.
415419
env : dict[str, str], optional
416420
A dictionary of environment variables which are overridden in the
417421
virtual environment, by default None
@@ -426,6 +430,7 @@ def __init__(
426430
*,
427431
args: Iterable[str],
428432
creator: Callable[[Path], Any] | None = None,
433+
temporary: bool = False,
429434
env: dict[str, str] | None = None,
430435
):
431436
"""
@@ -443,11 +448,29 @@ def __init__(
443448
The cmd arguments to pass to `pip install`.
444449
creator : Callable[[Path], Any], optional
445450
A callable for creating the venv, by default None
451+
temporary : bool, optional
452+
A flag to specify whether the environment should be created in the
453+
temporary directory, by default False. If this is True, `creator`
454+
must not be None and `venv` will be treated relative to `path`.
446455
env : dict[str, str], optional
447456
A dictionary of environment variables which are overridden in the
448457
virtual environment, by default None
449458
459+
Raises
460+
------
461+
ValueError
462+
If `temporary` is enabled but no valid creator is provided.
463+
450464
"""
465+
if temporary:
466+
if creator is None:
467+
raise ValueError(
468+
"Cannot create temporary virtual environment when creator is None.\n"
469+
"Please set creator to enable temporary virtual environments, or "
470+
"set temporary to False to use a pre-existing local environment "
471+
f"at path '{venv}'."
472+
)
473+
venv = path / venv
451474
super().__init__(path, name, venv, creator=creator, env=env)
452475
self.args = args
453476

tests/test_pyvenv.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,122 @@ async def test_run_with_env_variables(self, tmp_path: Path):
127127
class TestPip:
128128
"""Test the `Pip` class."""
129129

130+
@pytest.mark.asyncio
131+
@pytest.mark.asyncio
132+
async def test_creation_with_venv(self, tmp_path: Path):
133+
"""Test the `create_venv` method with a `VenvWrapper`."""
134+
location = tmp_path / "venv"
135+
env = Pip(
136+
tmp_path,
137+
"main",
138+
location,
139+
args=["tomli"],
140+
creator=VenvWrapper(),
141+
temporary=False,
142+
)
143+
144+
await env.create_venv()
145+
assert (location / "bin" / "python").exists()
146+
147+
@pytest.mark.asyncio
148+
async def test_creation_without_creator(self, tmp_path: Path):
149+
"""Test the `create_venv` method without any creator."""
150+
location = tmp_path / "venv"
151+
env = Pip(tmp_path, "main", location, args=["tomli"], temporary=False)
152+
153+
await env.create_venv()
154+
assert not (location / "bin" / "python").exists()
155+
156+
@pytest.mark.asyncio
157+
async def test_run_without_creator(self, tmp_path: Path):
158+
"""Test running a command in an existing venv."""
159+
location = tmp_path / "venv"
160+
161+
# create env
162+
await VenvWrapper([])(location)
163+
164+
async with Pip(
165+
tmp_path, "main", location, args=["tomli"], temporary=False
166+
) as env:
167+
out, err, rc = await env.run(
168+
"python",
169+
"-c",
170+
"import sys; print(sys.prefix)",
171+
stdout=asyncio.subprocess.PIPE,
172+
)
173+
assert rc == 0
174+
assert str(location) == out.strip()
175+
176+
@pytest.mark.asyncio
177+
async def test_run_with_creator(self, tmp_path: Path):
178+
"""Test running a command in a new venv."""
179+
location = tmp_path / "venv"
180+
181+
async with Pip(
182+
tmp_path,
183+
"main",
184+
location,
185+
args=["tomli"],
186+
creator=VenvWrapper(),
187+
temporary=False,
188+
) as env:
189+
out, err, rc = await env.run(
190+
"python",
191+
"-c",
192+
"import sys; print(sys.prefix)",
193+
stdout=asyncio.subprocess.PIPE,
194+
)
195+
assert rc == 0
196+
assert str(location) == out.strip()
197+
198+
@pytest.mark.asyncio
199+
async def test_creation_with_venv_temporary(self, tmp_path: Path):
200+
"""Test the `create_venv` method with a `VenvWrapper`."""
201+
location = "tmpvenv"
202+
env = Pip(
203+
tmp_path,
204+
"main",
205+
location,
206+
args=["tomli"],
207+
creator=VenvWrapper(),
208+
temporary=True,
209+
)
210+
211+
await env.create_venv()
212+
assert (tmp_path / location / "bin" / "python").exists()
213+
214+
@pytest.mark.asyncio
215+
async def test_creation_without_creator_temporary(self, tmp_path: Path):
216+
"""Test the `create_venv` method without any creator."""
217+
location = "tmpvenv"
218+
with pytest.raises(
219+
ValueError,
220+
match="Cannot create temporary virtual environment when creator is None",
221+
):
222+
Pip(tmp_path, "main", location, args=["tomli"], temporary=True)
223+
224+
@pytest.mark.asyncio
225+
async def test_run_with_creator_temporary(self, tmp_path: Path):
226+
"""Test running a command in a new venv."""
227+
location = "tmpvenv"
228+
229+
async with Pip(
230+
tmp_path,
231+
"main",
232+
location,
233+
args=["tomli"],
234+
creator=VenvWrapper(),
235+
temporary=True,
236+
) as env:
237+
out, err, rc = await env.run(
238+
"python",
239+
"-c",
240+
"import sys; print(sys.prefix)",
241+
stdout=asyncio.subprocess.PIPE,
242+
)
243+
assert rc == 0
244+
assert str(tmp_path / location) == out.strip()
245+
130246
@pytest.mark.asyncio
131247
async def test_install_into_existing_venv(self, tmp_path: Path):
132248
"""Test installing a package into an existing venv."""

0 commit comments

Comments
 (0)