Skip to content

Commit

Permalink
feat: support None to remone envvars (#812)
Browse files Browse the repository at this point in the history
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
  • Loading branch information
henryiii committed Apr 12, 2024
1 parent 5e3d90d commit bc21883
Show file tree
Hide file tree
Showing 5 changed files with 35 additions and 25 deletions.
10 changes: 4 additions & 6 deletions nox/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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,
Expand Down
18 changes: 9 additions & 9 deletions nox/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand Down
12 changes: 5 additions & 7 deletions nox/virtualenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions tests/test_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
7 changes: 4 additions & 3 deletions tests/test_sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit bc21883

Please sign in to comment.