Repository: wntrblm/nox (python)

Issue:
Error when running a uv backend session with active conda environment
### Current Behavior

If you have a conda environment active and try to run a session with using uv as a backend and `session.install`, uv throws an error because it detects both an active conda environment and an active virtual environment:

```
nox > Command uv pip install ruff failed with exit code 2:
error: Both VIRTUAL_ENV and CONDA_PREFIX are set. Please unset one of them.
```

### Expected Behavior

nox should clear both `VIRTUAL_ENV` and `CONDA_PREFIX` environment variables before trying to run a session. 

### Steps To Reproduce

```python
# noxfile.py
import nox


@nox.session(venv_backend="uv")
def lint(session):
    session.install("ruff")
```

```bash
# No conda environment active, no problem
❯ nox -s lint
nox > Running session lint
nox > Creating virtual environment (uv) using python in .nox/lint
nox > uv pip install ruff
nox > Session lint was successful.

# Activate a conda environment, try to run
❯ mamba activate my-conda-env
❯ nox -s lint
nox > Running session lint
nox > Creating virtual environment (uv) using python in .nox/lint
nox > uv pip install ruff
nox > Command uv pip install ruff failed with exit code 2:
error: Both VIRTUAL_ENV and CONDA_PREFIX are set. Please unset one of them.
nox > Session lint failed.
```

### Environment

```Markdown
- OS: macOS
- Python: 3.12.1
- Nox: 2024.3.2
```


### Anything else?

_No response_

Pull request:
fix: avoid mixing venv and conda from environment
Fix #801.

Needs a test were we manually set one variable and make sure it isn't present. It might be the parent `run` overrides it.

I'd like to allow this at the user level, too; setting `env={"UNSET_ME": None}` would be a good way to provide an "unset" list. You can set to an empty string, but that's not always the same as not having the variable in the first place.


Patch:
diff --git a/docs/usage.rst b/docs/usage.rst
index f29dc42..887b4d1 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -156,13 +156,6 @@ Note that using this option does not change the backend for sessions where ``ven
    as ``uv pip`` is used to install programs instead. If you need to manually
    interact with pip, you should install it with ``session.install("pip")``.
 
-.. warning::
-
-   Currently the ``uv`` backend requires the ``<program name> @ .`` syntax to
-   install a local folder in non-editable mode; it does not (yet) compute the
-   name from the install process like pip does if the name is omitted. Editable
-   installs do not require a name.
-
 Backends that could be missing (``uv``, ``conda``, and ``mamba``) can have a fallback using ``|``, such as ``uv|virtualenv`` or ``mamba|conda``. This will use the first item that is available on the users system.
 
 If you need to check to see which backend was selected, you can access it via
diff --git a/nox/virtualenv.py b/nox/virtualenv.py
index 64560b8..b561054 100644
--- a/nox/virtualenv.py
+++ b/nox/virtualenv.py
@@ -75,14 +75,18 @@ class ProcessEnv(abc.ABC):
     allowed_globals: ClassVar[tuple[Any, ...]] = ()
 
     def __init__(
-        self, bin_paths: None = None, env: Mapping[str, str] | None = None
+        self, bin_paths: None = None, env: Mapping[str, str | None] | None = None
     ) -> None:
         self._bin_paths = bin_paths
         self.env = os.environ.copy()
         self._reused = False
+        env = env or {}
 
-        if env is not None:
-            self.env.update(env)
+        for k, v in env.items():
+            if v is None:
+                self.env.pop(k, None)
+            else:
+                self.env[k] = v
 
         for key in _BLACKLISTED_ENV_VARS:
             self.env.pop(key, None)
@@ -249,7 +253,7 @@ class CondaEnv(ProcessEnv):
         self.reuse_existing = reuse_existing
         self.venv_params = venv_params or []
         self.conda_cmd = conda_cmd
-        super().__init__(env={"CONDA_PREFIX": self.location})
+        super().__init__(env={"CONDA_PREFIX": self.location, "VIRTUAL_ENV": None})
 
     def _clean_location(self) -> bool:
         """Deletes existing conda environment"""
@@ -379,7 +383,7 @@ class VirtualEnv(ProcessEnv):
         if venv_backend not in {"virtualenv", "venv", "uv"}:
             msg = f"venv_backend {venv_backend} not recognized"
             raise ValueError(msg)
-        super().__init__(env={"VIRTUAL_ENV": self.location})
+        super().__init__(env={"VIRTUAL_ENV": self.location, "CONDA_PREFIX": None})
 
     def _clean_location(self) -> bool:
         """Deletes any existing virtual environment"""


Test patch:
diff --git a/tests/test_virtualenv.py b/tests/test_virtualenv.py
index 12d41c5..340c2f9 100644
--- a/tests/test_virtualenv.py
+++ b/tests/test_virtualenv.py
@@ -364,9 +364,15 @@ def test_bin_windows(make_one):
 
 
 def test_create(monkeypatch, make_one):
+    monkeypatch.setenv("CONDA_PREFIX", "no-prefix-allowed")
+    monkeypatch.setenv("NOT_CONDA_PREFIX", "something-else")
+
     venv, dir_ = make_one()
     venv.create()
 
+    assert "CONDA_PREFIX" not in venv.env
+    assert "NOT_CONDA_PREFIX" in venv.env
+
     if IS_WINDOWS:
         assert dir_.join("Scripts", "python.exe").check()
         assert dir_.join("Scripts", "pip.exe").check()