Repository: wntrblm/nox (language: python)

Issue:
Support indicating that a command *is* an install command
### How would this feature be useful?

`nox` supports `--no-install` and `-R` options for not running installation commands, usually for use when reusing existing environments, and to speed up development where the install process is often the long pole.

But there doesn't seem to be a good way to indicate when something run via `session.run` *is* an install command -- specifically, if you're installing something *not* via `pip` or `conda` etc, which you may want to skip in the same scenarios as the above.

Concretely -- given a Javascript project, one might run `(p)npm install` to install all the project dependencies, followed by some test runner, e.g. `jest`, all under a noxenv called `ui-tests`.

In this scenario it would be nice if `nox -s ui-tests -R` was able to *not* run the install command, but the existing `-R` of course has no way to know which command to skip.

Even doing this externally (in the noxfile) seems nontrivial, as it seems the way `session.install` does this is by accessing private internals, e.g.:

https://github.com/wntrblm/nox/blob/5c82dc553bd04ee017784fc16193da0b35a44ab6/nox/sessions.py#L629-L630

### Describe the solution you'd like

Add a `session.run_install(...)` command which runs a subprocess but assumes that subprocess is an installation command which will be skipped when using `-R` / `--no-install`.

Refactoring `session.install` and `session.conda_install` to call this to do their `no_install` check seems also like a nice minor internal refactor.

### Describe alternatives you've considered

I personally don't like boolean flags (as a rule), but `session.run(..., is_install=True)` is an alternative for those who are less averse.

A further alternative is to make more things public API (e.g. whether no_install is active) so that someone can do this themselves in their noxfile, but given that `nox` itself has 2 places where it itself wants this, it seems like something generally useful to me.

### Anything else?

