Repository: wntrblm/nox
Pull request #912 (2025-01-21 17:36:45)

refactor: pull out env creation into helper method
Helping with #881 which is tripping up over the test mocking. This pulled out this function to make it reusable and also improves the test mocking.



Patch:
diff --git a/nox/sessions.py b/nox/sessions.py
index 07a7881..589910a 100644
--- a/nox/sessions.py
+++ b/nox/sessions.py
@@ -618,14 +618,7 @@ class Session:
             args = (nox.virtualenv.UV, *args[1:])
 
         # Combine the env argument with our virtualenv's env vars.
-        env = env or {}
-        env = {**self.env, **env}
-        if include_outer_env:
-            env = {**os.environ, **env}
-        if self.virtualenv.bin_paths:
-            env["PATH"] = os.pathsep.join(
-                [*self.virtualenv.bin_paths, env.get("PATH") or ""]
-            )
+        env = self.virtualenv._get_env(env or {}, include_outer_env=include_outer_env)
 
         # If --error-on-external-run is specified, error on external programs.
         if self._runner.global_config.error_on_external_run and external is None:
@@ -821,7 +814,7 @@ class Session:
         if not isinstance(
             venv, (CondaEnv, VirtualEnv, PassthroughEnv)
         ):  # pragma: no cover
-            msg = "A session without a virtualenv can not install dependencies."
+            msg = f"A session without a virtualenv (got {venv!r}) can not install dependencies."
             raise TypeError(msg)
         if isinstance(venv, PassthroughEnv):
             if self._runner.global_config.no_install:
diff --git a/nox/virtualenv.py b/nox/virtualenv.py
index 38fc1bd..2a8d8ba 100644
--- a/nox/virtualenv.py
+++ b/nox/virtualenv.py
@@ -191,6 +191,27 @@ class ProcessEnv(abc.ABC):
         Returns the string used to select this environment.
         """
 
+    def _get_env(
+        self,
+        /,
+        env: Mapping[str, str | None],
+        *,
+        include_outer_env: bool = True,
+    ) -> dict[str, str | None]:
+        """
+        Get the computed environment, with bin paths added.  You can request
+        the outer environment be excluded. The initial env can be empty.
+        """
+
+        computed_env = {**self.env, **env}
+        if include_outer_env:
+            computed_env = {**os.environ, **computed_env}
+        if self.bin_paths:
+            computed_env["PATH"] = os.pathsep.join(
+                [*self.bin_paths, computed_env.get("PATH") or ""]
+            )
+        return computed_env
+
 
 def locate_via_py(version: str) -> str | None:
     """Find the Python executable using the Windows Launcher.


Test patch:
diff --git a/tests/test_sessions.py b/tests/test_sessions.py
index b875527..abda550 100644
--- a/tests/test_sessions.py
+++ b/tests/test_sessions.py
@@ -105,6 +105,19 @@ def test__normalize_path_give_up() -> None:
     assert "any-path" in norm_path
 
 
+class FakeEnv(mock.MagicMock):
+    _get_env = nox.virtualenv.VirtualEnv._get_env
+
+
+def make_fake_env(venv_backend: str = "venv", **kwargs: Any) -> FakeEnv:
+    return FakeEnv(
+        spec=nox.virtualenv.VirtualEnv,
+        env={},
+        venv_backend=venv_backend,
+        **kwargs,
+    )
+
+
 class TestSession:
     def make_session_and_runner(
         self,
@@ -122,11 +135,8 @@ class TestSession:
             ),
             manifest=mock.create_autospec(nox.manifest.Manifest),
         )
-        runner.venv = mock.create_autospec(nox.virtualenv.VirtualEnv)
-        assert runner.venv
-        runner.venv.env = {}
-        runner.venv.bin_paths = ["/no/bin/for/you"]  # type: ignore[misc]
-        runner.venv.venv_backend = "venv"  # type: ignore[misc]
+        runner.venv = make_fake_env(bin_paths=["/no/bin/for/you"])
+        assert isinstance(runner.venv, nox.virtualenv.VirtualEnv)
         return nox.sessions.Session(runner=runner), runner
 
     def test_create_tmp(self) -> None:
@@ -750,10 +760,8 @@ class TestSession:
             global_config=_options.options.namespace(posargs=[]),
             manifest=mock.create_autospec(nox.manifest.Manifest),
         )
-        runner.venv = mock.create_autospec(nox.virtualenv.VirtualEnv)
-        assert runner.venv
-        runner.venv.env = {}
-        runner.venv.venv_backend = "venv"  # type: ignore[misc]
+        runner.venv = make_fake_env()
+        assert isinstance(runner.venv, nox.virtualenv.VirtualEnv)
 
         class SessionNoSlots(nox.sessions.Session):
             pass
@@ -782,10 +790,8 @@ class TestSession:
             global_config=_options.options.namespace(posargs=[]),
             manifest=mock.create_autospec(nox.manifest.Manifest),
         )
-        runner.venv = mock.create_autospec(nox.virtualenv.VirtualEnv)
-        assert runner.venv
-        runner.venv.env = {}
-        runner.venv.venv_backend = "venv"  # type: ignore[misc]
+        runner.venv = make_fake_env()
+        assert isinstance(runner.venv, nox.virtualenv.VirtualEnv)
 
         class SessionNoSlots(nox.sessions.Session):
             pass
@@ -936,10 +942,9 @@ class TestSession:
             global_config=_options.options.namespace(posargs=[]),
             manifest=mock.create_autospec(nox.manifest.Manifest),
         )
-        runner.venv = mock.create_autospec(nox.virtualenv.VirtualEnv)
-        assert runner.venv
-        runner.venv.env = {}
-        runner.venv.venv_backend = "uv"  # type: ignore[misc]
+        runner.venv = make_fake_env(venv_backend="uv")
+        assert isinstance(runner.venv, nox.virtualenv.VirtualEnv)
+        assert runner.venv.venv_backend == "uv"
 
         class SessionNoSlots(nox.sessions.Session):
             pass
@@ -965,10 +970,9 @@ class TestSession:
             global_config=_options.options.namespace(posargs=[]),
             manifest=mock.create_autospec(nox.manifest.Manifest),
         )
-        runner.venv = mock.create_autospec(nox.virtualenv.VirtualEnv)
-        assert runner.venv
-        runner.venv.env = {}
-        runner.venv.venv_backend = "uv"  # type: ignore[misc]
+        runner.venv = make_fake_env(venv_backend="uv")
+        assert isinstance(runner.venv, nox.virtualenv.VirtualEnv)
+        assert runner.venv.venv_backend == "uv"
 
         class SessionNoSlots(nox.sessions.Session):
             pass
@@ -1241,9 +1245,8 @@ class TestSessionRunner:
     def make_runner_with_mock_venv(self) -> nox.sessions.SessionRunner:
         runner = self.make_runner()
         runner._create_venv = mock.Mock()  # type: ignore[method-assign]
-        runner.venv = mock.create_autospec(nox.virtualenv.VirtualEnv)
-        assert runner.venv
-        runner.venv.env = {}
+        runner.venv = make_fake_env()
+        assert isinstance(runner.venv, nox.virtualenv.VirtualEnv)
         return runner
 
     def test_execute_noop_success(self, caplog: pytest.LogCaptureFixture) -> None: