diff --git a/docs/config.rst b/docs/config.rst index e2a83386..39628b08 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -132,7 +132,7 @@ Will produce these sessions: Note that this expansion happens *before* parameterization occurs, so you can still parametrize sessions with multiple interpreters. -If you want to disable virtualenv creation altogether, you can set ``python`` to ``False``: +If you want to disable virtualenv creation altogether, you can set ``python`` to ``False``, or set ``venv_backend`` to ``"none"``, both are equivalent. Note that this can be done temporarily through the :ref:`--no-venv ` commandline flag, too. .. code-block:: python @@ -375,6 +375,8 @@ The following options can be specified in the Noxfile: * ``nox.options.sessions`` is equivalent to specifying :ref:`-s or --sessions `. * ``nox.options.pythons`` is equivalent to specifying :ref:`-p or --pythons `. * ``nox.options.keywords`` is equivalent to specifying :ref:`-k or --keywords `. +* ``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.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. diff --git a/docs/usage.rst b/docs/usage.rst index 6f0e3418..591277ea 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -105,12 +105,51 @@ Then running ``nox --session tests`` will actually run all parametrized versions nox --session "tests(django='2.0')" +.. _opt-default-venv-backend: + +Changing the sessions default backend +------------------------------------- + +By default nox uses ``virtualenv`` as the virtual environment backend for the sessions, but it also supports ``conda`` and ``venv`` as well as no backend (passthrough to whatever python environment nox is running on). You can change the default behaviour by using ``-db `` or ``--default-venv-backend ``. Supported names are ``('none', 'virtualenv', 'conda', 'venv')``. + +.. code-block:: console + + nox -db conda + nox --default-venv-backend conda + + +You can also set this option in the Noxfile with ``nox.options.default_venv_backend``. In case both are provided, the commandline argument takes precedence. + +Note that using this option does not change the backend for sessions where ``venv_backend`` is explicitly set. + + +.. _opt-force-venv-backend: + +Forcing the sessions backend +---------------------------- + +You might work in a different environment than a project's default continuous integration setttings, and might wish to get a quick way to execute the same tasks but on a different venv backend. For this purpose, you can temporarily force the backend used by **all** sessions in the current nox execution by using ``-fb `` or ``--force-venv-backend ``. No exceptions are made, the backend will be forced for all sessions run whatever the other options values and nox file configuration. Supported names are ``('none', 'virtualenv', 'conda', 'venv')``. + +.. code-block:: console + + nox -fb conda + nox --force-venv-backend conda + + +You can also set this option in the Noxfile with ``nox.options.force_venv_backend``. In case both are provided, the commandline argument takes precedence. + +Finally note that the ``--no-venv`` flag is a shortcut for ``--force-venv-backend none`` and allows to temporarily run all selected sessions on the current python interpreter (the one running nox). + +.. code-block:: console + + nox --no-venv + .. _opt-reuse-existing-virtualenvs: 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 re-use 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``: .. code-block:: console diff --git a/nox/_decorators.py b/nox/_decorators.py index 32773f83..a4f031f5 100644 --- a/nox/_decorators.py +++ b/nox/_decorators.py @@ -1,7 +1,7 @@ import copy import functools import types -from typing import Any, Callable, Iterable, List, Optional, cast +from typing import Any, Callable, Iterable, List, Dict, Optional, cast from . import _typing @@ -40,12 +40,14 @@ def __init__( name: Optional[str] = None, venv_backend: Any = None, venv_params: Any = None, + should_warn: Dict[str, Any] = None, ): self.func = func self.python = python self.reuse_venv = reuse_venv self.venv_backend = venv_backend self.venv_params = venv_params + self.should_warn = should_warn or dict() def __call__(self, *args: Any, **kwargs: Any) -> Any: return self.func(*args, **kwargs) @@ -58,6 +60,7 @@ def copy(self, name: str = None) -> "Func": name, self.venv_backend, self.venv_params, + self.should_warn, ) @@ -70,6 +73,7 @@ def __init__(self, func: Func, param_spec: "Param") -> None: None, func.venv_backend, func.venv_params, + func.should_warn, ) self.param_spec = param_spec self.session_signature = "({})".format(param_spec) diff --git a/nox/_options.py b/nox/_options.py index 782a512f..a46943a5 100644 --- a/nox/_options.py +++ b/nox/_options.py @@ -60,6 +60,49 @@ def _session_filters_merge_func( return getattr(command_args, key) +def _default_venv_backend_merge_func( + command_args: argparse.Namespace, noxfile_args: argparse.Namespace +) -> str: + """Merge default_venv_backend from command args and nox file. Default is "virtualenv". + + Args: + command_args (_option_set.Namespace): The options specified on the + command-line. + noxfile_Args (_option_set.Namespace): The options specified in the + Noxfile. + """ + return ( + command_args.default_venv_backend + or noxfile_args.default_venv_backend + or "virtualenv" + ) + + +def _force_venv_backend_merge_func( + command_args: argparse.Namespace, noxfile_args: argparse.Namespace +) -> str: + """Merge force_venv_backend from command args and nox file. Default is None. + + Args: + command_args (_option_set.Namespace): The options specified on the + command-line. + noxfile_Args (_option_set.Namespace): The options specified in the + Noxfile. + """ + if command_args.no_venv: + if ( + command_args.force_venv_backend is not None + and command_args.force_venv_backend != "none" + ): + raise ValueError( + "You can not use `--no-venv` with a non-none `--force-venv-backend`" + ) + else: + return "none" + else: + return command_args.force_venv_backend or noxfile_args.force_venv_backend + + def _envdir_merge_func( command_args: argparse.Namespace, noxfile_args: argparse.Namespace ) -> str: @@ -221,6 +264,38 @@ def _session_completer( help="Logs the output of all commands run including commands marked silent.", noxfile=True, ), + _option_set.Option( + "default_venv_backend", + "-db", + "--default-venv-backend", + group=options.groups["secondary"], + noxfile=True, + merge_func=_default_venv_backend_merge_func, + help="Virtual environment backend to use by default for nox sessions, this is ``'virtualenv'`` by default but " + "any of ``('virtualenv', 'conda', 'venv')`` are accepted.", + choices=["none", "virtualenv", "conda", "venv"], + ), + _option_set.Option( + "force_venv_backend", + "-fb", + "--force-venv-backend", + group=options.groups["secondary"], + noxfile=True, + merge_func=_force_venv_backend_merge_func, + help="Virtual environment backend to force-use for all nox sessions in this run, overriding any other venv " + "backend declared in the nox file and ignoring the default backend. Any of ``('virtualenv', 'conda', 'venv')`` " + "are accepted.", + choices=["none", "virtualenv", "conda", "venv"], + ), + _option_set.Option( + "no_venv", + "--no-venv", + group=options.groups["secondary"], + default=False, + action="store_true", + help="Runs the selected sessions directly on the current interpreter, without creating a venv. This is an alias " + "for '--force-venv-backend none'.", + ), *_option_set.make_flag_pair( "reuse_existing_virtualenvs", ("-r", "--reuse-existing-virtualenvs"), diff --git a/nox/manifest.py b/nox/manifest.py index d90ba346..f2a008f0 100644 --- a/nox/manifest.py +++ b/nox/manifest.py @@ -21,6 +21,9 @@ from nox.sessions import Session, SessionRunner +WARN_PYTHONS_IGNORED = "python_ignored" + + class Manifest: """Session manifest. @@ -170,6 +173,18 @@ def make_session( """ sessions = [] + # if backend is none we wont parametrize the pythons + backend = ( + self._config.force_venv_backend + or func.venv_backend + or self._config.default_venv_backend + ) + if backend == "none" and isinstance(func.python, (list, tuple, set)): + # we can not log a warning here since the session is maybe deselected. + # instead let's set a flag, to warn later when session is actually run. + func.should_warn[WARN_PYTHONS_IGNORED] = func.python + func.python = False + # If the func has the python attribute set to a list, we'll need # to expand them. if isinstance(func.python, (list, tuple, set)): diff --git a/nox/sessions.py b/nox/sessions.py index 59a5cf4b..2f2b0eee 100644 --- a/nox/sessions.py +++ b/nox/sessions.py @@ -24,6 +24,7 @@ Callable, Dict, Iterable, + Tuple, List, Mapping, Optional, @@ -36,7 +37,7 @@ from nox import _typing from nox._decorators import Func from nox.logger import logger -from nox.virtualenv import CondaEnv, ProcessEnv, VirtualEnv +from nox.virtualenv import CondaEnv, ProcessEnv, VirtualEnv, PassthroughEnv if _typing.TYPE_CHECKING: from nox.manifest import Manifest @@ -272,10 +273,15 @@ def conda_install(self, *args: str, **kwargs: Any) -> None: .. _conda install: """ venv = self._runner.venv - if not isinstance(venv, CondaEnv): + + prefix_args = () # type: Tuple[str, ...] + if isinstance(venv, CondaEnv): + prefix_args = ("--prefix", venv.location) + elif not isinstance(venv, PassthroughEnv): # pragma: no cover raise ValueError( "A session without a conda environment can not install dependencies from conda." ) + if not args: raise ValueError("At least one argument required to install().") @@ -283,14 +289,7 @@ def conda_install(self, *args: str, **kwargs: Any) -> None: kwargs["silent"] = True self._run( - "conda", - "install", - "--yes", - "--prefix", - venv.location, - *args, - external="error", - **kwargs + "conda", "install", "--yes", *prefix_args, *args, external="error", **kwargs ) def install(self, *args: str, **kwargs: Any) -> None: @@ -318,7 +317,9 @@ def install(self, *args: str, **kwargs: Any) -> None: .. _pip: https://pip.readthedocs.org """ - if not isinstance(self._runner.venv, (CondaEnv, VirtualEnv)): + if not isinstance( + self._runner.venv, (CondaEnv, VirtualEnv, PassthroughEnv) + ): # pragma: no cover raise ValueError( "A session without a virtualenv can not install dependencies." ) @@ -389,8 +390,14 @@ def friendly_name(self) -> str: return self.signatures[0] if self.signatures else self.name def _create_venv(self) -> None: - if self.func.python is False: - self.venv = ProcessEnv() + backend = ( + self.global_config.force_venv_backend + or self.func.venv_backend + or self.global_config.default_venv_backend + ) + + if backend == "none" or self.func.python is False: + self.venv = PassthroughEnv() return path = _normalize_path(self.global_config.envdir, self.friendly_name) @@ -398,21 +405,21 @@ def _create_venv(self) -> None: self.func.reuse_venv or self.global_config.reuse_existing_virtualenvs ) - if not self.func.venv_backend or self.func.venv_backend == "virtualenv": + if backend is None or backend == "virtualenv": self.venv = VirtualEnv( path, interpreter=self.func.python, # type: ignore reuse_existing=reuse_existing, venv_params=self.func.venv_params, ) - elif self.func.venv_backend == "conda": + elif backend == "conda": self.venv = CondaEnv( path, interpreter=self.func.python, # type: ignore reuse_existing=reuse_existing, venv_params=self.func.venv_params, ) - elif self.func.venv_backend == "venv": + elif backend == "venv": self.venv = VirtualEnv( path, interpreter=self.func.python, # type: ignore @@ -423,7 +430,7 @@ def _create_venv(self) -> None: else: raise ValueError( "Expected venv_backend one of ('virtualenv', 'conda', 'venv'), but got '{}'.".format( - self.func.venv_backend + backend ) ) diff --git a/nox/tasks.py b/nox/tasks.py index 3cbbf3ef..bd5e1c37 100644 --- a/nox/tasks.py +++ b/nox/tasks.py @@ -24,7 +24,7 @@ from colorlog.escape_codes import parse_colors from nox import _options, registry from nox.logger import logger -from nox.manifest import Manifest +from nox.manifest import Manifest, WARN_PYTHONS_IGNORED from nox.sessions import Result @@ -233,6 +233,14 @@ def run_manifest(manifest: Manifest, global_config: Namespace) -> List[Result]: # Note that it is possible for the manifest to be altered in any given # iteration. for session in manifest: + # possibly raise warnings associated with this session + if WARN_PYTHONS_IGNORED in session.func.should_warn: + logger.warning( + "Session {} is set to run with venv_backend='none', IGNORING its python={} parametrization. ".format( + session.name, session.func.should_warn[WARN_PYTHONS_IGNORED] + ) + ) + result = session.execute() result.log( "Session {name} {status}.".format( diff --git a/nox/virtualenv.py b/nox/virtualenv.py index 918525e4..c5e23db4 100644 --- a/nox/virtualenv.py +++ b/nox/virtualenv.py @@ -141,6 +141,16 @@ def _clean_location(self: "Union[CondaEnv, VirtualEnv]") -> bool: return True +class PassthroughEnv(ProcessEnv): + """Represents the environment used to run nox itself + + For now, this class is empty but it might contain tools to grasp some + hints about the actual env. + """ + + pass + + class CondaEnv(ProcessEnv): """Conda environemnt management class. diff --git a/tests/resources/noxfile_pythons.py b/tests/resources/noxfile_pythons.py new file mode 100644 index 00000000..bfba5a08 --- /dev/null +++ b/tests/resources/noxfile_pythons.py @@ -0,0 +1,7 @@ +import nox + + +@nox.session(python=["3.6"]) +@nox.parametrize("cheese", ["cheddar", "jack", "brie"]) +def snack(unused_session, cheese): + print("Noms, {} so good!".format(cheese)) diff --git a/tests/test_main.py b/tests/test_main.py index 78002581..c9ef5467 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -55,6 +55,7 @@ def test_main_no_args(monkeypatch): config = execute.call_args[1]["global_config"] assert config.noxfile == "noxfile.py" assert config.sessions is None + assert not config.no_venv assert not config.reuse_existing_virtualenvs assert not config.stop_on_first_error assert config.posargs == [] @@ -70,6 +71,11 @@ def test_main_long_form_args(): "--sessions", "1", "2", + "--default-venv-backend", + "venv", + "--force-venv-backend", + "none", + "--no-venv", "--reuse-existing-virtualenvs", "--stop-on-first-error", ] @@ -87,14 +93,72 @@ def test_main_long_form_args(): assert config.noxfile == "noxfile.py" assert config.envdir.endswith(".other") assert config.sessions == ["1", "2"] + assert config.default_venv_backend == "venv" + assert config.force_venv_backend == "none" + assert config.no_venv is True assert config.reuse_existing_virtualenvs is True assert config.stop_on_first_error is True assert config.posargs == [] +def test_main_no_venv(monkeypatch, capsys): + # Check that --no-venv overrides force_venv_backend + monkeypatch.setattr( + sys, + "argv", + [ + "nox", + "--noxfile", + os.path.join(RESOURCES, "noxfile_pythons.py"), + "--no-venv", + "-s", + "snack(cheese='cheddar')", + ], + ) + + with mock.patch("sys.exit") as sys_exit: + nox.__main__.main() + stdout, stderr = capsys.readouterr() + assert stdout == "Noms, cheddar so good!\n" + assert ( + "Session snack is set to run with venv_backend='none', IGNORING its python" + in stderr + ) + assert "Session snack(cheese='cheddar') was successful." in stderr + sys_exit.assert_called_once_with(0) + + +def test_main_no_venv_error(): + # Check that --no-venv can not be set together with a non-none --force-venv-backend + sys.argv = [ + sys.executable, + "--noxfile", + "noxfile.py", + "--force-venv-backend", + "conda", + "--no-venv", + ] + with pytest.raises(ValueError, match="You can not use"): + nox.__main__.main() + + def test_main_short_form_args(monkeypatch): monkeypatch.setattr( - sys, "argv", [sys.executable, "-f", "noxfile.py", "-s", "1", "2", "-r"] + sys, + "argv", + [ + sys.executable, + "-f", + "noxfile.py", + "-s", + "1", + "2", + "-db", + "venv", + "-fb", + "conda", + "-r", + ], ) with mock.patch("nox.workflow.execute") as execute: execute.return_value = 0 @@ -109,6 +173,8 @@ def test_main_short_form_args(monkeypatch): config = execute.call_args[1]["global_config"] assert config.noxfile == "noxfile.py" assert config.sessions == ["1", "2"] + assert config.default_venv_backend == "venv" + assert config.force_venv_backend == "conda" assert config.reuse_existing_virtualenvs is True diff --git a/tests/test_manifest.py b/tests/test_manifest.py index 9864191c..491627a5 100644 --- a/tests/test_manifest.py +++ b/tests/test_manifest.py @@ -18,19 +18,31 @@ import nox import pytest from nox._decorators import Func -from nox.manifest import KeywordLocals, Manifest, _null_session_func +from nox.manifest import ( + KeywordLocals, + Manifest, + _null_session_func, + WARN_PYTHONS_IGNORED, +) def create_mock_sessions(): sessions = collections.OrderedDict() - sessions["foo"] = mock.Mock(spec=(), python=None) - sessions["bar"] = mock.Mock(spec=(), python=None) + sessions["foo"] = mock.Mock(spec=(), python=None, venv_backend=None) + sessions["bar"] = mock.Mock(spec=(), python=None, venv_backend=None) return sessions +def create_mock_config(): + cfg = mock.sentinel.CONFIG + cfg.force_venv_backend = None + cfg.default_venv_backend = None + return cfg + + def test_init(): sessions = create_mock_sessions() - manifest = Manifest(sessions, mock.sentinel.CONFIG) + manifest = Manifest(sessions, create_mock_config()) # Assert that basic properties look correctly. assert len(manifest) == 2 @@ -40,7 +52,7 @@ def test_init(): def test_contains(): sessions = create_mock_sessions() - manifest = Manifest(sessions, mock.sentinel.CONFIG) + manifest = Manifest(sessions, create_mock_config()) # Establish that contains works pre-iteration. assert "foo" in manifest @@ -60,7 +72,7 @@ def test_contains(): def test_getitem(): sessions = create_mock_sessions() - manifest = Manifest(sessions, mock.sentinel.CONFIG) + manifest = Manifest(sessions, create_mock_config()) # Establish that each session is present, and a made-up session # is not. @@ -79,7 +91,7 @@ def test_getitem(): def test_iteration(): sessions = create_mock_sessions() - manifest = Manifest(sessions, mock.sentinel.CONFIG) + manifest = Manifest(sessions, create_mock_config()) # There should be two sessions in the queue. assert len(manifest._queue) == 2 @@ -109,7 +121,7 @@ def test_iteration(): def test_len(): sessions = create_mock_sessions() - manifest = Manifest(sessions, mock.sentinel.CONFIG) + manifest = Manifest(sessions, create_mock_config()) assert len(manifest) == 2 for session in manifest: assert len(manifest) == 2 @@ -117,7 +129,7 @@ def test_len(): def test_filter_by_name(): sessions = create_mock_sessions() - manifest = Manifest(sessions, mock.sentinel.CONFIG) + manifest = Manifest(sessions, create_mock_config()) manifest.filter_by_name(("foo",)) assert "foo" in manifest assert "bar" not in manifest @@ -125,21 +137,21 @@ def test_filter_by_name(): def test_filter_by_name_maintains_order(): sessions = create_mock_sessions() - manifest = Manifest(sessions, mock.sentinel.CONFIG) + manifest = Manifest(sessions, create_mock_config()) manifest.filter_by_name(("bar", "foo")) assert [session.name for session in manifest] == ["bar", "foo"] def test_filter_by_name_not_found(): sessions = create_mock_sessions() - manifest = Manifest(sessions, mock.sentinel.CONFIG) + manifest = Manifest(sessions, create_mock_config()) with pytest.raises(KeyError): manifest.filter_by_name(("baz",)) def test_filter_by_python_interpreter(): sessions = create_mock_sessions() - manifest = Manifest(sessions, mock.sentinel.CONFIG) + manifest = Manifest(sessions, create_mock_config()) manifest["foo"].func.python = "3.8" manifest["bar"].func.python = "3.7" manifest.filter_by_python_interpreter(("3.8",)) @@ -149,7 +161,7 @@ def test_filter_by_python_interpreter(): def test_filter_by_keyword(): sessions = create_mock_sessions() - manifest = Manifest(sessions, mock.sentinel.CONFIG) + manifest = Manifest(sessions, create_mock_config()) assert len(manifest) == 2 manifest.filter_by_keywords("foo or bar") assert len(manifest) == 2 @@ -159,7 +171,7 @@ def test_filter_by_keyword(): def test_list_all_sessions_with_filter(): sessions = create_mock_sessions() - manifest = Manifest(sessions, mock.sentinel.CONFIG) + manifest = Manifest(sessions, create_mock_config()) assert len(manifest) == 2 manifest.filter_by_keywords("foo") assert len(manifest) == 1 @@ -171,15 +183,15 @@ def test_list_all_sessions_with_filter(): def test_add_session_plain(): - manifest = Manifest({}, mock.sentinel.CONFIG) - session_func = mock.Mock(spec=(), python=None) + manifest = Manifest({}, create_mock_config()) + session_func = mock.Mock(spec=(), python=None, venv_backend=None) for session in manifest.make_session("my_session", session_func): manifest.add_session(session) assert len(manifest) == 1 def test_add_session_multiple_pythons(): - manifest = Manifest({}, mock.sentinel.CONFIG) + manifest = Manifest({}, create_mock_config()) def session_func(): pass @@ -192,7 +204,7 @@ def session_func(): def test_add_session_parametrized(): - manifest = Manifest({}, mock.sentinel.CONFIG) + manifest = Manifest({}, create_mock_config()) # Define a session with parameters. @nox.parametrize("param", ("a", "b", "c")) @@ -208,7 +220,7 @@ def my_session(session, param): def test_add_session_parametrized_multiple_pythons(): - manifest = Manifest({}, mock.sentinel.CONFIG) + manifest = Manifest({}, create_mock_config()) # Define a session with parameters. @nox.parametrize("param", ("a", "b")) @@ -224,7 +236,7 @@ def my_session(session, param): def test_add_session_parametrized_noop(): - manifest = Manifest({}, mock.sentinel.CONFIG) + manifest = Manifest({}, create_mock_config()) # Define a session without any parameters. @nox.parametrize("param", ()) @@ -232,6 +244,7 @@ def my_session(session, param): pass my_session.python = None + my_session.venv_backend = None # Add the session to the manifest. for session in manifest.make_session("my_session", my_session): @@ -244,18 +257,20 @@ def my_session(session, param): def test_notify(): - manifest = Manifest({}, mock.sentinel.CONFIG) + manifest = Manifest({}, create_mock_config()) # Define a session. def my_session(session): pass my_session.python = None + my_session.venv_backend = None def notified(session): pass notified.python = None + notified.venv_backend = None # Add the sessions to the manifest. for session in manifest.make_session("my_session", my_session): @@ -274,13 +289,14 @@ def notified(session): def test_notify_noop(): - manifest = Manifest({}, mock.sentinel.CONFIG) + manifest = Manifest({}, create_mock_config()) # Define a session and add it to the manifest. def my_session(session): pass my_session.python = None + my_session.venv_backend = None for session in manifest.make_session("my_session", my_session): manifest.add_session(session) @@ -293,14 +309,14 @@ def my_session(session): def test_notify_error(): - manifest = Manifest({}, mock.sentinel.CONFIG) + manifest = Manifest({}, create_mock_config()) with pytest.raises(ValueError): manifest.notify("does_not_exist") def test_add_session_idempotent(): - manifest = Manifest({}, mock.sentinel.CONFIG) - session_func = mock.Mock(spec=(), python=None) + manifest = Manifest({}, create_mock_config()) + session_func = mock.Mock(spec=(), python=None, venv_backend=None) for session in manifest.make_session("my_session", session_func): manifest.add_session(session) manifest.add_session(session) @@ -322,3 +338,22 @@ def test_keyword_locals_iter(): values = ["foo", "bar"] kw = KeywordLocals(values) assert list(kw) == values + + +def test_no_venv_backend_but_some_pythons(): + manifest = Manifest({}, create_mock_config()) + + # Define a session and add it to the manifest. + def my_session(session): + pass + + # the session sets "no venv backend" but declares some pythons + my_session.python = ["3.7", "3.8"] + my_session.venv_backend = "none" + my_session.should_warn = dict() + + sessions = manifest.make_session("my_session", my_session) + + # check that the pythons were correctly removed (a log warning is also emitted) + assert sessions[0].func.python is False + assert sessions[0].func.should_warn == {WARN_PYTHONS_IGNORED: ["3.7", "3.8"]} diff --git a/tests/test_sessions.py b/tests/test_sessions.py index 8af3f858..ebdf2016 100644 --- a/tests/test_sessions.py +++ b/tests/test_sessions.py @@ -246,6 +246,7 @@ def test_run_external_with_error_on_external_run_condaenv(self): def test_conda_install_bad_args(self): session, runner = self.make_session_and_runner() runner.venv = mock.create_autospec(nox.virtualenv.CondaEnv) + runner.venv.location = "dummy" with pytest.raises(ValueError, match="arg"): session.conda_install() diff --git a/tests/test_tasks.py b/tests/test_tasks.py index e9143136..c81490ad 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -23,7 +23,7 @@ import nox import pytest from nox import _options, sessions, tasks -from nox.manifest import Manifest +from nox.manifest import Manifest, WARN_PYTHONS_IGNORED RESOURCES = os.path.join(os.path.dirname(__file__), "resources") @@ -33,6 +33,8 @@ def session_func(): session_func.python = None +session_func.venv_backend = None +session_func.should_warn = dict() def session_func_with_python(): @@ -40,6 +42,16 @@ def session_func_with_python(): session_func_with_python.python = "3.8" +session_func_with_python.venv_backend = None + + +def session_func_venv_pythons_warning(): + pass + + +session_func_venv_pythons_warning.python = ["3.7"] +session_func_venv_pythons_warning.venv_backend = "none" +session_func_venv_pythons_warning.should_warn = {WARN_PYTHONS_IGNORED: ["3.7"]} def test_load_nox_module(): @@ -185,7 +197,8 @@ def test_verify_manifest_nonempty(): assert return_value == manifest -def test_run_manifest(): +@pytest.mark.parametrize("with_warnings", [False, True], ids="with_warnings={}".format) +def test_run_manifest(with_warnings): # Set up a valid manifest. config = _options.options.namespace(stop_on_first_error=False) sessions_ = [ @@ -200,6 +213,12 @@ def test_run_manifest(): mock_session.execute.return_value = sessions.Result( session=mock_session, status=sessions.Status.SUCCESS ) + # we need the should_warn attribute, add some func + if with_warnings: + mock_session.name = "hello" + mock_session.func = session_func_venv_pythons_warning + else: + mock_session.func = session_func # Run the manifest. results = tasks.run_manifest(manifest, global_config=config) @@ -228,6 +247,8 @@ def test_run_manifest_abort_on_first_failure(): mock_session.execute.return_value = sessions.Result( session=mock_session, status=sessions.Status.FAILED ) + # we need the should_warn attribute, add some func + mock_session.func = session_func # Run the manifest. results = tasks.run_manifest(manifest, global_config=config)