Skip to content

Commit bc21883

Browse files
authored
feat: support None to remone envvars (#812)
Signed-off-by: Henry Schreiner <[email protected]>
1 parent 5e3d90d commit bc21883

File tree

5 files changed

+35
-25
lines changed

5 files changed

+35
-25
lines changed

nox/command.py

+4-6
Original file line numberDiff line numberDiff line change
@@ -55,17 +55,15 @@ def which(program: str | os.PathLike[str], paths: Sequence[str] | None) -> str:
5555
raise CommandFailed(f"Program {program} not found")
5656

5757

58-
def _clean_env(env: Mapping[str, str] | None = None) -> dict[str, str] | None:
58+
def _clean_env(env: Mapping[str, str | None] | None = None) -> dict[str, str] | None:
5959
if env is None:
6060
return None
6161

62-
clean_env: dict[str, str] = {}
62+
clean_env = {k: v for k, v in env.items() if v is not None}
6363

6464
# Ensure systemroot is passed down, otherwise Windows will explode.
6565
if sys.platform == "win32":
66-
clean_env["SYSTEMROOT"] = os.environ.get("SYSTEMROOT", "")
67-
68-
clean_env.update(env)
66+
clean_env.setdefault("SYSTEMROOT", os.environ.get("SYSTEMROOT", ""))
6967

7068
return clean_env
7169

@@ -77,7 +75,7 @@ def _shlex_join(args: Sequence[str | os.PathLike[str]]) -> str:
7775
def run(
7876
args: Sequence[str | os.PathLike[str]],
7977
*,
80-
env: Mapping[str, str] | None = None,
78+
env: Mapping[str, str | None] | None = None,
8179
silent: bool = False,
8280
paths: Sequence[str] | None = None,
8381
success_codes: Iterable[int] | None = None,

nox/sessions.py

+9-9
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ def _run_func(
283283
def run(
284284
self,
285285
*args: str | os.PathLike[str],
286-
env: Mapping[str, str] | None = None,
286+
env: Mapping[str, str | None] | None = None,
287287
include_outer_env: bool = True,
288288
**kwargs: Any,
289289
) -> Any | None:
@@ -350,7 +350,9 @@ def run(
350350
print("Current Git commit is", out.strip())
351351
352352
:param env: A dictionary of environment variables to expose to the
353-
command. By default, all environment variables are passed.
353+
command. By default, all environment variables are passed. You
354+
can block an environment variable from the outer environment by
355+
setting it to None.
354356
:type env: dict or None
355357
:param include_outer_env: Boolean parameter that determines if the
356358
environment variables from the nox invocation environment should
@@ -406,7 +408,7 @@ def run(
406408
def run_install(
407409
self,
408410
*args: str | os.PathLike[str],
409-
env: Mapping[str, str] | None = None,
411+
env: Mapping[str, str | None] | None = None,
410412
include_outer_env: bool = True,
411413
**kwargs: Any,
412414
) -> Any | None:
@@ -474,7 +476,7 @@ def run_install(
474476
def run_always(
475477
self,
476478
*args: str | os.PathLike[str],
477-
env: Mapping[str, str] | None = None,
479+
env: Mapping[str, str | None] | None = None,
478480
include_outer_env: bool = True,
479481
**kwargs: Any,
480482
) -> Any | None:
@@ -490,7 +492,7 @@ def run_always(
490492
def _run(
491493
self,
492494
*args: str | os.PathLike[str],
493-
env: Mapping[str, str] | None = None,
495+
env: Mapping[str, str | None] | None = None,
494496
include_outer_env: bool = True,
495497
**kwargs: Any,
496498
) -> Any:
@@ -501,10 +503,8 @@ def _run(
501503

502504
# Combine the env argument with our virtualenv's env vars.
503505
if include_outer_env:
504-
overlay_env = env
505-
env = self.env.copy()
506-
if overlay_env is not None:
507-
env.update(overlay_env)
506+
overlay_env = env or {}
507+
env = {**self.env, **overlay_env}
508508

509509
# If --error-on-external-run is specified, error on external programs.
510510
if self._runner.global_config.error_on_external_run:

nox/virtualenv.py

+5-7
Original file line numberDiff line numberDiff line change
@@ -83,15 +83,13 @@ def __init__(
8383
self, bin_paths: None = None, env: Mapping[str, str | None] | None = None
8484
) -> None:
8585
self._bin_paths = bin_paths
86-
self.env = os.environ.copy()
8786
self._reused = False
88-
env = env or {}
8987

90-
for k, v in env.items():
91-
if v is None:
92-
self.env.pop(k, None)
93-
else:
94-
self.env[k] = v
88+
# Filter envs now so `.env` is dict[str, str] (easier to use)
89+
# even though .command's env supports None.
90+
env = env or {}
91+
env = {**os.environ, **env}
92+
self.env = {k: v for k, v in env.items() if v is not None}
9593

9694
for key in _BLACKLISTED_ENV_VARS:
9795
self.env.pop(key, None)

tests/test_command.py

+13
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,19 @@ def test_run_env_unicode():
144144
assert "123" in result
145145

146146

147+
def test_run_env_remove(monkeypatch):
148+
monkeypatch.setenv("EMPTY", "notempty")
149+
nox.command.run(
150+
[PYTHON, "-c", 'import os; assert "EMPTY" in os.environ'],
151+
silent=True,
152+
)
153+
nox.command.run(
154+
[PYTHON, "-c", 'import os; assert "EMPTY" not in os.environ'],
155+
silent=True,
156+
env={"EMPTY": None},
157+
)
158+
159+
147160
@mock.patch("sys.platform", "win32")
148161
def test_run_env_systemroot():
149162
systemroot = os.environ.setdefault("SYSTEMROOT", "sigil")

tests/test_sessions.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -271,14 +271,15 @@ def test_run_overly_env(self):
271271
session, runner = self.make_session_and_runner()
272272
runner.venv.env["A"] = "1"
273273
runner.venv.env["B"] = "2"
274+
runner.venv.env["C"] = "4"
274275
result = session.run(
275276
sys.executable,
276277
"-c",
277-
'import os; print(os.environ["A"], os.environ["B"])',
278-
env={"B": "3"},
278+
'import os; print(os.environ["A"], os.environ["B"], os.environ.get("C", "5"))',
279+
env={"B": "3", "C": None},
279280
silent=True,
280281
)
281-
assert result.strip() == "1 3"
282+
assert result.strip() == "1 3 5"
282283

283284
def test_by_default_all_invocation_env_vars_are_passed(self):
284285
session, runner = self.make_session_and_runner()

0 commit comments

Comments
 (0)