diff --git a/docs/changelog/3598.bugfix.rst b/docs/changelog/3598.bugfix.rst new file mode 100644 index 000000000..f1ee47ffa --- /dev/null +++ b/docs/changelog/3598.bugfix.rst @@ -0,0 +1,6 @@ +The :func:`tox_extend_envs() hook ` +recently added in :pull:`3591` turned out to not work well with +``tox run``. It was fixed internally, not to exhaust the underlying +iterator on the first use. + +-- by :user:`webknjaz` diff --git a/src/tox/config/main.py b/src/tox/config/main.py index ff264ea21..9fbd9f4bc 100644 --- a/src/tox/config/main.py +++ b/src/tox/config/main.py @@ -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 @@ -81,7 +81,11 @@ 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) + # NOTE: `tee(self._extra_envs)[1]` is necessary for compatibility with + # NOTE: Python 3.11 and older versions. Once Python 3.12 is the lowest + # NOTE: supported version, it can be changed to + # NOTE: `chain.from_iterable(tee(self._extra_envs, 1))`. + return chain(self._src.envs(self.core), tee(self._extra_envs)[1]) def sections(self) -> Iterator[Section]: yield from self._src.sections() diff --git a/src/tox/session/state.py b/src/tox/session/state.py index 6b63f39da..3f59ecd73 100644 --- a/src/tox/session/state.py +++ b/src/tox/session/state.py @@ -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 @@ -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"], diff --git a/tests/plugin/test_inline.py b/tests/plugin/test_inline.py index 4fae64b27..46af969a3 100644 --- a/tests/plugin/test_inline.py +++ b/tests/plugin/test_inline.py @@ -1,5 +1,6 @@ from __future__ import annotations +import sys from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -43,6 +44,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( @@ -59,3 +61,15 @@ 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() + underlying_expected_oserror_msg = ( + "[WinError 2] The system cannot find the file specified" + if sys.platform == "win32" + else "[Errno 2] No such file or directory: 'sentinel-cmd'" + ) + expected_cmd_lookup_error_txt = ( + f"sentinel-env-name: Exception running subprocess {underlying_expected_oserror_msg!s}\n" + ) + assert expected_cmd_lookup_error_txt in tox_run_result.out