Repository: wntrblm/nox (python)

Issue:
[BUG] --no-reuse-existing-virtualenvs doesn't seem to work
<!-- Thank you for filing a bug! Please feel free to answer as much or as little of this template as you can. -->

**Describe the bug**
<!-- Please be as detailed as possible! -->

Adding `--no-reuse-existing-virtualenvs` does not seem to ignore the cache, and instead just reuses the existing environments anyway. Happened on the previous version, and then did a `brew upgrade`, and 2021.10.1 also has this problem. Unless I misunderstand this option, pretty sure it's broken.

**How to reproduce**
<!-- If possible, include your Noxfile and how you invoked Nox -->

```python
@nox.session(reuse_venv=True)
def type(session):
    session.install('mypy')
    session.run('mypy')
```
(simplified slightly, should be unimportant)

```console
$ nox --no-reuse-existing-virtualenvs -s type
nox > Running session type
nox > Re-using existing virtual environment at .nox/type.
```

**Expected behavior**
The existing virtual environment should not have been reused. (It really was, not just visually; I was trying to remove an item from the install list, but it remained installed).


Pull request description:
feat: support for reuse_venv option
Closes #488

This PR introduces a new option `reuse_venv` at the CLI and nox.options level as discussed in #488. It takes 4 possible values, namely `no`, `yes`, `never`, and `always` as discussed in the issue.
The code that evaluates if a venv should be reused has been switched to use this new option, and backwards compatiblity with `reuse_existing_virtualenvs` is kept using appropriate `finalizer_func` and `merge_func` on `_options.py`.

I've added tests to test new functionality as well do backwards compatibility checks and hopefully 🤞 updated the docs properly. I also added a new fixture `generate_noxfile_options` that helps with more dynamic configuration of `resources/noxfile_options.py` used in the tests to reduce boilerplate.


Patch:
diff --git a/docs/config.rst b/docs/config.rst
index 9311df5..74bb783 100644
--- a/docs/config.rst
+++ b/docs/config.rst
@@ -157,7 +157,7 @@ Use of :func:`session.install()` is deprecated without a virtualenv since it mod
     def tests(session):
         session.run("pip", "install", "nox")
 
-You can also specify that the virtualenv should *always* be reused instead of recreated every time:
+You can also specify that the virtualenv should *always* be reused instead of recreated every time unless ``--reuse-venv=never``:
 
 .. code-block:: python
 
@@ -432,7 +432,8 @@ The following options can be specified in the Noxfile:
 * ``nox.options.tags`` is equivalent to specifying :ref:`-t or --tags <opt-sessions-pythons-and-keywords>`.
 * ``nox.options.default_venv_backend`` is equivalent to specifying :ref:`-db or --default-venv-backend <opt-default-venv-backend>`.
 * ``nox.options.force_venv_backend`` is equivalent to specifying :ref:`-fb or --force-venv-backend <opt-force-venv-backend>`.
-* ``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.
+* ``nox.options.reuse_venv`` is equivalent to specifying :ref:`--reuse-venv <opt-reuse-venv>`. Preferred over using ``nox.options.reuse_existing_virtualenvs``.
+* ``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``.
 * ``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.
 * ``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.
 * ``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.
diff --git a/docs/usage.rst b/docs/usage.rst
index dc1e03d..6491dc0 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -179,26 +179,40 @@ Finally note that the ``--no-venv`` flag is a shortcut for ``--force-venv-backen
     nox --no-venv
 
 .. _opt-reuse-existing-virtualenvs:
+.. _opt-reuse-venv:
 
 Re-using virtualenvs
 --------------------
 
-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``:
+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`` or for fine-grained control use
+``--reuse-venv=yes|no|always|never``:
 
 .. code-block:: console
 
     nox -r
     nox --reuse-existing-virtualenvs
-
+    nox --reuse-venv=yes # preferred
 
 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``.
+Similarly you can override ``nox.options.reuse_venvs`` from the Noxfile via the command line by using ``--reuse-venv=yes|no|always|never``.
+
+.. note::
 
