Repository: tox-dev/tox

Issue:
TOX_DISCOVER not working (micromamba)
## Issue

I've set `TOX_DISCOVER` to a space separated list of paths, however, `tox` seems to ignore that. 
Even when I pass it directly to `--discover`, only the last path is actually discovered.

<details><summary>Console output from my investigation</summary>

```console
(ansible-lint-empty-lines-between-tasks) ➜  ansible-lint-empty-lines-between-tasks git:(master) ✗ which tox
/home/martin/micromamba/envs/ansible-lint-empty-lines-between-tasks/bin/tox  
(ansible-lint-empty-lines-between-tasks) ➜  ansible-lint-empty-lines-between-tasks git:(master) ✗ echo $TOX_DISCOVER
/home/martin/micromamba/envs/py37/bin /home/martin/micromamba/envs/py38/bin /home/martin/micromamba/envs/py39/bin /home/martin/micromamba/envs/py310/bin /home/martin/micromamba/envs/py311/bin /home/martin/micromamba/envs/py312/bin                  
(ansible-lint-empty-lines-between-tasks) ➜  ansible-lint-empty-lines-between-tasks git:(master) ✗ for p in $(echo $TOX_DISCOVER); do echo -n "$p: "; ${p}/python --version; done
/home/martin/micromamba/envs/py37/bin: Python 3.7.16
/home/martin/micromamba/envs/py38/bin: Python 3.8.17
/home/martin/micromamba/envs/py39/bin: Python 3.9.17
/home/martin/micromamba/envs/py310/bin: Python 3.10.12
/home/martin/micromamba/envs/py311/bin: Python 3.11.4
/home/martin/micromamba/envs/py312/bin: Python 3.12.0                                                                                                                                                                                                   

</details> 
## Environment
I'm using micromamba.

Provide at least:

- OS: Ubuntu 20.04

<details open> 
<summary>Output of <code>pip list</code> of the host Python, where <code>tox</code> is installed</summary>

```console
Package            Version
------------------ ---------------
annotated-types    0.6.0
black              24.4.0
build              1.2.1
bump2version       1.0.1
cachetools         5.3.3
certifi            2024.2.2
cffi               1.16.0
chardet            5.2.0
charset-normalizer 3.3.2
click              8.1.7
colorama           0.4.6
coverage           7.4.4
cryptography       42.0.5
distlib            0.3.8
distro             1.9.0
docutils           0.21.1
filelock           3.13.4
flake8             7.0.0
freezegun          1.4.0
idna               3.7
importlib_metadata 7.1.0
iniconfig          2.0.0
jaraco.classes     3.4.0
jaraco.context     5.3.0
jaraco.functools   4.0.0
jeepney            0.8.0
keyring            25.1.0
maison             1.4.3
markdown-it-py     3.0.0
mccabe             0.7.0
mdurl              0.1.2
more-itertools     10.2.0
mypy               1.9.0
mypy-extensions    1.0.0
nh3                0.2.17
packaging          24.0
pathspec           0.12.1
pip                24.0
pkginfo            1.10.0
platformdirs       4.2.0
pluggy             1.4.0
pycodestyle        2.11.1
pycparser          2.22
pydantic           2.7.0
pydantic_core      2.18.1
pyflakes           3.2.0
Pygments           2.17.2
pyproject-api      1.6.1
pyproject_hooks    1.0.0
pytest             8.1.1
pytest-cov         5.0.0
python-dateutil    2.9.0.post0
readme_renderer    43.0
requests           2.31.0
requests-toolbelt  1.0.0
rfc3986            2.0.0
rich               13.7.1
ruff               0.3.7
ruyaml             0.91.0
SecretStorage      3.3.3
setuptools         69.2.0
six                1.16.0
toml               0.10.2
tox                4.14.2
twine              5.0.0
types-freezegun    1.1.10
types-setuptools   69.2.0.20240317
typing_extensions  4.11.0
urllib3            2.2.1
virtualenv         20.25.1
wheel              0.43.0
yamlfix            1.16.0
zipp               3.18.1

