diff --git a/nox/command.py b/nox/command.py index b1f544de..4882e3a4 100644 --- a/nox/command.py +++ b/nox/command.py @@ -55,17 +55,15 @@ def which(program: str | os.PathLike[str], paths: Sequence[str] | None) -> str: raise CommandFailed(f"Program {program} not found") -def _clean_env(env: Mapping[str, str] | None = None) -> dict[str, str] | None: +def _clean_env(env: Mapping[str, str | None] | None = None) -> dict[str, str] | None: if env is None: return None - clean_env: dict[str, str] = {} + clean_env = {k: v for k, v in env.items() if v is not None} # Ensure systemroot is passed down, otherwise Windows will explode. if sys.platform == "win32": - clean_env["SYSTEMROOT"] = os.environ.get("SYSTEMROOT", "") - - clean_env.update(env) + clean_env.setdefault("SYSTEMROOT", os.environ.get("SYSTEMROOT", "")) return clean_env @@ -77,7 +75,7 @@ def _shlex_join(args: Sequence[str | os.PathLike[str]]) -> str: def run( args: Sequence[str | os.PathLike[str]], *, - env: Mapping[str, str] | None = None, + env: Mapping[str, str | None] | None = None, silent: bool = False, paths: Sequence[str] | None = None, success_codes: Iterable[int] | None = None, diff --git a/nox/sessions.py b/nox/sessions.py index 1544c3f4..b9d2669b 100644 --- a/nox/sessions.py +++ b/nox/sessions.py @@ -283,7 +283,7 @@ def _run_func( def run( self, *args: str | os.PathLike[str], - env: Mapping[str, str] | None = None, + env: Mapping[str, str | None] | None = None, include_outer_env: bool = True, **kwargs: Any, ) -> Any | None: @@ -350,7 +350,9 @@ def run( print("Current Git commit is", out.strip()) :param env: A dictionary of environment variables to expose to the - command. By default, all environment variables are passed. + command. By default, all environment variables are passed. You + can block an environment variable from the outer environment by + setting it to None. :type env: dict or None :param include_outer_env: Boolean parameter that determines if the environment variables from the nox invocation environment should @@ -406,7 +408,7 @@ def run( def run_install( self, *args: str | os.PathLike[str], - env: Mapping[str, str] | None = None, + env: Mapping[str, str | None] | None = None, include_outer_env: bool = True, **kwargs: Any, ) -> Any | None: @@ -474,7 +476,7 @@ def run_install( def run_always( self, *args: str | os.PathLike[str], - env: Mapping[str, str] | None = None, + env: Mapping[str, str | None] | None = None, include_outer_env: bool = True, **kwargs: Any, ) -> Any | None: @@ -490,7 +492,7 @@ def run_always( def _run( self, *args: str | os.PathLike[str], - env: Mapping[str, str] | None = None, + env: Mapping[str, str | None] | None = None, include_outer_env: bool = True, **kwargs: Any, ) -> Any: @@ -501,10 +503,8 @@ def _run( # Combine the env argument with our virtualenv's env vars. if include_outer_env: - overlay_env = env - env = self.env.copy() - if overlay_env is not None: - env.update(overlay_env) + overlay_env = env or {} + env = {**self.env, **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/nox/virtualenv.py b/nox/virtualenv.py index 17c59823..90ee0850 100644 --- a/nox/virtualenv.py +++ b/nox/virtualenv.py @@ -83,15 +83,13 @@ def __init__( self, bin_paths: None = None, env: Mapping[str, str | None] | None = None ) -> None: self._bin_paths = bin_paths - self.env = os.environ.copy() self._reused = False - env = env or {} - for k, v in env.items(): - if v is None: - self.env.pop(k, None) - else: - self.env[k] = v + # Filter envs now so `.env` is dict[str, str] (easier to use) + # even though .command's env supports None. + env = env or {} + env = {**os.environ, **env} + self.env = {k: v for k, v in env.items() if v is not None} for key in _BLACKLISTED_ENV_VARS: self.env.pop(key, None) diff --git a/tests/test_command.py b/tests/test_command.py index 68f370a9..9a50093b 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -144,6 +144,19 @@ def test_run_env_unicode(): assert "123" in result +def test_run_env_remove(monkeypatch): + monkeypatch.setenv("EMPTY", "notempty") + nox.command.run( + [PYTHON, "-c", 'import os; assert "EMPTY" in os.environ'], + silent=True, + ) + nox.command.run( + [PYTHON, "-c", 'import os; assert "EMPTY" not in os.environ'], + silent=True, + env={"EMPTY": None}, + ) + + @mock.patch("sys.platform", "win32") def test_run_env_systemroot(): systemroot = os.environ.setdefault("SYSTEMROOT", "sigil") diff --git a/tests/test_sessions.py b/tests/test_sessions.py index e720c61b..2a105b8a 100644 --- a/tests/test_sessions.py +++ b/tests/test_sessions.py @@ -271,14 +271,15 @@ def test_run_overly_env(self): session, runner = self.make_session_and_runner() runner.venv.env["A"] = "1" runner.venv.env["B"] = "2" + runner.venv.env["C"] = "4" result = session.run( sys.executable, "-c", - 'import os; print(os.environ["A"], os.environ["B"])', - env={"B": "3"}, + 'import os; print(os.environ["A"], os.environ["B"], os.environ.get("C", "5"))', + env={"B": "3", "C": None}, silent=True, ) - assert result.strip() == "1 3" + assert result.strip() == "1 3 5" def test_by_default_all_invocation_env_vars_are_passed(self): session, runner = self.make_session_and_runner()