diff --git a/docs/index.rst b/docs/index.rst index 5da9abb6..0071cbf6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -50,7 +50,7 @@ Projects that use Nox Nox is lucky to have several wonderful projects that use it and provide feedback and contributions. -- `Bezier `__ +- `Bézier `__ - `gapic-generator-python `__ - `gdbgui `__ - `Google Assistant SDK `__ diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 47a7ae81..8be056dc 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -124,6 +124,30 @@ If your project is a Python package and you want to install it: session.install(".") ... +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 + +.. code-block:: python + + @nox.session + def tests(session): + ... + session.run_always( + "cmake", "-DCMAKE_BUILD_TYPE=Debug", + "-S", libfoo_src_dir, + "-B", build_dir, + external=True, + ) + session.run_always( + "cmake", + "--build", build_dir, + "--config", "Debug", + "--target", "install", + external=True, + ) + session.install(".") + ... Running commands ---------------- diff --git a/nox/sessions.py b/nox/sessions.py index b687898e..bac226f1 100644 --- a/nox/sessions.py +++ b/nox/sessions.py @@ -225,6 +225,34 @@ def run( return self._run(*args, env=env, **kwargs) + def run_always( + self, *args: str, env: Mapping[str, str] = None, **kwargs: Any + ) -> Optional[Any]: + """Run a command **always**. + + This is a variant of :meth:`run` that runs in all cases, including in + the presence of ``--install-only``. + + :param env: A dictionary of environment variables to expose to the + command. By default, all environment variables are passed. + :type env: dict or None + :param bool silent: Silence command output, unless the command fails. + ``False`` by default. + :param success_codes: A list of return codes that are considered + successful. By default, only ``0`` is considered success. + :type success_codes: list, tuple, or None + :param external: If False (the default) then programs not in the + virtualenv path will cause a warning. If True, no warning will be + emitted. These warnings can be turned into errors using + ``--error-on-external-run``. This has no effect for sessions that + do not have a virtualenv. + :type external: bool + """ + if not args: + raise ValueError("At least one argument required to run_always().") + + return self._run(*args, env=env, **kwargs) + def _run(self, *args: str, env: Mapping[str, str] = None, **kwargs: Any) -> Any: """Like run(), except that it runs even if --install-only is provided.""" # Legacy support - run a function given. diff --git a/tests/test_sessions.py b/tests/test_sessions.py index 7d88de52..367314b9 100644 --- a/tests/test_sessions.py +++ b/tests/test_sessions.py @@ -14,6 +14,7 @@ import argparse import logging +import operator import os import sys import tempfile @@ -151,7 +152,7 @@ def test_run_bad_args(self): def test_run_with_func(self): session, _ = self.make_session_and_runner() - assert session.run(lambda a, b: a + b, 1, 2) == 3 + assert session.run(operator.add, 1, 2) == 3 def test_run_with_func_error(self): session, _ = self.make_session_and_runner() @@ -168,7 +169,7 @@ def test_run_install_only(self, caplog): runner.global_config.install_only = True with mock.patch.object(nox.command, "run") as run: - session.run("spam", "eggs") + assert session.run("spam", "eggs") is None run.assert_not_called() @@ -262,6 +263,26 @@ 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): + session, _ = self.make_session_and_runner() + + with pytest.raises(ValueError) as exc_info: + session.run_always() + + exc_args = exc_info.value.args + assert exc_args == ("At least one argument required to run_always().",) + + def test_run_always_success(self): + session, _ = self.make_session_and_runner() + + assert session.run_always(operator.add, 1300, 37) == 1337 + + def test_run_always_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 + def test_conda_install_bad_args(self): session, runner = self.make_session_and_runner() runner.venv = mock.create_autospec(nox.virtualenv.CondaEnv)