```

</details>

## Output of running tox

<details open>
<summary>Output of <code>tox -rvv</code></summary>

```console
(ansible-lint-empty-lines-between-tasks) ➜  ansible-lint-empty-lines-between-tasks git:(master) ✗ tox -rvv
py3.10: 295 W remove tox env folder /home/martin/workspace/ansible-lint-empty-lines-between-tasks/.tox/py3.10 [tox/tox_env/api.py:323]
.pkg: 297 W remove tox env folder /home/martin/workspace/ansible-lint-empty-lines-between-tasks/.tox/.pkg [tox/tox_env/api.py:323]
py3.10: 390 I find interpreter for spec PythonSpec(major=3, minor=10) [virtualenv/discovery/builtin.py:58]
py3.10: 391 D get interpreter info via cmd: / /home/martin/micromamba/envs/ansible-lint-empty-lines-between-tasks/lib/python3.12/site-packages/virtualenv/discovery/py_info.py WRvXQNTlINZPalllgkZop8FdDeClVXMC Rxdhh6bKxVUdHvEI1R1C4qYyZmifFTd0 [virtualenv/discovery/cached_py_info.py:112]
py3.10: 392 W skipped because could not find python interpreter with spec(s): py3.10 [tox/session/cmd/run/single.py:50]
py3.11: 394 W remove tox env folder /home/martin/workspace/ansible-lint-empty-lines-between-tasks/.tox/py3.11 [tox/tox_env/api.py:323]
py3.10: SKIP ⚠ in 0.1 seconds
py3.11: 398 I find interpreter for spec PythonSpec(major=3, minor=11) [virtualenv/discovery/builtin.py:58]
py3.11: 399 W skipped because could not find python interpreter with spec(s): py3.11 [tox/session/cmd/run/single.py:50]
py3.12: 400 W remove tox env folder /home/martin/workspace/ansible-lint-empty-lines-between-tasks/.tox/py3.12 [tox/tox_env/api.py:323]
py3.11: SKIP ⚠ in 0.01 seconds
py3.12: 579 I find interpreter for spec PythonSpec(major=3, minor=12) [virtualenv/discovery/builtin.py:58]
py3.12: 579 W skipped because could not find python interpreter with spec(s): py3.12 [tox/session/cmd/run/single.py:50]
lint: 582 W remove tox env folder /home/martin/workspace/ansible-lint-empty-lines-between-tasks/.tox/lint [tox/tox_env/api.py:323]
py3.12: SKIP ⚠ in 0.18 seconds
lint: 645 I find interpreter for spec PythonSpec(path=/home/martin/micromamba/envs/ansible-lint-empty-lines-between-tasks/bin/python3.12) [virtualenv/discovery/builtin.py:58]
lint: 646 W skipped because could not find python interpreter with spec(s): /home/martin/micromamba/envs/ansible-lint-empty-lines-between-tasks/bin/python3.12 [tox/session/cmd/run/single.py:50]
  py3.10: SKIP (0.10 seconds)
  py3.11: SKIP (0.01 seconds)
  py3.12: SKIP (0.18 seconds)
  lint: SKIP (0.06 seconds)
  evaluation failed :( (0.47 seconds)   
```

</details>

## Minimal example

<!--  If possible, provide a minimal reproducer for the issue. -->

You can check the repo https://github.com/mimre25/ansible-lint-empty-lines-between-tasks, it's rather small.


I've tried the same with the paths being `:` separated, but to no avail. 
Unfortunately, I couldn't find the format required for `--discover`/`TOX_DISCOVER` in the documentation, but the space separated format used to work in the past when I was using miniconda.


Discussion:
Thanks for your report.

The related PR would be https://github.com/tox-dev/tox/pull/2245

But as you have reported that this feature has worked for you in the past, we probably need to look somewhere else.

When I have a look at the implementation, then I can see that it uses the `nargs=*` option for argparse, so a space separated list seems to be fine, see https://docs.python.org/3/library/argparse.html#nargs

 FWIW, where did you read about `TOX_DISCOVER`? This env variable seems to be undocumented, and currently also untested, as least from a quick glance. `--discover` should work though.

I think the next steps would be to use a debugger and step though, and inspect at which step things go south. Interpreter discovery is not handled by tox itself, but by virtualenv.
So it seems that there is an issue with how the env var is parsed in https://github.com/tox-dev/tox/blob/main/src/tox/config/cli/env_var.py#L28-L32. 

The `TOX_DISCOVER` env var is split into individual strings of single characters.

I'll try to set something up for a bisect, maybe that can help figure out what's going on.


FYI: I've found the `TOX_DISCOVER` env in https://github.com/tox-dev/tox/pull/2076, but not in the docs. 
That was also a bit confusing to me - could it be that it used to be in the docs but was removed? - I'll also bisect that.
The documentation has been rewritten with version 4. Now we universally support all environment variables by translating CLI flags from environment variables. This sounds like a bug to me. 
I believe the issue was introduced with 4.0.0. 

It successfully runs with 3.28, but with 4.0.0 the buggy behavior comes in. 
I wanted to bisect this, but since the code changed so much, I just tried out these two versions.

Here is a minimal script to reproduce the issue:
```py
from tox.config.cli.env_var import get_env_var
import os


def main():
    os.environ["TOX_DISCOVER"] = "/foo/bar/baz /baz/bar/foo /bar/foo/baz"
    paths = get_env_var("discover", list)[0]
    print(paths)

    expected = ["/foo/bar/baz", "/baz/bar/foo", "/bar/foo/baz"]
    print(expected)

    if paths != expected:
        return 1
    return 0


if __name__ == "__main__":
    exit(main())
```

Output:
```console
['/', 'f', 'o', 'o', '/', 'b', 'a', 'r', '/', 'b', 'a', 'z', ' ', '/', 'b', 'a', 'z', '/', 'b', 'a', 'r', '/', 'f', 'o', 'o', ' ', '/', 'b', 'a', 'r', '/', 'f', 'o', 'o', '/', 'b', 'a', 'z']
['/foo/bar/baz', '/baz/bar/foo', '/bar/foo/baz']
```

I'd open be to contributing a fix if new contributors are desired :slightly_smiling_face: 
Please do so 😊 



Patch:
diff --git a/docs/changelog/3272.bugfix.rst b/docs/changelog/3272.bugfix.rst
new file mode 100644
index 000000000..7877bd400
--- /dev/null
+++ b/docs/changelog/3272.bugfix.rst
@@ -0,0 +1,1 @@
+Fix broken input parsing for ``--discover`` flag. - by :user:`mimre25`
diff --git a/docs/changelog/3274.doc.rst b/docs/changelog/3274.doc.rst
new file mode 100644
index 000000000..d2ffe6028
--- /dev/null
+++ b/docs/changelog/3274.doc.rst
@@ -0,0 +1,1 @@
+Rephrase ``--discover`` flag's description to avoid confusion between paths and executables. - by :user:`mimre25`
diff --git a/src/tox/config/cli/parser.py b/src/tox/config/cli/parser.py
index 7afe87753..707c7fc8a 100644
--- a/src/tox/config/cli/parser.py
+++ b/src/tox/config/cli/parser.py
@@ -232,7 +232,8 @@ def __call__(
             dest="discover",
             nargs="+",
             metavar="path",
-            help="for Python discovery first try the Python executables under these paths",
+            of_type=List[str],
+            help="for Python discovery first try these Python executables",
             default=[],
         )
         list_deps = sub_parser.add_mutually_exclusive_group()


Test patch:
diff --git a/tests/config/cli/test_cli_env_var.py b/tests/config/cli/test_cli_env_var.py
index 3c870ae9e..7f265c78c 100644
--- a/tests/config/cli/test_cli_env_var.py
+++ b/tests/config/cli/test_cli_env_var.py
@@ -84,6 +84,7 @@ def test_env_var_exhaustive_parallel_values(
     monkeypatch.setenv("TOX_PARALLEL", "3")
     monkeypatch.setenv("TOX_PARALLEL_LIVE", "no")
     monkeypatch.setenv("TOX_OVERRIDE", "a=b;c=d")
+    monkeypatch.setenv("TOX_DISCOVER", "/foo/bar;/bar/baz;/baz/foo")
 
     options = get_options()
     assert vars(options.parsed) == {
@@ -93,7 +94,7 @@ def test_env_var_exhaustive_parallel_values(
         "default_runner": "virtualenv",
         "develop": False,
         "devenv_path": None,
-        "discover": [],
+        "discover": ["/foo/bar", "/bar/baz", "/baz/foo"],
         "env": CliEnv(["py37", "py36"]),
         "force_dep": [],
         "hash_seed": ANY,