Repository: tox-dev/tox

Issue:
tox 4 ignores TOX_SKIP_ENV
## Issue

My `make release` for [gitlab-trace](https://github.com/mgedmin/gitlab-trace) runs `TOX_SKIP_ENV=check-manifest tox -p auto`.  This worked with tox 3.x.  It now fails in tox 4, because it doesn't skip the check-manifest toxenv.

Since the documentation page describing the TOX_SKIP_ENV has 'latest' in the URL (https://tox.wiki/en/latest/config.html#envlist), I think it should still be supported?

## Environment

Provide at least:

- OS: Ubuntu 22.10
- `pip list` of the host Python where `tox` is installed:

```console
$ pipx runpip tox list
Package       Version
------------- -------
cachetools    5.2.0
chardet       5.1.0
colorama      0.4.6
distlib       0.3.6
filelock      3.8.2
packaging     22.0
pip           22.3.1
platformdirs  2.6.0
pluggy        1.0.0
py            1.11.0
pyparsing     3.0.9
pyproject_api 1.2.1
setuptools    65.6.3
six           1.16.0
toml          0.10.2
tomli         2.0.1
tox           4.0.2
virtualenv    20.17.1
wheel         0.38.4
```

<!-- do I need these?
## Output of running tox

Provide the output of `tox -rvv`:

```console

```

## Minimal example

If possible, provide a minimal reproducer for the issue:

```console

```
-->

Discussion:
mgedmin: I am very confused about logging levels.  `tox --help` says

```
verbosity:
  every -v increases, every -q decreases verbosity level, default INFO, map 0=CRITICAL|1=ERROR|2=WARNING|3=INFO|4=DEBUG|5=NOTSET

  -v, --verbose               increase verbosity (default: 2)
  -q, --quiet                 decrease verbosity (default: 0)
```

so level 2 is WARNING, and the default level is INFO, which is 3, but also the default level is 2, which is WARNING.

Anyway, I'm logging with .info(), and it's visible only if you pass a single -v to tox, which means the actual default is WARNING and the --help output lies to me?

Should I change the log level of this message to .warning() so it would match docs/config.rst?

Patch:
diff --git a/docs/changelog/2698.bugfix.rst b/docs/changelog/2698.bugfix.rst
new file mode 100644
index 00000000..71e65651
--- /dev/null
+++ b/docs/changelog/2698.bugfix.rst
@@ -0,0 +1,2 @@
+``TOX_SKIP_ENV`` environment variable now works again, and can also be set via the CLI argument ``--skip-env``
+for any command where ``-e`` can be set - by :user:`mgedmin`.
diff --git a/docs/changelog/2719.feature.rst b/docs/changelog/2719.feature.rst
new file mode 100644
index 00000000..b80cace7
--- /dev/null
+++ b/docs/changelog/2719.feature.rst
@@ -0,0 +1,4 @@
+Modified handling of ``NO_COLOR`` environment variable, consistent with
+`de facto conventions <https://no-color.org>`_: any non-empty string will enable ``NO_COLOR`` (disable colorized
+output); no ``NO_COLOR`` variable or ``NO_COLOR`` with an empty string will disable ``NO_COLOR`` (enable colorized
+output) - by :user:`ptmcg`.
diff --git a/docs/changelog/2726.bugfix.rst b/docs/changelog/2726.bugfix.rst
new file mode 100644
index 00000000..a6fde490
--- /dev/null
+++ b/docs/changelog/2726.bugfix.rst
@@ -0,0 +1,1 @@
+``tox config`` should only show :ref:`env_list` arguments by default instead of ``ALL`` - by :user:`gaborbernat`.
diff --git a/src/tox/config/cli/parser.py b/src/tox/config/cli/parser.py
index 234fce2a..b7daeb15 100644
--- a/src/tox/config/cli/parser.py
+++ b/src/tox/config/cli/parser.py
@@ -290,7 +290,7 @@ def add_verbosity_flags(parser: ArgumentParser) -> None:
 
 def add_color_flags(parser: ArgumentParser) -> None:
     converter = StrConvert()
-    if converter.to_bool(os.environ.get("NO_COLOR", "")):
+    if os.environ.get("NO_COLOR", "") != "":
         color = "no"
     elif converter.to_bool(os.environ.get("FORCE_COLOR", "")):
         color = "yes"
diff --git a/src/tox/session/cmd/show_config.py b/src/tox/session/cmd/show_config.py
index 82181e3c..b00d30ee 100644
--- a/src/tox/session/cmd/show_config.py
+++ b/src/tox/session/cmd/show_config.py
@@ -32,10 +32,10 @@ def tox_add_option(parser: ToxParser) -> None:
     our.add_argument(
         "--core",
         action="store_true",
-        help="show core options too when selecting an env with -e",
+        help="show core options (by default is hidden unless -e ALL is passed)",
         dest="show_core",
     )
-    register_env_select_flags(our, default=CliEnv("ALL"))
+    register_env_select_flags(our, default=CliEnv())
     env_run_create_flags(our, mode="config")
 
 
diff --git a/src/tox/session/env_select.py b/src/tox/session/env_select.py
index 15d41c43..fcf28893 100644
--- a/src/tox/session/env_select.py
+++ b/src/tox/session/env_select.py
@@ -1,5 +1,7 @@
 from __future__ import annotations
 
+import logging
+import re
 from argparse import ArgumentParser
 from collections import Counter
 from dataclasses import dataclass
@@ -21,6 +23,9 @@ if TYPE_CHECKING:
     from tox.session.state import State
 
 
+LOGGER = logging.getLogger(__name__)
+
+
 class CliEnv:
     """CLI tox env selection"""
 
@@ -88,6 +93,8 @@ def register_env_select_flags(
         add_to.add_argument("-m", dest="labels", metavar="label", help=help_msg, default=[], type=str, nargs="+")
         help_msg = "factors to evaluate"
         add_to.add_argument("-f", dest="factors", metavar="factor", help=help_msg, default=[], type=str, nargs="+")
+    help_msg = "exclude all environments selected that match this regular expression"
+    add_to.add_argument("--skip-env", dest="skip_env", metavar="re", help=help_msg, default="", type=str)
     return add_to
 
 
@@ -106,6 +113,7 @@ class EnvSelector:
         # to load the package environments of a run environments we need the run environment builder
         # to load labels we need core + the run environment
         self.on_empty_fallback_py = True
+        self._warned_about: set[str] = set()  #: shared set of skipped environments that were already warned about
         self._state = state
         self._cli_envs: CliEnv | None = getattr(self._state.conf.options, "env", None)
         self._defined_envs_: None | dict[str, _ToxEnvInfo] = None
@@ -118,6 +126,8 @@ class EnvSelector:
         self._provision: None | tuple[bool, str, MemoryLoader] = None
 
         self._state.conf.core.add_config("labels", Dict[str, EnvList], {}, "core labels")
+        tox_env_filter_regex = getattr(state.conf.options, "skip_env", "").strip()
+        self._filter_re = re.compile(tox_env_filter_regex) if tox_env_filter_regex else None
 
     def _collect_names(self) -> Iterator[tuple[Iterable[str], bool]]:
         """:return: sources of tox environments defined with name and if is marked as target to run"""
@@ -320,14 +330,17 @@ class EnvSelector:
 
         :return: an iteration of tox environments
         """
-        ignore_envs: set[str] = set()
         for name, env_info in self._defined_envs.items():
             if only_active and not env_info.is_active:
                 continue
             if not package and not isinstance(env_info.env, RunToxEnv):
                 continue
+            if self._filter_re is not None and self._filter_re.match(name):
+                if name not in self._warned_about:
+                    self._warned_about.add(name)
+                    LOGGER.warning("skip environment %s, matches filter %r", name, self._filter_re.pattern)
+                continue
             yield name
-            ignore_envs.add(name)
 
     def ensure_only_run_env_is_active(self) -> None:
         envs, active = self._defined_envs, self._env_name_to_active()


Test patch:
diff --git a/tests/config/cli/test_cli_env_var.py b/tests/config/cli/test_cli_env_var.py
index 89f156b2..831f9dae 100644
--- a/tests/config/cli/test_cli_env_var.py
+++ b/tests/config/cli/test_cli_env_var.py
@@ -62,6 +62,7 @@ def test_verbose_no_test() -> None:
         "index_url": [],
         "factors": [],
         "labels": [],
+        "skip_env": "",
     }
 
 
@@ -120,6 +121,7 @@ def test_env_var_exhaustive_parallel_values(
         "factors": [],
         "labels": [],
         "exit_and_dump_after": 0,
+        "skip_env": "",
     }
     assert options.parsed.verbosity == 4
     assert options.cmd_handlers == core_handlers
diff --git a/tests/config/cli/test_cli_ini.py b/tests/config/cli/test_cli_ini.py
index cfb8a283..6d7e245b 100644
--- a/tests/config/cli/test_cli_ini.py
+++ b/tests/config/cli/test_cli_ini.py
@@ -99,6 +99,7 @@ def default_options(tmp_path: Path) -> dict[str, Any]:
         "factors": [],
         "labels": [],
         "exit_and_dump_after": 0,
+        "skip_env": "",
     }
 
 
@@ -134,6 +135,7 @@ def test_ini_exhaustive_parallel_values(exhaustive_ini: Path, core_handlers: dic
         "factors": [],
         "labels": [],
         "exit_and_dump_after": 0,
+        "skip_env": "",
     }
     assert options.parsed.verbosity == 4
     assert options.cmd_handlers == core_handlers
diff --git a/tests/config/cli/test_parser.py b/tests/config/cli/test_parser.py
index 2cbf21d7..1029b25a 100644
--- a/tests/config/cli/test_parser.py
+++ b/tests/config/cli/test_parser.py
@@ -28,7 +28,7 @@ def test_parser_const_with_default_none(monkeypatch: MonkeyPatch) -> None:
 
 
 @pytest.mark.parametrize("is_atty", [True, False])
-@pytest.mark.parametrize("no_color", [None, "0", "1"])
+@pytest.mark.parametrize("no_color", [None, "0", "1", "", "\t", " ", "false", "true"])
 @pytest.mark.parametrize("force_color", [None, "0", "1"])
 @pytest.mark.parametrize("tox_color", [None, "bad", "no", "yes"])
 @pytest.mark.parametrize("term", [None, "xterm", "dumb"])
@@ -56,7 +56,7 @@ def test_parser_color(
 
     if tox_color in ("yes", "no"):
         expected = tox_color == "yes"
-    elif no_color == "1":
+    elif bool(no_color):
         expected = False
     elif force_color == "1":
         expected = True
diff --git a/tests/config/test_main.py b/tests/config/test_main.py
index cff7d2f4..25c07740 100644
--- a/tests/config/test_main.py
+++ b/tests/config/test_main.py
@@ -88,15 +88,8 @@ def test_args_are_paths_when_with_change_dir(tox_project: ToxProjectCreator) ->
 
 def test_relative_config_paths_resolve(tox_project: ToxProjectCreator) -> None:
     project = tox_project({"tox.ini": "[tox]"})
-    result = project.run(
-        "c",
-        "-c",
-        str(Path(project.path.name) / "tox.ini"),
-        "-k",
-        "change_dir",
-        "env_dir",
-        from_cwd=project.path.parent,
-    )
+    ini = str(Path(project.path.name) / "tox.ini")
+    result = project.run("c", "-c", ini, "-k", "change_dir", "env_dir", from_cwd=project.path.parent)
     result.assert_success()
-    expected = f"[testenv:py]\nchange_dir = {project.path}\nenv_dir = {project.path / '.tox' / 'py'}\n\n[tox]\n"
+    expected = f"[testenv:py]\nchange_dir = {project.path}\nenv_dir = {project.path / '.tox' / 'py'}\n"
     assert result.out == expected
diff --git a/tests/session/cmd/test_show_config.py b/tests/session/cmd/test_show_config.py
index cf2e719a..d4fa3b37 100644
--- a/tests/session/cmd/test_show_config.py
+++ b/tests/session/cmd/test_show_config.py
@@ -129,10 +129,9 @@ def test_show_config_pkg_env_once(
     patch_prev_py: Callable[[bool], tuple[str, str]],
 ) -> None:
     prev_ver, impl = patch_prev_py(True)
-    project = tox_project(
-        {"tox.ini": f"[tox]\nenv_list=py{prev_ver},py\n[testenv]\npackage=wheel", "pyproject.toml": ""},
-    )
-    result = project.run("c")
+    ini = f"[tox]\nenv_list=py{prev_ver},py\n[testenv]\npackage=wheel"
+    project = tox_project({"tox.ini": ini, "pyproject.toml": ""})
+    result = project.run("c", "-e", "ALL")
     result.assert_success()
     parser = ConfigParser(interpolation=None)
     parser.read_string(result.out)
@@ -145,10 +144,9 @@ def test_show_config_pkg_env_skip(
     patch_prev_py: Callable[[bool], tuple[str, str]],
 ) -> None:
     prev_ver, impl = patch_prev_py(False)
-    project = tox_project(
-        {"tox.ini": f"[tox]\nenv_list=py{prev_ver},py\n[testenv]\npackage=wheel", "pyproject.toml": ""},
-    )
-    result = project.run("c")
+    ini = f"[tox]\nenv_list=py{prev_ver},py\n[testenv]\npackage=wheel"
+    project = tox_project({"tox.ini": ini, "pyproject.toml": ""})
+    result = project.run("c", "-e", "ALL")
     result.assert_success()
     parser = ConfigParser(interpolation=None)
     parser.read_string(result.out)
diff --git a/tests/session/test_env_select.py b/tests/session/test_env_select.py
index 90d72a43..b6683541 100644
--- a/tests/session/test_env_select.py
+++ b/tests/session/test_env_select.py
@@ -1,6 +1,6 @@
 from __future__ import annotations
 
-from tox.pytest import ToxProjectCreator
+from tox.pytest import MonkeyPatch, ToxProjectCreator
 
 
 def test_label_core_can_define(tox_project: ToxProjectCreator) -> None:
@@ -70,3 +70,27 @@ def test_factor_select(tox_project: ToxProjectCreator) -> None:
     outcome = project.run("l", "--no-desc", "-f", "cov", "django20")
     outcome.assert_success()
     outcome.assert_out_err("py310-django20-cov\npy39-django20-cov\n", "")
+
+
+def test_tox_skip_env(tox_project: ToxProjectCreator, monkeypatch: MonkeyPatch) -> None:
+    monkeypatch.setenv("TOX_SKIP_ENV", "m[y]py")
+    project = tox_project({"tox.ini": "[tox]\nenv_list = py3{10,9},mypy"})
+    outcome = project.run("l", "--no-desc", "-q")
+    outcome.assert_success()
+    outcome.assert_out_err("py310\npy39\n", "")
+
+
+def test_tox_skip_env_cli(tox_project: ToxProjectCreator, monkeypatch: MonkeyPatch) -> None:
+    monkeypatch.delenv("TOX_SKIP_ENV", raising=False)
+    project = tox_project({"tox.ini": "[tox]\nenv_list = py3{10,9},mypy"})
+    outcome = project.run("l", "--no-desc", "-q", "--skip-env", "m[y]py")
+    outcome.assert_success()
+    outcome.assert_out_err("py310\npy39\n", "")
+
+
+def test_tox_skip_env_logs(tox_project: ToxProjectCreator, monkeypatch: MonkeyPatch) -> None:
+    monkeypatch.setenv("TOX_SKIP_ENV", "m[y]py")
+    project = tox_project({"tox.ini": "[tox]\nenv_list = py3{10,9},mypy"})
+    outcome = project.run("l", "--no-desc")
+    outcome.assert_success()
+    outcome.assert_out_err("ROOT: skip environment mypy, matches filter 'm[y]py'\npy310\npy39\n", "")
diff --git a/tests/tox_env/test_tox_env_api.py b/tests/tox_env/test_tox_env_api.py
index 70097443..65623b73 100644
--- a/tests/tox_env/test_tox_env_api.py
+++ b/tests/tox_env/test_tox_env_api.py
@@ -87,7 +87,7 @@ def test_tox_env_pass_env_fails_on_whitespace(tox_project: ToxProjectCreator) ->
     result.assert_success()
     msg = (
         '[testenv:py]\npass_env = # Exception: Fail("pass_env values cannot contain whitespace, use comma to have '
-        f'multiple values in a single line, invalid values found {first!r}, {second!r}")\n\n[tox]\n'
+        f'multiple values in a single line, invalid values found {first!r}, {second!r}")\n'
     )
     assert result.out == msg