Repository: theacodes/nox (python)

Issue:
creation of conda environment fails when venv_params is not None
**Describe the bug**
If venv_params is used when creating a conda environment, then the value of venv_params gets inserted into the conda command line argument list at the wrong location. The current code attempts to add pip to the list of packages to be installed and places 'pip' before venv_params. However, the `conda create ...` command expects all options to be listed before package names, so if venv_params contains an optional switch, then an invalid command is generated.  This causes the `conda create ...` command to fail and not create the desired environment.

**How to reproduce**
```
$ uname -a
Darwin myhostname 19.6.0 Darwin Kernel Version 19.6.0: Tue Jan 12 22:13:05 PST 2021; root:xnu-6153.141.16~1/RELEASE_X86_64 x86_64
$ python --version
Python 3.8.5
$ nox --version
2020.12.31
$ cat noxfile.py 
import nox

@nox.session(venv_backend='conda', venv_params=['--channel=conda-forge'])
def should_work(session):
    session.conda_install('flake8')
$ nox
nox > Running session should_work
nox > Creating conda env in .nox/should_work with python
nox > Command conda create --yes --prefix /Users/myuserid/myproject/.nox/should_work pip --channel=conda-forge python failed with exit code 2:
usage: conda [-h] [-V] command ...
conda: error: unrecognized arguments: python
nox > Session should_work failed.
$ 
```

**Expected behavior**
The command executed should be:
```
conda create --yes --prefix /Users/myuserid/myproject/.nox/should_work --channel=conda-forge pip python
```
And then the conda environment is successfully created.


Pull request:
fix position of venv_params within conda create
Fixes #419 by moving 'pip' to after venv_params

Patch:
diff --git a/nox/virtualenv.py b/nox/virtualenv.py
index 0e3bbca..78322e0 100644
--- a/nox/virtualenv.py
+++ b/nox/virtualenv.py
@@ -226,18 +226,13 @@ class CondaEnv(ProcessEnv):
             )
             return False
 
-        cmd = [
-            "conda",
-            "create",
-            "--yes",
-            "--prefix",
-            self.location,
-            # Ensure the pip package is installed.
-            "pip",
-        ]
+        cmd = ["conda", "create", "--yes", "--prefix", self.location]
 
         cmd.extend(self.venv_params)
 
+        # Ensure the pip package is installed.
+        cmd.append("pip")
+
         if self.interpreter:
             python_dep = "python={}".format(self.interpreter)
         else:
@@ -313,12 +308,19 @@ class VirtualEnv(ProcessEnv):
         """Deletes any existing virtual environment"""
         if os.path.exists(self.location):
             if self.reuse_existing:
+                self._check_reused_environment()
                 return False
             else:
                 shutil.rmtree(self.location)
 
         return True
 
+    def _check_reused_environment(self) -> None:
+        """Check if reused environment type is the same."""
+        with open(os.path.join(self.location, "pyvenv.cfg")) as fp:
+            old_env = "virtualenv" if "virtualenv" in fp.read() else "venv"
+        assert old_env == self.venv_or_virtualenv
+
     @property
     def _resolved_interpreter(self) -> str:
         """Return the interpreter, appropriately resolved for the platform.
@@ -394,12 +396,25 @@ class VirtualEnv(ProcessEnv):
     def create(self) -> bool:
         """Create the virtualenv or venv."""
         if not self._clean_location():
-            logger.debug(
-                "Re-using existing virtual environment at {}.".format(
-                    self.location_name
-                )
+            original = nox.command.run(
+                [self._resolved_interpreter, "-c", "import sys; print(sys.prefix)"],
+                silent=True,
             )
-            return False
+            created = nox.command.run(
+                [
+                    os.path.join(self.location, "bin", "python"),
+                    "-c",
+                    "import sys; print(sys.real_prefix)",
+                ],
+                silent=True,
+            )
+            if original == created:
+                logger.debug(
+                    "Re-using existing virtual environment at {}.".format(
+                        self.location_name
+                    )
+                )
+                return False
 
         if self.venv_or_virtualenv == "virtualenv":
             cmd = [sys.executable, "-m", "virtualenv", self.location]


Test patch:
diff --git a/tests/test_virtualenv.py b/tests/test_virtualenv.py
index 75598d6..cb76a87 100644
--- a/tests/test_virtualenv.py
+++ b/tests/test_virtualenv.py
@@ -181,6 +181,18 @@ def test_condaenv_create(make_conda):
     assert dir_.join("test.txt").check()
 
 
+@pytest.mark.skipif(not HAS_CONDA, reason="Missing conda command.")
+def test_condaenv_create_with_params(make_conda):
+    venv, dir_ = make_conda(venv_params=["--verbose"])
+    venv.create()
+    if IS_WINDOWS:
+        assert dir_.join("python.exe").check()
+        assert dir_.join("Scripts", "pip.exe").check()
+    else:
+        assert dir_.join("bin", "python").check()
+        assert dir_.join("bin", "pip").check()
+
+
 @pytest.mark.skipif(not HAS_CONDA, reason="Missing conda command.")
 def test_condaenv_create_interpreter(make_conda):
     venv, dir_ = make_conda(interpreter="3.7")
@@ -244,6 +256,9 @@ def test__clean_location(monkeypatch, make_one):
 
     # Don't re-use existing, but doesn't currently exist.
     # Should return True indicating that the venv needs to be created.
+    monkeypatch.setattr(
+        nox.virtualenv.VirtualEnv, "_check_reused_environment", mock.MagicMock()
+    )
     monkeypatch.delattr(nox.virtualenv.shutil, "rmtree")
     assert not dir_.check()
     assert venv._clean_location()
@@ -289,7 +304,7 @@ def test_bin_windows(make_one):
     assert dir_.join("Scripts").strpath == venv.bin
 
 
-def test_create(make_one):
+def test_create(monkeypatch, make_one):
     venv, dir_ = make_one()
     venv.create()
 
@@ -312,10 +327,26 @@ def test_create(make_one):
     dir_.ensure("test.txt")
     assert dir_.join("test.txt").check()
     venv.reuse_existing = True
+    monkeypatch.setattr(nox.virtualenv.nox.command, "run", mock.MagicMock())
     venv.create()
     assert dir_.join("test.txt").check()
 
 
+def test_create_check_interpreter(make_one, monkeypatch, tmpdir):
+    cmd_mock = mock.MagicMock(
+        side_effect=["python-1", "python-1", "python-2", "python-3", "python-4"]
+    )
+    monkeypatch.setattr(nox.virtualenv.nox.command, "run", cmd_mock)
+    test_dir = tmpdir.mkdir("pytest")
+    fp = test_dir.join("pyvenv.cfg")
+    fp.write("virtualenv")
+    venv, dir_ = make_one()
+    venv.reuse_existing = True
+    venv.location = test_dir.strpath
+    assert not venv.create()
+    assert venv.create()
+
+
 def test_create_venv_backend(make_one):
     venv, dir_ = make_one(venv=True)
     venv.create()