From 552c5068d451af56ead3624fe747dc9e4677521a Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Tue, 10 Sep 2024 00:08:38 -0400 Subject: [PATCH] feat: install_and_run_script (PEP 721) Signed-off-by: Henry Schreiner --- docs/tutorial.rst | 9 ++++++++ nox/sessions.py | 35 +++++++++++++++++++++++++++++++ tests/resources/pep721example1.py | 7 +++++++ tests/test_sessions.py | 12 +++++++++++ 4 files changed, 63 insertions(+) create mode 100644 tests/resources/pep721example1.py diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 75677dd2..52def2b1 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -208,6 +208,15 @@ You can make a session for it like this: session.install(*requirements) session.run("peps.py") +This is a common structure for scripts following this PEP, so a helper for it +is provided: + +.. code-block:: python + + @nox.session + def peps(session): + session.install_and_run_script("peps.py") + Running commands ---------------- diff --git a/nox/sessions.py b/nox/sessions.py index a9b361d2..5bf3eab6 100644 --- a/nox/sessions.py +++ b/nox/sessions.py @@ -244,6 +244,41 @@ def interactive(self) -> bool: """Returns True if Nox is being run in an interactive session or False otherwise.""" return not self._runner.global_config.non_interactive and sys.stdin.isatty() + def install_and_run_script( + self, + script: str | os.PathLike[str], + *args: str | os.PathLike[str], + env: Mapping[str, str | None] | None = None, + include_outer_env: bool = True, + silent: bool = False, + success_codes: Iterable[int] | None = None, + log: bool = True, + stdout: int | IO[str] | None = None, + stderr: int | IO[str] | None = subprocess.STDOUT, + interrupt_timeout: float | None = DEFAULT_INTERRUPT_TIMEOUT, + terminate_timeout: float | None = DEFAULT_TERMINATE_TIMEOUT, + ) -> Any | None: + """ + Install dependencies and run a Python script. + """ + deps = (nox.project.load_toml(script) or {}).get("dependencies", []) + self.install(*deps) + + return self.run( + "python", + script, + *args, + env=env, + include_outer_env=include_outer_env, + silent=silent, + success_codes=success_codes, + external=None, + stdout=stdout, + stderr=stderr, + interrupt_timeout=interrupt_timeout, + terminate_timeout=terminate_timeout, + ) + @property def invoked_from(self) -> str: """The directory that Nox was originally invoked from. diff --git a/tests/resources/pep721example1.py b/tests/resources/pep721example1.py new file mode 100644 index 00000000..cbf36f41 --- /dev/null +++ b/tests/resources/pep721example1.py @@ -0,0 +1,7 @@ +# /// script +# dependencies = ["rich"] +# /// + +import rich + +rich.print("[blue]This worked!") diff --git a/tests/test_sessions.py b/tests/test_sessions.py index 90b0caf6..19827d64 100644 --- a/tests/test_sessions.py +++ b/tests/test_sessions.py @@ -39,6 +39,8 @@ HAS_CONDA = shutil.which("conda") is not None has_conda = pytest.mark.skipif(not HAS_CONDA, reason="Missing conda command.") +DIR = Path(__file__).parent.resolve() + def run_with_defaults(**kwargs): return { @@ -298,6 +300,16 @@ def test_run_error(self): with pytest.raises(nox.command.CommandFailed): session.run(sys.executable, "-c", "import sys; sys.exit(1)") + def test_run_install_script(self): + session, _ = self.make_session_and_runner() + + with mock.patch.object(nox.command, "run") as run: + session.install_and_run_script(DIR / "resources/pep721example1.py") + + assert len(run.call_args_list) == 2 + assert "rich" in run.call_args_list[0][0][0] + assert DIR / "resources/pep721example1.py" in run.call_args_list[1][0][0] + def test_run_overly_env(self): session, runner = self.make_session_and_runner() runner.venv.env["A"] = "1"