Repository: tox-dev/tox

Issue:
Allow CI runs to disable listing dependencies
## What's the problem this feature will solve?

<!-- What are you trying to do, that you are unable to achieve with tox as it currently stands? -->
Right now CI runs always list dependencies for each environment. While this is often nice, it can result in excessive output, especially when using tox to run linters and other tools.

## Describe the solution you'd like

<!-- Clear and concise description of what you want to happen. -->
If `list_dependencies_command` is explicitly set as an empty string for an environment, skip the listing dependencies command.
<!-- Provide examples of real world use cases that this would enable and how it solves the problem described above. -->

## Alternative Solutions

<!-- Have you tried to workaround the problem using tox or other tools? Or a different approach to solving this issue?
Please elaborate here. -->
One alternative would be a separate configuration option for an environment to specifically disable listing dependencies.

## Additional context

<!-- Add any other context, links, etc. about the feature here. -->

snapcraft and several other projects use tox to run our linters. However, when running in CI, it can be difficult to find specific linting failures between all the `pip freeze` lines:

![image](https://github.com/tox-dev/tox/assets/4305943/1a51f38a-74ec-4fac-925d-736db96b4301)


Comments:

Patch:
diff --git a/docs/changelog/3024.feature.rst b/docs/changelog/3024.feature.rst
new file mode 100644
index 00000000..d63fa00d
--- /dev/null
+++ b/docs/changelog/3024.feature.rst
@@ -0,0 +1,2 @@
+Addded ``--list-dependencies`` and ``--no-list-dependencies`` CLI parameters.
+If unspecified, defaults to listing when in CI, but not otherwise.
diff --git a/src/tox/session/cmd/run/common.py b/src/tox/session/cmd/run/common.py
index 49aeb87b..2846f8d9 100644
--- a/src/tox/session/cmd/run/common.py
+++ b/src/tox/session/cmd/run/common.py
@@ -22,6 +22,7 @@ from tox.session.cmd.run.single import ToxEnvRunResult, run_one
 from tox.session.state import State
 from tox.tox_env.api import ToxEnv
 from tox.tox_env.runner import RunToxEnv
+from tox.util.ci import is_ci
 from tox.util.graph import stable_topological_sort
 from tox.util.spinner import MISS_DURATION, Spinner
 
@@ -156,6 +157,19 @@ def env_run_create_flags(parser: ArgumentParser, mode: str) -> None:
             help="if recreate is set do not recreate packaging tox environment(s)",
             action="store_true",
         )
+        list_deps = parser.add_mutually_exclusive_group()
+        list_deps.add_argument(
+            "--list-dependencies",
+            action="store_true",
+            default=is_ci(),
+            help="list the dependencies installed during environment setup",
+        )
+        list_deps.add_argument(
+            "--no-list-dependencies",
+            action="store_false",
+            dest="list_dependencies",
+            help="never list the dependencies installed during environment setup",
+        )
     if mode not in ("devenv", "config", "depends"):
         parser.add_argument(
             "--skip-pkg-install",
diff --git a/src/tox/tox_env/python/api.py b/src/tox/tox_env/python/api.py
index da9e435f..6e57626e 100644
--- a/src/tox/tox_env/python/api.py
+++ b/src/tox/tox_env/python/api.py
@@ -15,7 +15,6 @@ from virtualenv.discovery.py_spec import PythonSpec
 from tox.config.main import Config
 from tox.tox_env.api import ToxEnv, ToxEnvCreateArgs
 from tox.tox_env.errors import Fail, Recreate, Skip
-from tox.util.ci import is_ci
 
 
 class VersionInfo(NamedTuple):
@@ -227,12 +226,11 @@ class Python(ToxEnv, ABC):
     def _done_with_setup(self) -> None:
         """called when setup is done"""
         super()._done_with_setup()
-        running_in_ci = is_ci()
-        if self.journal or running_in_ci:
+        if self.journal or self.options.list_dependencies:
             outcome = self.installer.installed()
             if self.journal:
                 self.journal["installed_packages"] = outcome
-            if running_in_ci:
+            if self.options.list_dependencies:
                 logging.warning(",".join(outcome))
 
     def python_cache(self) -> dict[str, Any]:


Test patch:
diff --git a/tests/config/cli/test_cli_env_var.py b/tests/config/cli/test_cli_env_var.py
index 61dbf962..1d83175e 100644
--- a/tests/config/cli/test_cli_env_var.py
+++ b/tests/config/cli/test_cli_env_var.py
@@ -10,6 +10,7 @@ from tox.config.loader.api import Override
 from tox.pytest import CaptureFixture, LogCaptureFixture, MonkeyPatch
 from tox.session.env_select import CliEnv
 from tox.session.state import State
+from tox.util.ci import is_ci
 
 
 def test_verbose() -> None:
@@ -63,6 +64,7 @@ def test_verbose_no_test() -> None:
         "factors": [],
         "labels": [],
         "skip_env": "",
+        "list_dependencies": is_ci(),
     }
 
 
