From 7997f3eede530cc96eaf7ade2c6ff0a71db257fa Mon Sep 17 00:00:00 2001 From: samypr100 <3933065+samypr100@users.noreply.github.com> Date: Sun, 23 Jul 2023 20:38:48 -0400 Subject: [PATCH 01/11] feat: support for reuse_venv option --- docs/config.rst | 5 +- docs/usage.rst | 21 +++- nox/_options.py | 51 +++++++++- nox/sessions.py | 18 +++- tests/resources/noxfile_options.py | 5 +- tests/test_main.py | 150 ++++++++++++++++++++++++++--- tests/test_sessions.py | 26 ++++- tests/test_tasks.py | 8 +- 8 files changed, 251 insertions(+), 33 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index 9311df53..ee90a030 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 `. * ``nox.options.default_venv_backend`` is equivalent to specifying :ref:`-db or --default-venv-backend `. * ``nox.options.force_venv_backend`` is equivalent to specifying :ref:`-fb or --force-venv-backend `. -* ``nox.options.reuse_existing_virtualenvs`` is equivalent to specifying :ref:`--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 `. Preferred over using ``nox.options.reuse_existing_virtualenvs``. +* ``nox.options.reuse_existing_virtualenvs`` is equivalent to specifying :ref:`--reuse-existing-virtualenvs `. You can force this off by specifying ``--no-reuse-existing-virtualenvs`` during invocation. Alias of ``nox.options.reuse_venv``. * ``nox.options.stop_on_first_error`` is equivalent to specifying :ref:`--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 `. 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 `. 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 dc1e03d3..30af1aeb 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -179,26 +179,36 @@ 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 `_ 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 `_ makes +re-install rather quick. However, there are some situations where it is +advantageous to re-use 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``. -Additionally, you can skip the re-installation of packages when a virtualenv is reused. Use ``-R`` or ``--reuse-existing-virtualenvs --no-install``: +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 +216,10 @@ The ``--no-install`` option causes the following session methods to return early - :func:`session.conda_install ` - :func:`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 7c4b89e8..f9b1b20d 100644 --- a/nox/_options.py +++ b/nox/_options.py @@ -148,6 +148,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 +) -> str: + """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. @@ -199,12 +223,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]: @@ -414,6 +448,18 @@ def _tag_completer( " 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"), @@ -422,7 +468,8 @@ def _tag_completer( "--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 f29e6bfc..a45ac513 100644 --- a/nox/sessions.py +++ b/nox/sessions.py @@ -767,9 +767,7 @@ def _create_venv(self) -> None: 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,20 @@ def _create_venv(self) -> None: self.venv.create() + def reuse_existing_venv(self) -> bool: + return any( + ( + # forces every session to re-use its env + self.global_config.reuse_venv == "always", + # sessions marked True will always be reused unless never is specified + self.func.reuse_venv is True + and self.global_config.reuse_venv != "never", + # session marked False will never be reused unless always is specified + 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}") diff --git a/tests/resources/noxfile_options.py b/tests/resources/noxfile_options.py index b625cae5..6bd4e884 100644 --- a/tests/resources/noxfile_options.py +++ b/tests/resources/noxfile_options.py @@ -16,8 +16,9 @@ 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 e33277b3..8b97da1e 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -16,8 +16,10 @@ import contextlib import os +import re import sys from pathlib import Path +from string import Template from unittest import mock import pytest @@ -61,6 +63,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 +104,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 +184,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 +471,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 +482,7 @@ def test_main_noxfile_options(monkeypatch): "-s", "test", "--noxfile", - os.path.join(RESOURCES, "noxfile_options.py"), + noxfile_path, ], ) @@ -491,9 +497,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 +512,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 +527,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: @@ -541,6 +551,29 @@ def test_main_noxfile_options_sessions(monkeypatch): assert config.sessions == ["test"] +@pytest.fixture +def generate_noxfile_options(tmp_path): + """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): + 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 + + @pytest.fixture def generate_noxfile_options_pythons(tmp_path): """Generate noxfile.py with test and launch_rocket sessions. @@ -693,7 +726,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 +746,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 +760,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 +780,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 4b831460..24ebd635 100644 --- a/tests/test_sessions.py +++ b/tests/test_sessions.py @@ -857,7 +857,7 @@ def make_runner(self): 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 @@ def test__create_venv_options(self, create_method, venv_backend, expected_backen 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 fe3e0b5e..093b5826 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -24,6 +24,7 @@ from unittest import mock import pytest +from test_main import generate_noxfile_options # noqa: F401 import nox from nox import _options, sessions, tasks @@ -320,7 +321,9 @@ def quux(): 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 # noqa: F811 +): @nox.session(tags=["foobar"]) def test(): pass @@ -329,8 +332,9 @@ def test(): 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=[], From a13dd112b1a75a10cf416918f19c6fde7d5bd3a7 Mon Sep 17 00:00:00 2001 From: samypr100 <3933065+samypr100@users.noreply.github.com> Date: Mon, 24 Jul 2023 08:25:41 -0400 Subject: [PATCH 02/11] typing: adding Literal for reuse_venv choices --- nox/_options.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/nox/_options.py b/nox/_options.py index f9b1b20d..933f6dc2 100644 --- a/nox/_options.py +++ b/nox/_options.py @@ -27,6 +27,13 @@ from nox import _option_set from nox.tasks import discover_manifest, filter_manifest, load_nox_module +if sys.version_info < (3, 8): # pragma: no cover + from typing_extensions import Literal +else: # pragma: no cover + from typing import Literal + +ReuseVenvType = Literal["no", "yes", "never", "always"] + """All of Nox's configuration options.""" options = _option_set.OptionSet( @@ -150,7 +157,7 @@ def _envdir_merge_func( def _reuse_venv_merge_func( command_args: argparse.Namespace, noxfile_args: argparse.Namespace -) -> str: +) -> ReuseVenvType: """Merge reuse_venv from command args and Noxfile while maintaining backwards compatibility with reuse_existing_virtualenvs. Default is "no". From 3efc4c1cdb1de7b17e9a9e1f824e40e196718d46 Mon Sep 17 00:00:00 2001 From: samypr100 <3933065+samypr100@users.noreply.github.com> Date: Sun, 19 Nov 2023 10:21:42 -0500 Subject: [PATCH 03/11] chore: ruff-format --- tests/test_tasks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_tasks.py b/tests/test_tasks.py index 093b5826..4627b9f2 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -322,7 +322,8 @@ def quux(): def test_merge_sessions_and_tags( - reset_global_nox_options, generate_noxfile_options # noqa: F811 + reset_global_nox_options, + generate_noxfile_options, # noqa: F811 ): @nox.session(tags=["foobar"]) def test(): From 397d89d11b922238def9506d6c95080a2c9ac729 Mon Sep 17 00:00:00 2001 From: samypr100 <3933065+samypr100@users.noreply.github.com> Date: Sun, 19 Nov 2023 10:24:11 -0500 Subject: [PATCH 04/11] chore: codespell --- nox/sessions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nox/sessions.py b/nox/sessions.py index a45ac513..95bd049b 100644 --- a/nox/sessions.py +++ b/nox/sessions.py @@ -796,7 +796,7 @@ def _create_venv(self) -> None: def reuse_existing_venv(self) -> bool: return any( ( - # forces every session to re-use its env + # forces every session to reuse its env self.global_config.reuse_venv == "always", # sessions marked True will always be reused unless never is specified self.func.reuse_venv is True From 4eb4c4ee29f0482427163c0b5b76ff47b1b9314f Mon Sep 17 00:00:00 2001 From: samypr100 <3933065+samypr100@users.noreply.github.com> Date: Tue, 21 Nov 2023 16:00:43 -0500 Subject: [PATCH 05/11] chore: move `generate_noxfile_options` fixture to `conftest.py` --- tests/conftest.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ tests/test_main.py | 25 ------------------------- tests/test_tasks.py | 6 +----- 3 files changed, 45 insertions(+), 30 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 52662964..dc0abf41 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,22 @@ +# 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 +25,28 @@ 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/test_main.py b/tests/test_main.py index 8b97da1e..a30e7efb 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -16,10 +16,8 @@ import contextlib import os -import re import sys from pathlib import Path -from string import Template from unittest import mock import pytest @@ -551,29 +549,6 @@ def test_main_noxfile_options_sessions(monkeypatch, generate_noxfile_options): assert config.sessions == ["test"] -@pytest.fixture -def generate_noxfile_options(tmp_path): - """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): - 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 - - @pytest.fixture def generate_noxfile_options_pythons(tmp_path): """Generate noxfile.py with test and launch_rocket sessions. diff --git a/tests/test_tasks.py b/tests/test_tasks.py index 4627b9f2..947239ed 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -24,7 +24,6 @@ from unittest import mock import pytest -from test_main import generate_noxfile_options # noqa: F401 import nox from nox import _options, sessions, tasks @@ -321,10 +320,7 @@ def quux(): assert "Tag selection caused no sessions to be selected." in caplog.text -def test_merge_sessions_and_tags( - reset_global_nox_options, - generate_noxfile_options, # noqa: F811 -): +def test_merge_sessions_and_tags(reset_global_nox_options, generate_noxfile_options): @nox.session(tags=["foobar"]) def test(): pass From 0085e2b139f48b24411a0ad58af1a0a147f182c2 Mon Sep 17 00:00:00 2001 From: samypr100 <3933065+samypr100@users.noreply.github.com> Date: Tue, 21 Nov 2023 16:04:54 -0500 Subject: [PATCH 06/11] chore: ruff-format --- tests/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/conftest.py b/tests/conftest.py index dc0abf41..2c556499 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,6 +12,7 @@ # 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 97e21eebcd4e48b99054f35f198c66e1f088dea5 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Fri, 23 Feb 2024 14:43:56 -0500 Subject: [PATCH 07/11] chore: fix lints Signed-off-by: Henry Schreiner --- docs/usage.rst | 2 +- tests/conftest.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/usage.rst b/docs/usage.rst index 30af1aeb..20306f99 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -188,7 +188,7 @@ 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 `_ makes re-install rather quick. However, there are some situations where it is -advantageous to re-use the virtualenvs between runs. Use ``-r`` or +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``: diff --git a/tests/conftest.py b/tests/conftest.py index 2c556499..160beae4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -27,6 +27,7 @@ def reset_color_envvars(monkeypatch): monkeypatch.delenv("FORCE_COLOR", raising=False) monkeypatch.delenv("NO_COLOR", raising=False) + RESOURCES = Path(__file__).parent.joinpath("resources") From 715bc9a63d2098968c176bb154407a06ddb890c3 Mon Sep 17 00:00:00 2001 From: samypr100 <3933065+samypr100@users.noreply.github.com> Date: Sat, 24 Feb 2024 21:05:57 -0500 Subject: [PATCH 08/11] docs: clarify reuse_existing_venv function decision matrix --- nox/sessions.py | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/nox/sessions.py b/nox/sessions.py index 7a649114..a98916f4 100644 --- a/nox/sessions.py +++ b/nox/sessions.py @@ -794,14 +794,45 @@ def _create_venv(self) -> None: 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( ( - # forces every session to reuse its env + # "always" forces reuse regardless of func.reuse_venv self.global_config.reuse_venv == "always", - # sessions marked True will always be reused unless never is specified + # 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", - # session marked False will never be reused unless always is specified + # 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", ) From 61dd27233d639b20ec18a8e00d1c6b3dc1ec3ed2 Mon Sep 17 00:00:00 2001 From: samypr100 <3933065+samypr100@users.noreply.github.com> Date: Sat, 24 Feb 2024 21:20:37 -0500 Subject: [PATCH 09/11] chore: remove ``# pragma: no cover` due to recent main branch changes --- nox/_options.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nox/_options.py b/nox/_options.py index f57550e0..cf9cbde9 100644 --- a/nox/_options.py +++ b/nox/_options.py @@ -27,9 +27,9 @@ from nox import _option_set from nox.tasks import discover_manifest, filter_manifest, load_nox_module -if sys.version_info < (3, 8): # pragma: no cover +if sys.version_info < (3, 8): from typing_extensions import Literal -else: # pragma: no cover +else: from typing import Literal ReuseVenvType = Literal["no", "yes", "never", "always"] From fdf876c16f2605ad4319c0bc42f68108ad15ceb6 Mon Sep 17 00:00:00 2001 From: samypr100 <3933065+samypr100@users.noreply.github.com> Date: Sun, 25 Feb 2024 08:25:22 -0500 Subject: [PATCH 10/11] docs: call out that --reuse-existing-virtualenvs/--no-reuse-existing-virtualenvs is alias to --reuse-venv=yes|no in usage.rst --- docs/usage.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/usage.rst b/docs/usage.rst index 20306f99..6491dc0a 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -201,6 +201,10 @@ advantageous to reuse the virtualenvs between runs. Use ``-r`` or 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:: + + ``--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``: From 88d11b7000a16718a6d9afa85fb03bb2551eb4b9 Mon Sep 17 00:00:00 2001 From: samypr100 <3933065+samypr100@users.noreply.github.com> Date: Sun, 25 Feb 2024 09:22:44 -0500 Subject: [PATCH 11/11] docs: small update to config.rst --- docs/config.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.rst b/docs/config.rst index ee90a030..74bb7836 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -433,7 +433,7 @@ The following options can be specified in the Noxfile: * ``nox.options.default_venv_backend`` is equivalent to specifying :ref:`-db or --default-venv-backend `. * ``nox.options.force_venv_backend`` is equivalent to specifying :ref:`-fb or --force-venv-backend `. * ``nox.options.reuse_venv`` is equivalent to specifying :ref:`--reuse-venv `. Preferred over using ``nox.options.reuse_existing_virtualenvs``. -* ``nox.options.reuse_existing_virtualenvs`` is equivalent to specifying :ref:`--reuse-existing-virtualenvs `. You can force this off by specifying ``--no-reuse-existing-virtualenvs`` during invocation. Alias of ``nox.options.reuse_venv``. +* ``nox.options.reuse_existing_virtualenvs`` is equivalent to specifying :ref:`--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 `. 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 `. 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 `. You can force this off by specifying ``--no-error-on-external-run`` during invocation.