Repository: wntrblm/nox
Pull request #723 (2023-07-04 09:30:21)

Change _color_finalizer behavior
Issue: https://github.com/wntrblm/nox/issues/708

### Expected Behavior
`Tests passing. Ideally, I think --force-color should override NO_COLOR in the containing environment rather than collide with it.`


With this modification, if `--force-color` is provided, it will return `True` and **enforce colors**, even if `NO_COLOR` is set in the environment. If `--no-color` is provided or `NO_COLOR` is set in the environment, it will return `False` and **disable** colors unless `--force-color` is also provided.



stasfilin: Added a new commit to fix the linter issue 

From 
```python
if args.nocolor or 'NO_COLOR' in os.environ:
```

to
```python
if args.nocolor or "NO_COLOR" in os.environ:
```

Patch:
diff --git a/nox/_decorators.py b/nox/_decorators.py
index 72636ae..1d4c4ea 100644
--- a/nox/_decorators.py
+++ b/nox/_decorators.py
@@ -66,6 +66,7 @@ class Func(FunctionDecorator):
         self.func = func
         self.python = python
         self.reuse_venv = reuse_venv
+        self.name = name
         self.venv_backend = venv_backend
         self.venv_params = venv_params
         self.should_warn = should_warn or {}
diff --git a/nox/_options.py b/nox/_options.py
index f761277..8d13c84 100644
--- a/nox/_options.py
+++ b/nox/_options.py
@@ -66,11 +66,11 @@ options.add_groups(
 )
 
 
-def _sessions_and_keywords_merge_func(
+def _sessions_merge_func(
     key: str, command_args: argparse.Namespace, noxfile_args: argparse.Namespace
 ) -> list[str]:
-    """Only return the Noxfile value for sessions/keywords if neither sessions
-    or keywords are specified on the command-line.
+    """Only return the Noxfile value for sessions/keywords if neither sessions,
+    keywords or tags are specified on the command-line.
 
     Args:
         key (str): This function is used for both the "sessions" and "keywords"
@@ -80,7 +80,11 @@ def _sessions_and_keywords_merge_func(
             command-line.
         noxfile_Args (_option_set.Namespace): The options specified in the
             Noxfile."""
-    if not command_args.sessions and not command_args.keywords:
+    if (
+        not command_args.sessions
+        and not command_args.keywords
+        and not command_args.tags
+    ):
         return getattr(noxfile_args, key)  # type: ignore[no-any-return]
     return getattr(command_args, key)  # type: ignore[no-any-return]
 
@@ -177,7 +181,7 @@ def _color_finalizer(value: bool, args: argparse.Namespace) -> bool:
     if args.forcecolor:
         return True
 
-    if args.nocolor:
+    if args.nocolor or "NO_COLOR" in os.environ:
         return False
 
     return sys.stdout.isatty()
@@ -280,7 +284,7 @@ options.add_options(
         "--session",
         group=options.groups["sessions"],
         noxfile=True,
-        merge_func=functools.partial(_sessions_and_keywords_merge_func, "sessions"),
+        merge_func=functools.partial(_sessions_merge_func, "sessions"),
         nargs="*",
         default=default_env_var_list_factory("NOXSESSION"),
         help="Which sessions to run. By default, all sessions will run.",
@@ -303,7 +307,7 @@ options.add_options(
         "--keywords",
         group=options.groups["sessions"],
         noxfile=True,
-        merge_func=functools.partial(_sessions_and_keywords_merge_func, "keywords"),
+        merge_func=functools.partial(_sessions_merge_func, "keywords"),
         help="Only run sessions that match the given expression.",
     ),
     _option_set.Option(
@@ -312,6 +316,7 @@ options.add_options(
         "--tags",
         group=options.groups["sessions"],
         noxfile=True,
+        merge_func=functools.partial(_sessions_merge_func, "tags"),
         nargs="*",
         help="Only run sessions with the given tags.",
     ),
diff --git a/nox/command.py b/nox/command.py
index ca3dd35..9398a8d 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/registry.py b/nox/registry.py
index 376c55e..35cb389 100644
--- a/nox/registry.py
+++ b/nox/registry.py
@@ -85,8 +85,11 @@ def session_decorator(
     if python is None:
         python = py
 
-    fn = Func(func, python, reuse_venv, name, venv_backend, venv_params, tags=tags)
-    _REGISTRY[name or func.__name__] = fn
+    final_name = name or func.__name__
+    fn = Func(
+        func, python, reuse_venv, final_name, venv_backend, venv_params, tags=tags
+    )
+    _REGISTRY[final_name] = fn
     return fn
 
 
diff --git a/nox/sessions.py b/nox/sessions.py
index d86c36f..dc1aa7e 100644
--- a/nox/sessions.py
+++ b/nox/sessions.py
@@ -270,7 +270,11 @@ class Session:
             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.
 
@@ -337,6 +341,10 @@ class Session:
         :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.
@@ -373,10 +381,19 @@ class Session:
             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**.
 
@@ -394,6 +411,10 @@ class Session:
         :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
@@ -426,10 +447,19 @@ class Session:
         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.
@@ -437,12 +467,11 @@ class Session:
             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:


Test patch:
diff --git a/tests/test_command.py b/tests/test_command.py
index e58077a..68f370a 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 dffd283..27bd8b8 100644
--- a/tests/test_sessions.py
+++ b/tests/test_sessions.py
@@ -273,6 +273,37 @@ class TestSession:
         )
         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()
diff --git a/tests/test_tasks.py b/tests/test_tasks.py
index c6bdbfe..99ffae3 100644
--- a/tests/test_tasks.py
+++ b/tests/test_tasks.py
@@ -315,6 +315,31 @@ def test_filter_manifest_tags_not_found(tags, caplog):
     assert "Tag selection caused no sessions to be selected." in caplog.text
 
 
+def test_merge_sessions_and_tags():
+    @nox.session(tags=["foobar"])
+    def test():
+        pass
+
+    @nox.session(tags=["foobar"])
+    def bar():
+        pass
+
+    config = _options.options.namespace(
+        noxfile=os.path.join(RESOURCES, "noxfile_options.py"),
+        sessions=None,
+        pythons=(),
+        posargs=[],
+        tags=["foobar"],
+    )
+
+    nox_module = tasks.load_nox_module(config)
+    tasks.merge_noxfile_options(nox_module, config)
+    manifest = Manifest({"test": test, "bar": bar}, config)
+    return_value = tasks.filter_manifest(manifest, config)
+    assert return_value is manifest
+    assert len(manifest) == 2
+
+
 def test_honor_list_request_noop():
     config = _options.options.namespace(list_sessions=False)
     manifest = {"thing": mock.sentinel.THING}