Skip to content

Commit 1c1c905

Browse files
worksbyfridayclaude
andcommitted
Fix factor selection via TOX_FACTORS environment variable (#3557)
The argparse action type for `-f`/`--factors` is `_AppendAction` with `nargs="+"`, which produces `list[list[str]]` at runtime. But `get_type()` only inferred `list[str]`, so `get_env_var()` produced a flat list of strings. When `_parse_factors()` iterated over this, it treated each character as a separate factor instead of each string. Fix `get_type()` to detect `nargs` on `_AppendAction` and return the correct nested type `list[list[str]]`. The existing `Convert` infrastructure already handles nested generics — semicolons separate OR groups, commas separate AND factors within a group. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0ee48ce commit 1c1c905

3 files changed

Lines changed: 32 additions & 1 deletion

File tree

docs/changelog/3557.bugfix.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix factor selection via ``TOX_FACTORS`` environment variable producing wrong results because ``append`` + ``nargs="+"``
2+
actions need nested list types for proper env var conversion - by :user:`Fridayai700`.

src/tox/config/cli/parser.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,10 @@ def get_type(action: Action) -> type[Any]:
6666
of_type: type[Any] | None = getattr(action, "of_type", None)
6767
if of_type is None:
6868
if isinstance(action, argparse._AppendAction): # noqa: SLF001
69-
of_type = list[action.type] # ty: ignore[invalid-type-form] # runtime generic from argparse action type
69+
if action.nargs in ("+", "*") or (isinstance(action.nargs, int) and action.nargs > 1):
70+
of_type = list[list[action.type]] # ty: ignore[invalid-type-form] # nargs produces list per invocation
71+
else:
72+
of_type = list[action.type] # ty: ignore[invalid-type-form] # runtime generic from argparse action type
7073
elif isinstance(action, argparse._StoreAction) and action.choices: # noqa: SLF001
7174
loc = locals()
7275
loc["Literal"] = Literal

tests/session/test_env_select.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,32 @@ def test_factor_select(
152152
outcome.assert_out_err("{}\n".format("\n".join(expect_envs)), "")
153153

154154

155+
@pytest.mark.parametrize(
156+
("env_value", "expect_envs"),
157+
[
158+
("cov", ("py310-django20-cov", "py310-django21-cov", "py39-django20-cov", "py39-django21-cov")),
159+
("py39,django20", ("py39-django20-cov", "py39-django20")),
160+
("py39;py310", ("py310-django20-cov", "py310-django20", "py310-django21-cov", "py310-django21",
161+
"py39-django20-cov", "py39-django20", "py39-django21-cov", "py39-django21")),
162+
],
163+
)
164+
def test_factor_select_via_env_var(
165+
tox_project: ToxProjectCreator,
166+
monkeypatch: MonkeyPatch,
167+
env_value: str,
168+
expect_envs: tuple[str, ...],
169+
) -> None:
170+
ini = """
171+
[tox]
172+
env_list = py3{10,9}-{django20,django21}{-cov,}
173+
"""
174+
monkeypatch.setenv("TOX_FACTORS", env_value)
175+
project = tox_project({"tox.ini": ini})
176+
outcome = project.run("l", "--no-desc")
177+
outcome.assert_success()
178+
outcome.assert_out_err("{}\n".format("\n".join(expect_envs)), "")
179+
180+
155181
def test_tox_skip_env(tox_project: ToxProjectCreator, monkeypatch: MonkeyPatch) -> None:
156182
monkeypatch.setenv("TOX_SKIP_ENV", "m[y]py")
157183
project = tox_project({"tox.ini": "[tox]\nenv_list = py3{10,9},mypy"})

0 commit comments

Comments
 (0)