-Additionally, you can skip the re-installation of packages when a virtualenv is reused. Use ``-R`` or ``--reuse-existing-virtualenvs --no-install``:
+    ``--reuse-existing-virtualenvs`` is a alias for ``--reuse-venv=yes`` and ``--no-reuse-existing-virtualenvs`` is an alias for ``--reuse-venv=no``.
+
+Additionally, you can skip the re-installation of packages when a virtualenv is reused.
+Use ``-R`` or ``--reuse-existing-virtualenvs --no-install`` or ``--reuse-venv=yes --no-install``:
 
 .. code-block:: console
 
     nox -R
     nox --reuse-existing-virtualenvs --no-install
+    nox --reuse-venv=yes --no-install
 
 The ``--no-install`` option causes the following session methods to return early:
 
@@ -206,7 +220,10 @@ The ``--no-install`` option causes the following session methods to return early
 - :func:`session.conda_install <nox.sessions.Session.conda_install>`
 - :func:`session.run_install <nox.sessions.Session.run_install>`
 
-This option has no effect if the virtualenv is not being reused.
+The ``never`` and ``always`` options in ``--reuse-venv`` gives you more fine-grained control
+as it ignores when a ``@nox.session`` has ``reuse_venv=True|False`` defined.
+
+These options have no effect if the virtualenv is not being reused.
 
 .. _opt-running-extra-pythons:
 
diff --git a/nox/_options.py b/nox/_options.py
index f6dd828..cf9cbde 100644
--- a/nox/_options.py
+++ b/nox/_options.py
@@ -27,6 +27,13 @@ import argcomplete
 from nox import _option_set
 from nox.tasks import discover_manifest, filter_manifest, load_nox_module
 
