Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance environment variables passing #652

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions nox/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
47 changes: 38 additions & 9 deletions nox/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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**.

Expand All @@ -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
Expand Down Expand Up @@ -428,23 +449,31 @@ 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.
if callable(args[0]):
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:
Expand Down
3 changes: 2 additions & 1 deletion tests/test_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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):
Expand Down
31 changes: 31 additions & 0 deletions tests/test_sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down