diff --git a/nox/command.py b/nox/command.py index 1034366a..2a0a5009 100644 --- a/nox/command.py +++ b/nox/command.py @@ -52,16 +52,16 @@ def which(program: str, paths: list[str] | None) -> str: raise CommandFailed(f"Program {program} not found") -def _clean_env(env: dict[str, str] | None) -> dict[str, str] | None: - if env is None: - return None - +def _clean_env(env: dict[str, str] | None) -> dict[str, str]: clean_env = {} # Ensure systemroot is passed down, otherwise Windows will explode. - clean_env["SYSTEMROOT"] = os.environ.get("SYSTEMROOT", "") + if sys.platform == "win32": + clean_env["SYSTEMROOT"] = os.environ.get("SYSTEMROOT", "") + + if env is not None: + clean_env.update(env) - clean_env.update(env) return clean_env diff --git a/nox/sessions.py b/nox/sessions.py index 097dbe68..414e9943 100644 --- a/nox/sessions.py +++ b/nox/sessions.py @@ -272,7 +272,11 @@ def _run_func( raise nox.command.CommandFailed() from e def run( - self, *args: str, env: Mapping[str, str] | None = None, **kwargs: Any + self, + *args: str, + env: dict[str, str] | None = None, + include_outer_env: bool = True, + **kwargs: Any, ) -> Any | None: """Run a command. @@ -339,6 +343,10 @@ def run( :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 include_outer_env: Boolean parameter that determines if the + environment variables from the nox invocation environment should + be passed to the command. ``True`` by default. + :type include_outer_env: bool :param bool silent: Silence command output, unless the command fails. If ``True``, returns the command output (unless the command fails). ``False`` by default. @@ -375,10 +383,19 @@ def run( logger.info(f"Skipping {args[0]} run, as --install-only is set.") return None - return self._run(*args, env=env, **kwargs) + return self._run( + *args, + env=env, + include_outer_env=include_outer_env, + **kwargs, + ) def run_always( - self, *args: str, env: Mapping[str, str] | None = None, **kwargs: Any + self, + *args: str, + env: dict[str, str] | None = None, + include_outer_env: bool = True, + **kwargs: Any, ) -> Any | None: """Run a command **always**. @@ -396,6 +413,10 @@ def run_always( :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 include_outer_env: Boolean parameter that determines if the + environment variables from the nox invocation environment should + be passed to the command. ``True`` by default. + :type include_outer_env: bool :param bool silent: Silence command output, unless the command fails. ``False`` by default. :param success_codes: A list of return codes that are considered @@ -428,10 +449,19 @@ def run_always( if not args: raise ValueError("At least one argument required to run_always().") - return self._run(*args, env=env, **kwargs) + return self._run( + *args, + env=env, + include_outer_env=include_outer_env, + **kwargs, + ) def _run( - self, *args: str, env: Mapping[str, str] | None = None, **kwargs: Any + self, + *args: str, + env: dict[str, str] | None = None, + include_outer_env: bool = True, + **kwargs: Any, ) -> Any: """Like run(), except that it runs even if --install-only is provided.""" # Legacy support - run a function given. @@ -439,12 +469,11 @@ def _run( return self._run_func(args[0], args[1:], kwargs) # Combine the env argument with our virtualenv's env vars. - if env is not None: + if include_outer_env: overlay_env = env env = self.env.copy() - env.update(overlay_env) - else: - env = self.env + if overlay_env is not None: + env.update(overlay_env) # If --error-on-external-run is specified, error on external programs. if self._runner.global_config.error_on_external_run: diff --git a/tests/test_command.py b/tests/test_command.py index 154ec0b8..8ab48b9f 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -144,6 +144,7 @@ def test_run_env_unicode(): assert "123" in result +@mock.patch("sys.platform", "win32") def test_run_env_systemroot(): systemroot = os.environ.setdefault("SYSTEMROOT", "sigil") @@ -181,7 +182,7 @@ def test_run_path_existent(tmp_path: Path): with mock.patch("nox.command.popen") as mock_command: mock_command.return_value = (0, "") nox.command.run([executable_name], silent=True, paths=[str(tmp_path)]) - mock_command.assert_called_with([str(executable)], env=None, silent=True) + mock_command.assert_called_with([str(executable)], env=mock.ANY, silent=True) def test_run_external_warns(tmpdir, caplog): diff --git a/tests/test_sessions.py b/tests/test_sessions.py index 5f1422f1..b601a8c5 100644 --- a/tests/test_sessions.py +++ b/tests/test_sessions.py @@ -273,6 +273,37 @@ def test_run_overly_env(self): ) assert result.strip() == "1 3" + def test_by_default_all_invocation_env_vars_are_passed(self): + session, runner = self.make_session_and_runner() + runner.venv.env["I_SHOULD_BE_INCLUDED"] = "happy" + runner.venv.env["I_SHOULD_BE_INCLUDED_TOO"] = "happier" + runner.venv.env["EVERYONE_SHOULD_BE_INCLUDED_TOO"] = "happiest" + result = session.run( + sys.executable, + "-c", + "import os; print(os.environ)", + silent=True, + ) + assert "happy" in result + assert "happier" in result + assert "happiest" in result + + def test_no_included_invocation_env_vars_are_passed(self): + session, runner = self.make_session_and_runner() + runner.venv.env["I_SHOULD_NOT_BE_INCLUDED"] = "sad" + runner.venv.env["AND_NEITHER_SHOULD_I"] = "unhappy" + result = session.run( + sys.executable, + "-c", + "import os; print(os.environ)", + env={"I_SHOULD_BE_INCLUDED": "happy"}, + include_outer_env=False, + silent=True, + ) + assert "sad" not in result + assert "unhappy" not in result + assert "happy" in result + def test_run_external_not_a_virtualenv(self): # Non-virtualenv sessions should always allow external programs. session, runner = self.make_session_and_runner()