Repository: wntrblm/nox (python)

Issue:
Allow combination of `--no-venv` and `--no-install` to suppress error on `session.install`
### How would this feature be useful?

For normal operation, we configure `nox` sessions to install their (potentially heavy) requirements to simplify dependency management. In CI, we build a container including all dependencies and run the same `nox` sessions within the container to verify that the container built correctly. In this case, the global (container) interpreter is pre-configured with all requirements, so using `--no-venv` is meaningfully faster.

For sessions that call `session.install`, `nox` rightfully throws an error that the global interpreter would be modified. Adding `--no-install` doesn't resolve the error, however, which would be a convenient way of retaining the ability to both have sessions manage their own setup and skip setup when it's not needed.

### Describe the solution you'd like

Suppress error on `session.install` when both `--no-venv` and `--no-install` are provided, since `session.install` will be a no-op with this combination of arguments.

### Describe alternatives you've considered

#167 and #710 could provide other ways to create this same behavior.

Currently, we use an environment variable to skip installation when in CI and prevent an error on `--no-venv`. This works, but isn't ideal.

### Anything else?

Potentially related to #693. I'm happy to dig in and pull together a PR if this feature is desired!

Pull request description:
fix: don't error if not installing to passthrough
If you have a passthrough environment and have passed `--no-install`, don't error because there's an `install` command that's not going to do anything.

Fixes #739. This fixes the underlying problem in #693 - that was asking for a way to detect `run("pip", "install", ...)` precisely because this bug kept `.install(...)` from being usable.


Patch:
diff --git a/nox/sessions.py b/nox/sessions.py
index 432db0c..57cc7bd 100644
--- a/nox/sessions.py
+++ b/nox/sessions.py
@@ -565,7 +565,7 @@ class Session:
         prefix_args: tuple[str, ...] = ()
         if isinstance(venv, CondaEnv):
             prefix_args = ("--prefix", venv.location)
-        elif not isinstance(venv, PassthroughEnv):  # pragma: no cover
+        elif not isinstance(venv, PassthroughEnv):
             raise ValueError(
                 "A session without a conda environment can not install dependencies"
                 " from conda."
@@ -574,7 +574,9 @@ class Session:
         if not args:
             raise ValueError("At least one argument required to install().")
 
-        if self._runner.global_config.no_install and venv._reused:
+        if self._runner.global_config.no_install and (
+            isinstance(venv, PassthroughEnv) or venv._reused
+        ):
             return
 
         # Escape args that should be (conda-specific; pip install does not need this)
@@ -648,6 +650,8 @@ class Session:
                 "A session without a virtualenv can not install dependencies."
             )
         if isinstance(venv, PassthroughEnv):
+            if self._runner.global_config.no_install:
+                return
             raise ValueError(
                 f"Session {self.name} does not have a virtual environment, so use of"
                 " session.install() is no longer allowed since it would modify the"


Test patch:
diff --git a/tests/test_sessions.py b/tests/test_sessions.py
index e6006ea..e720c61 100644
--- a/tests/test_sessions.py
+++ b/tests/test_sessions.py
@@ -374,6 +374,20 @@ class TestSession:
         exc_args = exc_info.value.args
         assert exc_args == ("At least one argument required to run_install().",)
 
+    def test_run_no_install_passthrough(self):
+        session, runner = self.make_session_and_runner()
+        runner.venv = nox.virtualenv.PassthroughEnv()
+        runner.global_config.no_install = True
+
+        session.install("numpy")
+        session.conda_install("numpy")
+
+    def test_run_no_conda_install(self):
+        session, runner = self.make_session_and_runner()
+
+        with pytest.raises(ValueError, match="A session without a conda"):
+            session.conda_install("numpy")
+
     def test_run_install_success(self):
         session, _ = self.make_session_and_runner()