A concrete `noxfile` where I wanted this functionality is [here](https://github.com/bowtie-json-schema/bowtie/blob/5cdb622567b556815a54db59b4276e82e89aa1ce/noxfile.py#L333-L379).


Discussion / hints:
This already exists, but for historic reasons it's called session.run_always. It keeps tripping up people. IMO we should alias this to session.run_install, keeping the old name for compatibility.
Huh, I swear I saw `run_always` and for some reason thought it still wasn't the right thing, but looking at it again it does indeed look like you're right!

I understand the history I think, there was only a `--install-only` original right, and so this was "run in that case too" hence the "always" -- but yeah the name seems really unfortunate now.
So `.run_always` does not run if installs are skipped, only `.run` is run?
Yup. On `main` of my repo all works as expected using it, though I'm going to likely alias it directly in my noxfile to remove the confusion.


Fix patch:
diff --git a/docs/tutorial.rst b/docs/tutorial.rst
index ac76a543..4b3131f1 100644
--- a/docs/tutorial.rst
+++ b/docs/tutorial.rst
@@ -143,20 +143,20 @@ If your project is a Python package and you want to install it:
 
 In some cases such as Python binary extensions, your package may depend on
 code compiled outside of the Python ecosystem. To make sure a low-level
-dependency (e.g. ``libfoo``) is available during installation
+dependency (e.g. ``libfoo``) is available during installation:
 
 .. code-block:: python
 
     @nox.session
     def tests(session):
         ...
-        session.run_always(
+        session.run_install(
             "cmake", "-DCMAKE_BUILD_TYPE=Debug",
             "-S", libfoo_src_dir,
             "-B", build_dir,
             external=True,
         )
-        session.run_always(
+        session.run_install(
             "cmake",
             "--build", build_dir,
             "--config", "Debug",
@@ -166,6 +166,9 @@ dependency (e.g. ``libfoo``) is available during installation
         session.install(".")
         ...
 
+These commands will run even if you are only installing, and will not run if
+the environment is being reused without reinstallation.
+
 Running commands
 ----------------
 
diff --git a/docs/usage.rst b/docs/usage.rst
index 2864e35e..bc237610 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -186,7 +186,7 @@ The ``--no-install`` option causes the following session methods to return early
 
 - :func:`session.install <nox.sessions.Session.install>`
 - :func:`session.conda_install <nox.sessions.Session.conda_install>`
-- :func:`session.run_always <nox.sessions.Session.run_always>`
+- :func:`session.run_install <nox.sessions.Session.run_install>`
 
 This option has no effect if the virtualenv is not being reused.
 
diff --git a/nox/_options.py b/nox/_options.py
index 24987b44..02c1b977 100644
--- a/nox/_options.py
+++ b/nox/_options.py
@@ -484,7 +484,7 @@ def _session_completer(
         action="store_true",
         help=(
             "Skip invocations of session methods for installing packages"
-            " (session.install, session.conda_install, session.run_always)"
+            " (session.install, session.conda_install, session.run_install)"
             " when a virtualenv is being reused."
         ),
     ),
diff --git a/nox/sessions.py b/nox/sessions.py
index bab59d5b..02e1e69f 100644
--- a/nox/sessions.py
+++ b/nox/sessions.py
@@ -390,18 +390,20 @@ def run(
             **kwargs,
         )
 
-    def run_always(
+    def run_install(
         self,
         *args: str | os.PathLike[str],
         env: Mapping[str, str] | None = None,
         include_outer_env: bool = True,
         **kwargs: Any,
     ) -> Any | None:
-        """Run a command **always**.
+        """Run a command in the install step.
 
         This is a variant of :meth:`run` that runs even in the presence of
         ``--install-only``. This method returns early if ``--no-install`` is
-        specified and the virtualenv is being reused.
+        specified and the virtualenv is being reused. (In nox 2023.04.22 and
+        earlier, this was called ``run_always``, and that continues to be
+        available as an alias.)
 
         Here are some cases where this method is useful:
 
@@ -447,7 +449,7 @@ def run_always(
             return None
 
         if not args:
-            raise ValueError("At least one argument required to run_always().")
+            raise ValueError("At least one argument required to run_install().")
 
         return self._run(
             *args,
@@ -456,6 +458,22 @@ def run_always(
             **kwargs,
         )
 
+    def run_always(
+        self,
+        *args: str | os.PathLike[str],
+        env: Mapping[str, str] | None = None,
+        include_outer_env: bool = True,
+        **kwargs: Any,
+    ) -> Any | None:
+        """This is an alias to ``run_install``, which better describes the use case.
+
+        :meta private:
+        """
+
+        return self.run_install(
+            *args, env=env, include_outer_env=include_outer_env, **kwargs
+        )
+
     def _run(
         self,
         *args: str | os.PathLike[str],


Test patch:
diff --git a/tests/test_sessions.py b/tests/test_sessions.py
index 00a912c6..1926a332 100644
--- a/tests/test_sessions.py
+++ b/tests/test_sessions.py
@@ -358,25 +358,25 @@ def test_run_external_with_error_on_external_run_condaenv(self):
         with pytest.raises(nox.command.CommandFailed, match="External"):
             session.run(sys.executable, "--version")
 
-    def test_run_always_bad_args(self):
+    def test_run_install_bad_args(self):
         session, _ = self.make_session_and_runner()
 
         with pytest.raises(ValueError) as exc_info:
-            session.run_always()
+            session.run_install()
 
         exc_args = exc_info.value.args
-        assert exc_args == ("At least one argument required to run_always().",)
+        assert exc_args == ("At least one argument required to run_install().",)
 
-    def test_run_always_success(self):
+    def test_run_install_success(self):
         session, _ = self.make_session_and_runner()
 
-        assert session.run_always(operator.add, 1300, 37) == 1337
+        assert session.run_install(operator.add, 1300, 37) == 1337
 
-    def test_run_always_install_only(self, caplog):
+    def test_run_install_install_only(self, caplog):
         session, runner = self.make_session_and_runner()
         runner.global_config.install_only = True
 
-        assert session.run_always(operator.add, 23, 19) == 42
+        assert session.run_install(operator.add, 23, 19) == 42
 
     @pytest.mark.parametrize(
         (
@@ -437,13 +437,17 @@ def test_run_shutdown_process_timeouts(
             (False, False, True),
         ],
     )
-    def test_run_always_no_install(self, no_install, reused, run_called):
+    @pytest.mark.parametrize("run_install_func", ["run_always", "run_install"])
+    def test_run_install_no_install(
+        self, no_install, reused, run_called, run_install_func
+    ):
         session, runner = self.make_session_and_runner()
         runner.global_config.no_install = no_install
         runner.venv._reused = reused
 
         with mock.patch.object(nox.command, "run") as run:
-            session.run_always("python", "-m", "pip", "install", "requests")
+            run_install = getattr(session, run_install_func)
+            run_install("python", "-m", "pip", "install", "requests")
 
         assert run.called is run_called