+if sys.version_info < (3, 8):
+    from typing_extensions import Literal
+else:
+    from typing import Literal
+
+ReuseVenvType = Literal["no", "yes", "never", "always"]
+
 """All of Nox's configuration options."""
 
 options = _option_set.OptionSet(
@@ -146,6 +153,30 @@ def _envdir_merge_func(
     return command_args.envdir or noxfile_args.envdir or ".nox"
 
 
+def _reuse_venv_merge_func(
+    command_args: argparse.Namespace, noxfile_args: argparse.Namespace
+) -> ReuseVenvType:
+    """Merge reuse_venv from command args and Noxfile while maintaining
+    backwards compatibility with reuse_existing_virtualenvs. Default is "no".
+
+    Args:
+        command_args (_option_set.Namespace): The options specified on the
+            command-line.
+        noxfile_Args (_option_set.Namespace): The options specified in the
+            Noxfile.
+    """
+    # back-compat scenario with no_reuse_existing_virtualenvs/reuse_existing_virtualenvs
+    if command_args.no_reuse_existing_virtualenvs:
+        return "no"
+    if (
+        command_args.reuse_existing_virtualenvs
+        or noxfile_args.reuse_existing_virtualenvs
+    ):
+        return "yes"
+    # regular option behavior
+    return command_args.reuse_venv or noxfile_args.reuse_venv or "no"
+
+
 def default_env_var_list_factory(env_var: str) -> Callable[[], list[str] | None]:
     """Looks at the env var to set the default value for a list of env vars.
 
@@ -197,12 +228,22 @@ def _force_pythons_finalizer(
 
 
 def _R_finalizer(value: bool, args: argparse.Namespace) -> bool:
-    """Propagate -R to --reuse-existing-virtualenvs and --no-install."""
+    """Propagate -R to --reuse-existing-virtualenvs and --no-install and --reuse-venv=yes."""
     if value:
+        args.reuse_venv = "yes"
         args.reuse_existing_virtualenvs = args.no_install = value
     return value
 
 
+def _reuse_existing_virtualenvs_finalizer(
+    value: bool, args: argparse.Namespace
+) -> bool:
+    """Propagate --reuse-existing-virtualenvs to --reuse-venv=yes."""
+    if value:
+        args.reuse_venv = "yes"
+    return value
+
+
 def _posargs_finalizer(
     value: Sequence[Any], args: argparse.Namespace
 ) -> Sequence[Any] | list[Any]:
@@ -412,6 +453,18 @@ options.add_options(
             " creating a venv. This is an alias for '--force-venv-backend none'."
         ),
     ),
+    _option_set.Option(
+        "reuse_venv",
+        "--reuse-venv",
+        group=options.groups["environment"],
+        noxfile=True,
+        merge_func=_reuse_venv_merge_func,
+        help=(
+            "Controls existing virtualenvs recreation. This is ``'no'`` by"
+            " default, but any of ``('yes', 'no', 'always', 'never')`` are accepted."
+        ),
+        choices=["yes", "no", "always", "never"],
+    ),
     *_option_set.make_flag_pair(
         "reuse_existing_virtualenvs",
         ("-r", "--reuse-existing-virtualenvs"),
@@ -420,7 +473,8 @@ options.add_options(
             "--no-reuse-existing-virtualenvs",
         ),
         group=options.groups["environment"],
-        help="Reuse existing virtualenvs instead of recreating them.",
+        help="This is an alias for '--reuse-venv=yes|no'.",
+        finalizer_func=_reuse_existing_virtualenvs_finalizer,
     ),
     _option_set.Option(
         "R",
diff --git a/nox/sessions.py b/nox/sessions.py
index f3c2540..a98916f 100644
--- a/nox/sessions.py
+++ b/nox/sessions.py
@@ -767,9 +767,7 @@ class SessionRunner:
             self.venv = PassthroughEnv()
             return
 
-        reuse_existing = (
-            self.func.reuse_venv or self.global_config.reuse_existing_virtualenvs
-        )
+        reuse_existing = self.reuse_existing_venv()
 
         if backend is None or backend in {"virtualenv", "venv", "uv"}:
             self.venv = VirtualEnv(
@@ -795,6 +793,51 @@ class SessionRunner:
 
         self.venv.create()
 
+    def reuse_existing_venv(self) -> bool:
+        """
+        Determines whether to reuse an existing virtual environment.
+
+        The decision matrix is as follows:
+
+        +--------------------------+-----------------+-------------+
+        | global_config.reuse_venv | func.reuse_venv | Reuse venv? |
+        +==========================+=================+=============+
+        | "always"                 | N/A             | Yes         |
+        +--------------------------+-----------------+-------------+
+        | "never"                  | N/A             | No          |
+        +--------------------------+-----------------+-------------+
+        | "yes"                    | True|None       | Yes         |
+        +--------------------------+-----------------+-------------+
+        | "yes"                    | False           | No          |
+        +--------------------------+-----------------+-------------+
+        | "no"                     | True            | Yes         |
+        +--------------------------+-----------------+-------------+
+        | "no"                     | False|None      | No          |
+        +--------------------------+-----------------+-------------+
+
+        Summary
+        ~~~~~~~
+        - "always" forces reuse regardless of `func.reuse_venv`.
+        - "never" forces recreation regardless of `func.reuse_venv`.
+        - "yes" and "no" respect `func.reuse_venv` being ``False`` or ``True`` respectively.
+
+        Returns:
+            bool: True if the existing virtual environment should be reused, False otherwise.
+        """
+
+        return any(
+            (
+                # "always" forces reuse regardless of func.reuse_venv
+                self.global_config.reuse_venv == "always",
+                # Respect func.reuse_venv when it's explicitly True, unless global_config is "never"
+                self.func.reuse_venv is True
+                and self.global_config.reuse_venv != "never",
+                # Delegate to reuse ("yes") when func.reuse_venv is not explicitly False
+                self.func.reuse_venv is not False
+                and self.global_config.reuse_venv == "yes",
+            )
+        )
+
     def execute(self) -> Result:
         logger.warning(f"Running session {self.friendly_name}")
 


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 5266296..160beae 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,3 +1,23 @@
+# Copyright 2023 Alethea Katherine Flowers
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from __future__ import annotations
+
+import re
+from pathlib import Path
+from string import Template
+from typing import Callable
+
 import pytest
 
 
@@ -6,3 +26,29 @@ def reset_color_envvars(monkeypatch):
     """Remove color-related envvars to fix test output"""
     monkeypatch.delenv("FORCE_COLOR", raising=False)
     monkeypatch.delenv("NO_COLOR", raising=False)
+
+
+RESOURCES = Path(__file__).parent.joinpath("resources")
+
+
+@pytest.fixture
+def generate_noxfile_options(tmp_path: Path) -> Callable[..., str]:
+    """Generate noxfile.py with test and templated options.
+
+    The options are enabled (if disabled) and the values are applied
+    if a matching format string is encountered with the option name.
+    """
+
+    def generate_noxfile(**option_mapping: str | bool) -> str:
+        path = Path(RESOURCES) / "noxfile_options.py"
+        text = path.read_text(encoding="utf8")
+        if option_mapping:
+            for opt, _val in option_mapping.items():
+                # "uncomment" options with values provided
+                text = re.sub(rf"(# )?nox.options.{opt}", f"nox.options.{opt}", text)
+            text = Template(text).safe_substitute(**option_mapping)
+        path = tmp_path / "noxfile.py"
+        path.write_text(text)
+        return str(path)
+
+    return generate_noxfile
diff --git a/tests/resources/noxfile_options.py b/tests/resources/noxfile_options.py
index b625cae..6bd4e88 100644
--- a/tests/resources/noxfile_options.py
+++ b/tests/resources/noxfile_options.py
@@ -16,8 +16,9 @@ from __future__ import annotations
 
 import nox
 
-nox.options.reuse_existing_virtualenvs = True
-# nox.options.error_on_missing_interpreters = {error_on_missing_interpreters} # used by tests
+# nox.options.reuse_existing_virtualenvs = ${reuse_existing_virtualenvs}
+# nox.options.reuse_venv = "${reuse_venv}"
+# nox.options.error_on_missing_interpreters = ${error_on_missing_interpreters}
 nox.options.sessions = ["test"]
 
 
diff --git a/tests/test_main.py b/tests/test_main.py
index 158702f..251432a 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -61,6 +61,7 @@ def test_main_no_args(monkeypatch):
         assert config.sessions is None
         assert not config.no_venv
         assert not config.reuse_existing_virtualenvs
+        assert not config.reuse_venv
         assert not config.stop_on_first_error
         assert config.posargs == []
 
@@ -101,6 +102,7 @@ def test_main_long_form_args():
         assert config.force_venv_backend == "none"
         assert config.no_venv is True
         assert config.reuse_existing_virtualenvs is True
+        assert config.reuse_venv == "yes"
         assert config.stop_on_first_error is True
         assert config.posargs == []
 
@@ -180,6 +182,7 @@ def test_main_short_form_args(monkeypatch):
         assert config.default_venv_backend == "venv"
         assert config.force_venv_backend == "conda"
         assert config.reuse_existing_virtualenvs is True
+        assert config.reuse_venv == "yes"
 
 
 def test_main_explicit_sessions(monkeypatch):
@@ -466,7 +469,8 @@ def test_main_with_bad_session_names(run_nox, session):
     assert session in stderr
 
 
-def test_main_noxfile_options(monkeypatch):
+def test_main_noxfile_options(monkeypatch, generate_noxfile_options):
+    noxfile_path = generate_noxfile_options(reuse_existing_virtualenvs=True)
     monkeypatch.setattr(
         sys,
         "argv",
@@ -476,7 +480,7 @@ def test_main_noxfile_options(monkeypatch):
             "-s",
             "test",
             "--noxfile",
-            os.path.join(RESOURCES, "noxfile_options.py"),
+            noxfile_path,
         ],
     )
 
@@ -491,9 +495,11 @@ def test_main_noxfile_options(monkeypatch):
         # Verify that the config looks correct.
         config = honor_list_request.call_args[1]["global_config"]
         assert config.reuse_existing_virtualenvs is True
+        assert config.reuse_venv == "yes"
 
 
-def test_main_noxfile_options_disabled_by_flag(monkeypatch):
+def test_main_noxfile_options_disabled_by_flag(monkeypatch, generate_noxfile_options):
+    noxfile_path = generate_noxfile_options(reuse_existing_virtualenvs=True)
     monkeypatch.setattr(
         sys,
         "argv",
@@ -504,7 +510,7 @@ def test_main_noxfile_options_disabled_by_flag(monkeypatch):
             "test",
             "--no-reuse-existing-virtualenvs",
             "--noxfile",
-            os.path.join(RESOURCES, "noxfile_options.py"),
+            noxfile_path,
         ],
     )
 
@@ -519,13 +525,15 @@ def test_main_noxfile_options_disabled_by_flag(monkeypatch):
         # Verify that the config looks correct.
         config = honor_list_request.call_args[1]["global_config"]
         assert config.reuse_existing_virtualenvs is False
+        assert config.reuse_venv == "no"
 
 
-def test_main_noxfile_options_sessions(monkeypatch):
+def test_main_noxfile_options_sessions(monkeypatch, generate_noxfile_options):
+    noxfile_path = generate_noxfile_options(reuse_existing_virtualenvs=True)
     monkeypatch.setattr(
         sys,
         "argv",
-        ["nox", "-l", "--noxfile", os.path.join(RESOURCES, "noxfile_options.py")],
+        ["nox", "-l", "--noxfile", noxfile_path],
     )
 
     with mock.patch("nox.tasks.honor_list_request") as honor_list_request:
@@ -693,7 +701,11 @@ def test_main_reuse_existing_virtualenvs_no_install(monkeypatch):
         with mock.patch.object(sys, "exit"):
             nox.__main__.main()
         config = execute.call_args[1]["global_config"]
-    assert config.reuse_existing_virtualenvs and config.no_install
+    assert (
+        config.reuse_existing_virtualenvs
+        and config.no_install
+        and config.reuse_venv == "yes"
+    )
 
 
 @pytest.mark.parametrize(
@@ -709,7 +721,7 @@ def test_main_reuse_existing_virtualenvs_no_install(monkeypatch):
 )
 def test_main_noxfile_options_with_ci_override(
     monkeypatch,
-    tmp_path,
+    generate_noxfile_options,
     should_set_ci_env_var,
     noxfile_option_value,
     expected_final_value,
@@ -723,16 +735,12 @@ def test_main_noxfile_options_with_ci_override(
     )
     monkeypatch.setattr(nox, "options", nox._options.noxfile_options)
 
-    noxfile_path = Path(RESOURCES) / "noxfile_options.py"
-    if noxfile_option_value is not None:
-        # Temp noxfile with error_on_missing_interpreters set
-        noxfile_text = noxfile_path.read_text()
-        noxfile_text = noxfile_text.replace("# nox", "nox")
-        noxfile_text = noxfile_text.format(
+    if noxfile_option_value is None:
+        noxfile_path = generate_noxfile_options()
+    else:
+        noxfile_path = generate_noxfile_options(
             error_on_missing_interpreters=noxfile_option_value
         )
-        noxfile_path = tmp_path / "noxfile.py"
-        noxfile_path.write_text(noxfile_text)
 
     monkeypatch.setattr(
         sys,
@@ -747,3 +755,88 @@ def test_main_noxfile_options_with_ci_override(
             nox.__main__.main()
         config = honor_list_request.call_args[1]["global_config"]
     assert config.error_on_missing_interpreters == expected_final_value
+
+
+@pytest.mark.parametrize(
+    "reuse_venv",
+    [
+        "yes",
+        "no",
+        "always",
+        "never",
+    ],
+)
+def test_main_reuse_venv_cli_flags(monkeypatch, generate_noxfile_options, reuse_venv):
+    monkeypatch.setattr(sys, "argv", ["nox", "--reuse-venv", reuse_venv])
+    with mock.patch("nox.workflow.execute", return_value=0) as execute:
+        with mock.patch.object(sys, "exit"):
+            nox.__main__.main()
+        config = execute.call_args[1]["global_config"]
+    assert (
+        not config.reuse_existing_virtualenvs
+    )  # should remain unaffected in this case
+    assert config.reuse_venv == reuse_venv
+
+
+@pytest.mark.parametrize(
+    ("reuse_venv", "reuse_existing_virtualenvs", "expected"),
+    [
+        ("yes", None, "yes"),
+        ("yes", False, "yes"),
+        ("yes", True, "yes"),
+        ("yes", "--no-reuse-existing-virtualenvs", "no"),
+        ("yes", "--reuse-existing-virtualenvs", "yes"),
+        ("no", None, "no"),
+        ("no", False, "no"),
+        ("no", True, "yes"),
+        ("no", "--no-reuse-existing-virtualenvs", "no"),
+        ("no", "--reuse-existing-virtualenvs", "yes"),
+        ("always", None, "always"),
+        ("always", False, "always"),
+        ("always", True, "yes"),
+        ("always", "--no-reuse-existing-virtualenvs", "no"),
+        ("always", "--reuse-existing-virtualenvs", "yes"),
+        ("never", None, "never"),
+        ("never", False, "never"),
+        ("never", True, "yes"),
+        ("never", "--no-reuse-existing-virtualenvs", "no"),
+        ("never", "--reuse-existing-virtualenvs", "yes"),
+    ],
+)
+def test_main_noxfile_options_reuse_venv_compat_check(
+    monkeypatch,
+    generate_noxfile_options,
+    reuse_venv,
+    reuse_existing_virtualenvs,
+    expected,
+):
+    cmd_args = ["nox", "-l"]
+    # CLI Compat Check
+    if isinstance(reuse_existing_virtualenvs, str):
+        cmd_args += [reuse_existing_virtualenvs]
+
+    # Generate noxfile
+    if isinstance(reuse_existing_virtualenvs, bool):
+        # Non-CLI Compat Check
+        noxfile_path = generate_noxfile_options(
+            reuse_venv=reuse_venv, reuse_existing_virtualenvs=reuse_existing_virtualenvs
+        )
+    else:
+        noxfile_path = generate_noxfile_options(reuse_venv=reuse_venv)
+    cmd_args += ["--noxfile", str(noxfile_path)]
+
+    # Reset nox.options
+    monkeypatch.setattr(
+        nox._options, "noxfile_options", nox._options.options.noxfile_namespace()
+    )
+    monkeypatch.setattr(nox, "options", nox._options.noxfile_options)
+
+    # Execute
+    monkeypatch.setattr(sys, "argv", cmd_args)
+    with mock.patch(
+        "nox.tasks.honor_list_request", return_value=0
+    ) as honor_list_request:
+        with mock.patch("sys.exit"):
+            nox.__main__.main()
+        config = honor_list_request.call_args[1]["global_config"]
+    assert config.reuse_venv == expected
diff --git a/tests/test_sessions.py b/tests/test_sessions.py
index 4b83146..24ebd63 100644
--- a/tests/test_sessions.py
+++ b/tests/test_sessions.py
@@ -857,7 +857,7 @@ class TestSessionRunner:
                 noxfile=os.path.join(os.getcwd(), "noxfile.py"),
                 envdir="envdir",
                 posargs=[],
-                reuse_existing_virtualenvs=False,
+                reuse_venv="no",
                 error_on_missing_interpreters="CI" in os.environ,
             ),
             manifest=mock.create_autospec(nox.manifest.Manifest),
@@ -960,10 +960,32 @@ class TestSessionRunner:
     def test__create_venv_unexpected_venv_backend(self):
         runner = self.make_runner()
         runner.func.venv_backend = "somenewenvtool"
-
         with pytest.raises(ValueError, match="venv_backend"):
             runner._create_venv()
 
+    @pytest.mark.parametrize(
+        ("reuse_venv", "reuse_venv_func", "should_reuse"),
+        [
+            ("yes", None, True),
+            ("yes", False, False),
+            ("yes", True, True),
+            ("no", None, False),
+            ("no", False, False),
+            ("no", True, True),
+            ("always", None, True),
+            ("always", False, True),
+            ("always", True, True),
+            ("never", None, False),
+            ("never", False, False),
+            ("never", True, False),
+        ],
+    )
+    def test__reuse_venv_outcome(self, reuse_venv, reuse_venv_func, should_reuse):
+        runner = self.make_runner()
+        runner.func.reuse_venv = reuse_venv_func
+        runner.global_config.reuse_venv = reuse_venv
+        assert runner.reuse_existing_venv() == should_reuse
+
     def make_runner_with_mock_venv(self):
         runner = self.make_runner()
         runner._create_venv = mock.Mock()
diff --git a/tests/test_tasks.py b/tests/test_tasks.py
index fe3e0b5..947239e 100644
--- a/tests/test_tasks.py
+++ b/tests/test_tasks.py
@@ -320,7 +320,7 @@ def test_filter_manifest_tags_not_found(tags, caplog):
     assert "Tag selection caused no sessions to be selected." in caplog.text
 
 
-def test_merge_sessions_and_tags(reset_global_nox_options):
+def test_merge_sessions_and_tags(reset_global_nox_options, generate_noxfile_options):
     @nox.session(tags=["foobar"])
     def test():
         pass
@@ -329,8 +329,9 @@ def test_merge_sessions_and_tags(reset_global_nox_options):
     def bar():
         pass
 
+    noxfile_path = generate_noxfile_options(reuse_existing_virtualenvs=True)
     config = _options.options.namespace(
-        noxfile=os.path.join(RESOURCES, "noxfile_options.py"),
+        noxfile=noxfile_path,
         sessions=None,
         pythons=(),
         posargs=[],