Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Ensure tox_extend_envs list can be read twice
Previously (PR #3591) a new hook point was added. The tests checked
that it works well with the `tox config` and `tox list` commands.
However, `tox run` turned out to have a problem — it would complain
that there's no such env, when invoked:

```console
ROOT: 170 E HandledError| provided environments not found in configuration file:
pip-compile-tox-env-lock [tox/run.py:23]
```

Turned out, this was because the config object is being interated twice
in some subcommands. This in turn iterates over the discovered
additional ephemeral environments list object. The implementation passes
an iterator into it and so when it's first accessed, it's exhausted and
the second attempt does not give the same envs, causing inconsistency.

The patch solves this by using `itertools.tee()`, making sure that the
underlying iterable is always cached and it's possible to repeat
iteration as many times as possible without loosing the data in the
process.
  • Loading branch information
webknjaz committed Sep 2, 2025
commit 473af4dbad89043e0dc73a41cb6505ed12816a85
4 changes: 2 additions & 2 deletions src/tox/config/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import os
from collections import OrderedDict, defaultdict
from itertools import chain
from itertools import chain, tee
from pathlib import Path
from typing import TYPE_CHECKING, Any, Iterable, Iterator, Sequence, TypeVar

Expand Down Expand Up @@ -81,7 +81,7 @@ def src_path(self) -> Path:

def __iter__(self) -> Iterator[str]:
""":return: an iterator that goes through existing environments"""
return chain(self._src.envs(self.core), self._extra_envs)
return chain(self._src.envs(self.core), chain.from_iterable(tee(self._extra_envs)))

def sections(self) -> Iterator[Section]:
yield from self._src.sections()
Expand Down
4 changes: 2 additions & 2 deletions src/tox/session/state.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

import sys
from itertools import chain
from itertools import chain, tee
from typing import TYPE_CHECKING, Sequence

from tox.config.main import Config
Expand All @@ -20,7 +20,7 @@ class State:
"""Runtime state holder."""

def __init__(self, options: Options, args: Sequence[str]) -> None:
extended_envs = chain.from_iterable(MANAGER.tox_extend_envs())
(extended_envs,) = tee(chain.from_iterable(MANAGER.tox_extend_envs()), 1)
self.conf = Config.make(options.parsed, options.pos_args, options.source, extended_envs)
self.conf.core.add_constant(
keys=["on_platform"],
Expand Down
8 changes: 8 additions & 0 deletions tests/plugin/test_inline.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def tox_extend_envs() -> tuple[str]:
def tox_add_core_config(core_conf: ConfigSet, state: State) -> None: # noqa: ARG001
in_memory_config_loader = MemoryLoader(
base=["sentinel-base"],
commands_pre=["sentinel-cmd"],
description="sentinel-description",
)
state.conf.memory_seed_loaders[env_name].append(
Expand All @@ -59,3 +60,10 @@ def tox_add_core_config(core_conf: ConfigSet, state: State) -> None: # noqa: AR
tox_config_result = project.run("config", "-e", "sentinel-env-name", "-qq")
tox_config_result.assert_success()
assert "base = sentinel-base" in tox_config_result.out

tox_run_result = project.run("run", "-e", "sentinel-env-name", "-q")
tox_run_result.assert_failed()
expected_cmd_lookup_error_txt = (
"sentinel-env-name: Exception running subprocess [Errno 2] No such file or directory: 'sentinel-cmd'\n"
)
assert expected_cmd_lookup_error_txt in tox_run_result.out
Loading