Skip to content

Commit 5a097cd

Browse files
samypr100henryiii
andauthored
feat: support for reuse_venv option (#730)
* feat: support for reuse_venv option * typing: adding Literal for reuse_venv choices * chore: ruff-format * chore: codespell * chore: move `generate_noxfile_options` fixture to `conftest.py` * chore: ruff-format * chore: fix lints Signed-off-by: Henry Schreiner <[email protected]> * docs: clarify reuse_existing_venv function decision matrix * chore: remove ``# pragma: no cover` due to recent main branch changes * docs: call out that --reuse-existing-virtualenvs/--no-reuse-existing-virtualenvs is alias to --reuse-venv=yes|no in usage.rst * docs: small update to config.rst --------- Signed-off-by: Henry Schreiner <[email protected]> Co-authored-by: Henry Schreiner <[email protected]>
1 parent 55e09cd commit 5a097cd

File tree

9 files changed

+311
-33
lines changed

9 files changed

+311
-33
lines changed

docs/config.rst

+3-2
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ Use of :func:`session.install()` is deprecated without a virtualenv since it mod
157157
def tests(session):
158158
session.run("pip", "install", "nox")
159159
160-
You can also specify that the virtualenv should *always* be reused instead of recreated every time:
160+
You can also specify that the virtualenv should *always* be reused instead of recreated every time unless ``--reuse-venv=never``:
161161

162162
.. code-block:: python
163163
@@ -432,7 +432,8 @@ The following options can be specified in the Noxfile:
432432
* ``nox.options.tags`` is equivalent to specifying :ref:`-t or --tags <opt-sessions-pythons-and-keywords>`.
433433
* ``nox.options.default_venv_backend`` is equivalent to specifying :ref:`-db or --default-venv-backend <opt-default-venv-backend>`.
434434
* ``nox.options.force_venv_backend`` is equivalent to specifying :ref:`-fb or --force-venv-backend <opt-force-venv-backend>`.
435-
* ``nox.options.reuse_existing_virtualenvs`` is equivalent to specifying :ref:`--reuse-existing-virtualenvs <opt-reuse-existing-virtualenvs>`. You can force this off by specifying ``--no-reuse-existing-virtualenvs`` during invocation.
435+
* ``nox.options.reuse_venv`` is equivalent to specifying :ref:`--reuse-venv <opt-reuse-venv>`. Preferred over using ``nox.options.reuse_existing_virtualenvs``.
436+
* ``nox.options.reuse_existing_virtualenvs`` is equivalent to specifying :ref:`--reuse-existing-virtualenvs <opt-reuse-existing-virtualenvs>`. You can force this off by specifying ``--no-reuse-existing-virtualenvs`` during invocation. Alias of ``nox.options.reuse_venv=yes|no``.
436437
* ``nox.options.stop_on_first_error`` is equivalent to specifying :ref:`--stop-on-first-error <opt-stop-on-first-error>`. You can force this off by specifying ``--no-stop-on-first-error`` during invocation.
437438
* ``nox.options.error_on_missing_interpreters`` is equivalent to specifying :ref:`--error-on-missing-interpreters <opt-error-on-missing-interpreters>`. You can force this off by specifying ``--no-error-on-missing-interpreters`` during invocation.
438439
* ``nox.options.error_on_external_run`` is equivalent to specifying :ref:`--error-on-external-run <opt-error-on-external-run>`. You can force this off by specifying ``--no-error-on-external-run`` during invocation.

docs/usage.rst

+21-4
Original file line numberDiff line numberDiff line change
@@ -179,34 +179,51 @@ Finally note that the ``--no-venv`` flag is a shortcut for ``--force-venv-backen
179179
nox --no-venv
180180
181181
.. _opt-reuse-existing-virtualenvs:
182+
.. _opt-reuse-venv:
182183

183184
Re-using virtualenvs
184185
--------------------
185186

186-
By default, Nox deletes and recreates virtualenvs every time it is run. This is usually fine for most projects and continuous integration environments as `pip's caching <https://pip.pypa.io/en/stable/cli/pip_install/#caching>`_ makes re-install rather quick. However, there are some situations where it is advantageous to reuse the virtualenvs between runs. Use ``-r`` or ``--reuse-existing-virtualenvs``:
187+
By default, Nox deletes and recreates virtualenvs every time it is run. This is
188+
usually fine for most projects and continuous integration environments as
189+
`pip's caching <https://pip.pypa.io/en/stable/cli/pip_install/#caching>`_ makes
190+
re-install rather quick. However, there are some situations where it is
191+
advantageous to reuse the virtualenvs between runs. Use ``-r`` or
192+
``--reuse-existing-virtualenvs`` or for fine-grained control use
193+
``--reuse-venv=yes|no|always|never``:
187194

188195
.. code-block:: console
189196
190197
nox -r
191198
nox --reuse-existing-virtualenvs
192-
199+
nox --reuse-venv=yes # preferred
193200
194201
If the Noxfile sets ``nox.options.reuse_existing_virtualenvs``, you can override the Noxfile setting from the command line by using ``--no-reuse-existing-virtualenvs``.
202+
Similarly you can override ``nox.options.reuse_venvs`` from the Noxfile via the command line by using ``--reuse-venv=yes|no|always|never``.
203+
204+
.. note::
195205

196-
Additionally, you can skip the re-installation of packages when a virtualenv is reused. Use ``-R`` or ``--reuse-existing-virtualenvs --no-install``:
206+
``--reuse-existing-virtualenvs`` is a alias for ``--reuse-venv=yes`` and ``--no-reuse-existing-virtualenvs`` is an alias for ``--reuse-venv=no``.
207+
208+
Additionally, you can skip the re-installation of packages when a virtualenv is reused.
209+
Use ``-R`` or ``--reuse-existing-virtualenvs --no-install`` or ``--reuse-venv=yes --no-install``:
197210

198211
.. code-block:: console
199212
200213
nox -R
201214
nox --reuse-existing-virtualenvs --no-install
215+
nox --reuse-venv=yes --no-install
202216
203217
The ``--no-install`` option causes the following session methods to return early:
204218

205219
- :func:`session.install <nox.sessions.Session.install>`
206220
- :func:`session.conda_install <nox.sessions.Session.conda_install>`
207221
- :func:`session.run_install <nox.sessions.Session.run_install>`
208222

209-
This option has no effect if the virtualenv is not being reused.
223+
The ``never`` and ``always`` options in ``--reuse-venv`` gives you more fine-grained control
224+
as it ignores when a ``@nox.session`` has ``reuse_venv=True|False`` defined.
225+
226+
These options have no effect if the virtualenv is not being reused.
210227

211228
.. _opt-running-extra-pythons:
212229

nox/_options.py

+56-2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@
2727
from nox import _option_set
2828
from nox.tasks import discover_manifest, filter_manifest, load_nox_module
2929

30+
if sys.version_info < (3, 8):
31+
from typing_extensions import Literal
32+
else:
33+
from typing import Literal
34+
35+
ReuseVenvType = Literal["no", "yes", "never", "always"]
36+
3037
"""All of Nox's configuration options."""
3138

3239
options = _option_set.OptionSet(
@@ -146,6 +153,30 @@ def _envdir_merge_func(
146153
return command_args.envdir or noxfile_args.envdir or ".nox"
147154

148155

156+
def _reuse_venv_merge_func(
157+
command_args: argparse.Namespace, noxfile_args: argparse.Namespace
158+
) -> ReuseVenvType:
159+
"""Merge reuse_venv from command args and Noxfile while maintaining
160+
backwards compatibility with reuse_existing_virtualenvs. Default is "no".
161+
162+
Args:
163+
command_args (_option_set.Namespace): The options specified on the
164+
command-line.
165+
noxfile_Args (_option_set.Namespace): The options specified in the
166+
Noxfile.
167+
"""
168+
# back-compat scenario with no_reuse_existing_virtualenvs/reuse_existing_virtualenvs
169+
if command_args.no_reuse_existing_virtualenvs:
170+
return "no"
171+
if (
172+
command_args.reuse_existing_virtualenvs
173+
or noxfile_args.reuse_existing_virtualenvs
174+
):
175+
return "yes"
176+
# regular option behavior
177+
return command_args.reuse_venv or noxfile_args.reuse_venv or "no"
178+
179+
149180
def default_env_var_list_factory(env_var: str) -> Callable[[], list[str] | None]:
150181
"""Looks at the env var to set the default value for a list of env vars.
151182
@@ -197,12 +228,22 @@ def _force_pythons_finalizer(
197228

198229

199230
def _R_finalizer(value: bool, args: argparse.Namespace) -> bool:
200-
"""Propagate -R to --reuse-existing-virtualenvs and --no-install."""
231+
"""Propagate -R to --reuse-existing-virtualenvs and --no-install and --reuse-venv=yes."""
201232
if value:
233+
args.reuse_venv = "yes"
202234
args.reuse_existing_virtualenvs = args.no_install = value
203235
return value
204236

205237

238+
def _reuse_existing_virtualenvs_finalizer(
239+
value: bool, args: argparse.Namespace
240+
) -> bool:
241+
"""Propagate --reuse-existing-virtualenvs to --reuse-venv=yes."""
242+
if value:
243+
args.reuse_venv = "yes"
244+
return value
245+
246+
206247
def _posargs_finalizer(
207248
value: Sequence[Any], args: argparse.Namespace
208249
) -> Sequence[Any] | list[Any]:
@@ -412,6 +453,18 @@ def _tag_completer(
412453
" creating a venv. This is an alias for '--force-venv-backend none'."
413454
),
414455
),
456+
_option_set.Option(
457+
"reuse_venv",
458+
"--reuse-venv",
459+
group=options.groups["environment"],
460+
noxfile=True,
461+
merge_func=_reuse_venv_merge_func,
462+
help=(
463+
"Controls existing virtualenvs recreation. This is ``'no'`` by"
464+
" default, but any of ``('yes', 'no', 'always', 'never')`` are accepted."
465+
),
466+
choices=["yes", "no", "always", "never"],
467+
),
415468
*_option_set.make_flag_pair(
416469
"reuse_existing_virtualenvs",
417470
("-r", "--reuse-existing-virtualenvs"),
@@ -420,7 +473,8 @@ def _tag_completer(
420473
"--no-reuse-existing-virtualenvs",
421474
),
422475
group=options.groups["environment"],
423-
help="Reuse existing virtualenvs instead of recreating them.",
476+
help="This is an alias for '--reuse-venv=yes|no'.",
477+
finalizer_func=_reuse_existing_virtualenvs_finalizer,
424478
),
425479
_option_set.Option(
426480
"R",

nox/sessions.py

+46-3
Original file line numberDiff line numberDiff line change
@@ -767,9 +767,7 @@ def _create_venv(self) -> None:
767767
self.venv = PassthroughEnv()
768768
return
769769

770-
reuse_existing = (
771-
self.func.reuse_venv or self.global_config.reuse_existing_virtualenvs
772-
)
770+
reuse_existing = self.reuse_existing_venv()
773771

774772
if backend is None or backend in {"virtualenv", "venv", "uv"}:
775773
self.venv = VirtualEnv(
@@ -795,6 +793,51 @@ def _create_venv(self) -> None:
795793

796794
self.venv.create()
797795

796+
def reuse_existing_venv(self) -> bool:
797+
"""
798+
Determines whether to reuse an existing virtual environment.
799+
800+
The decision matrix is as follows:
801+
802+
+--------------------------+-----------------+-------------+
803+
| global_config.reuse_venv | func.reuse_venv | Reuse venv? |
804+
+==========================+=================+=============+
805+
| "always" | N/A | Yes |
806+
+--------------------------+-----------------+-------------+
807+
| "never" | N/A | No |
808+
+--------------------------+-----------------+-------------+
809+
| "yes" | True|None | Yes |
810+
+--------------------------+-----------------+-------------+
811+
| "yes" | False | No |
812+
+--------------------------+-----------------+-------------+
813+
| "no" | True | Yes |
814+
+--------------------------+-----------------+-------------+
815+
| "no" | False|None | No |
816+
+--------------------------+-----------------+-------------+
817+
818+
Summary
819+
~~~~~~~
820+
- "always" forces reuse regardless of `func.reuse_venv`.
821+
- "never" forces recreation regardless of `func.reuse_venv`.
822+
- "yes" and "no" respect `func.reuse_venv` being ``False`` or ``True`` respectively.
823+
824+
Returns:
825+
bool: True if the existing virtual environment should be reused, False otherwise.
826+
"""
827+
828+
return any(
829+
(
830+
# "always" forces reuse regardless of func.reuse_venv
831+
self.global_config.reuse_venv == "always",
832+
# Respect func.reuse_venv when it's explicitly True, unless global_config is "never"
833+
self.func.reuse_venv is True
834+
and self.global_config.reuse_venv != "never",
835+
# Delegate to reuse ("yes") when func.reuse_venv is not explicitly False
836+
self.func.reuse_venv is not False
837+
and self.global_config.reuse_venv == "yes",
838+
)
839+
)
840+
798841
def execute(self) -> Result:
799842
logger.warning(f"Running session {self.friendly_name}")
800843

tests/conftest.py

+46
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,23 @@
1+
# Copyright 2023 Alethea Katherine Flowers
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
from __future__ import annotations
15+
16+
import re
17+
from pathlib import Path
18+
from string import Template
19+
from typing import Callable
20+
121
import pytest
222

323

@@ -6,3 +26,29 @@ def reset_color_envvars(monkeypatch):
626
"""Remove color-related envvars to fix test output"""
727
monkeypatch.delenv("FORCE_COLOR", raising=False)
828
monkeypatch.delenv("NO_COLOR", raising=False)
29+
30+
31+
RESOURCES = Path(__file__).parent.joinpath("resources")
32+
33+
34+
@pytest.fixture
35+
def generate_noxfile_options(tmp_path: Path) -> Callable[..., str]:
36+
"""Generate noxfile.py with test and templated options.
37+
38+
The options are enabled (if disabled) and the values are applied
39+
if a matching format string is encountered with the option name.
40+
"""
41+
42+
def generate_noxfile(**option_mapping: str | bool) -> str:
43+
path = Path(RESOURCES) / "noxfile_options.py"
44+
text = path.read_text(encoding="utf8")
45+
if option_mapping:
46+
for opt, _val in option_mapping.items():
47+
# "uncomment" options with values provided
48+
text = re.sub(rf"(# )?nox.options.{opt}", f"nox.options.{opt}", text)
49+
text = Template(text).safe_substitute(**option_mapping)
50+
path = tmp_path / "noxfile.py"
51+
path.write_text(text)
52+
return str(path)
53+
54+
return generate_noxfile

tests/resources/noxfile_options.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@
1616

1717
import nox
1818

19-
nox.options.reuse_existing_virtualenvs = True
20-
# nox.options.error_on_missing_interpreters = {error_on_missing_interpreters} # used by tests
19+
# nox.options.reuse_existing_virtualenvs = ${reuse_existing_virtualenvs}
20+
# nox.options.reuse_venv = "${reuse_venv}"
21+
# nox.options.error_on_missing_interpreters = ${error_on_missing_interpreters}
2122
nox.options.sessions = ["test"]
2223

2324

0 commit comments

Comments
 (0)