@@ -121,6 +123,7 @@ def test_env_var_exhaustive_parallel_values(
         "labels": [],
         "exit_and_dump_after": 0,
         "skip_env": "",
+        "list_dependencies": is_ci(),
     }
     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 c7324e1a..623a56f5 100644
--- a/tests/config/cli/test_cli_ini.py
+++ b/tests/config/cli/test_cli_ini.py
@@ -19,6 +19,7 @@ from tox.config.source import discover_source
 from tox.pytest import CaptureFixture, LogCaptureFixture, MonkeyPatch
 from tox.session.env_select import CliEnv
 from tox.session.state import State
+from tox.util.ci import is_ci
 
 
 @pytest.fixture()
@@ -102,6 +103,7 @@ def default_options() -> dict[str, Any]:
         "labels": [],
         "exit_and_dump_after": 0,
         "skip_env": "",
+        "list_dependencies": is_ci(),
     }
 
 
@@ -139,6 +141,7 @@ def test_ini_exhaustive_parallel_values(core_handlers: dict[str, Callable[[State
         "labels": [],
         "exit_and_dump_after": 0,
         "skip_env": "",
+        "list_dependencies": is_ci(),
     }
     assert options.parsed.verbosity == 4
     assert options.cmd_handlers == core_handlers
diff --git a/tests/tox_env/python/test_python_api.py b/tests/tox_env/python/test_python_api.py
index 3348acda..a9ff7522 100644
--- a/tests/tox_env/python/test_python_api.py
+++ b/tests/tox_env/python/test_python_api.py
@@ -218,9 +218,25 @@ def test_python_set_hash_seed_incorrect(tox_project: ToxProjectCreator) -> None:
 
 @pytest.mark.parametrize("in_ci", [True, False])
 def test_list_installed_deps(in_ci: bool, tox_project: ToxProjectCreator, mocker: MockerFixture) -> None:
-    mocker.patch("tox.tox_env.python.api.is_ci", return_value=in_ci)
+    mocker.patch("tox.session.cmd.run.common.is_ci", return_value=in_ci)
     result = tox_project({"tox.ini": "[testenv]\nskip_install = true"}).run("r", "-e", "py")
     if in_ci:
         assert "pip==" in result.out
     else:
         assert "pip==" not in result.out
+
+
+@pytest.mark.parametrize("list_deps", ["--list-dependencies", "--no-list-dependencies"])
+@pytest.mark.parametrize("in_ci", [True, False])
+def test_list_installed_deps_explicit_cli(
+    list_deps: str,
+    in_ci: bool,
+    tox_project: ToxProjectCreator,
+    mocker: MockerFixture,
+) -> None:
+    mocker.patch("tox.session.cmd.run.common.is_ci", return_value=in_ci)
+    result = tox_project({"tox.ini": "[testenv]\nskip_install = true"}).run(list_deps, "r", "-e", "py")
+    if list_deps == "--list-dependencies":
+        assert "pip==" in result.out
+    else:
+        assert "pip==" not in result.out