Skip to content

Commit 969fbec

Browse files
authored
Fix fresh subprocesses and allow duplicate register config calls for the core set only (#3237)
1 parent d37cb08 commit 969fbec

File tree

13 files changed

+62
-40
lines changed

13 files changed

+62
-40
lines changed

docs/changelog/3235.bugfix.rst

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix crash with fresh subprocess, if the build backend is setuptools automatically enable fresh subprocesses for
2+
build backend calls - by :user:`gaborbernat`.

docs/config.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -797,7 +797,7 @@ Python virtual environment packaging
797797
.. conf::
798798
:keys: fresh_subprocess
799799
:version_added: 4.14.0
800-
:default: False
800+
:default: True if build backend is setuptools otherwise False
801801

802802
A flag controlling if each call to the build backend should be done in a fresh subprocess or not (especially older
803803
build backends such as ``setuptools`` might require this to discover newly provisioned dependencies).

src/tox/config/main.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def __init__( # noqa: PLR0913
4040
self._overrides[override.namespace].append(override)
4141

4242
self._src = config_source
43-
self._key_to_conf_set: dict[tuple[str, str], ConfigSet] = OrderedDict()
43+
self._key_to_conf_set: dict[tuple[str, str, str], ConfigSet] = OrderedDict()
4444
self._core_set: CoreConfigSet | None = None
4545
self.memory_seed_loaders: defaultdict[str, list[MemoryLoader]] = defaultdict(list)
4646

@@ -131,7 +131,7 @@ def get_section_config( # noqa: PLR0913
131131
for_env: str | None,
132132
loaders: Sequence[Loader[Any]] | None = None,
133133
) -> T:
134-
key = section.key, for_env or ""
134+
key = section.key, for_env or "", "-".join(base or [])
135135
try:
136136
return self._key_to_conf_set[key] # type: ignore[return-value] # expected T but found ConfigSet
137137
except KeyError:
@@ -154,7 +154,7 @@ def get_env(
154154
"""
155155
Return the configuration for a given tox environment (will create if not exist yet).
156156
157-
:param item: the name of the environment
157+
:param item: the name of the environment is
158158
:param package: a flag indicating if the environment is of type packaging or not (only used for creation)
159159
:param loaders: loaders to use for this configuration (only used for creation)
160160
:return: the tox environments config
@@ -170,7 +170,7 @@ def get_env(
170170

171171
def clear_env(self, name: str) -> None:
172172
section, _, __ = self._src.get_tox_env_section(name)
173-
del self._key_to_conf_set[(section.key, name)]
173+
self._key_to_conf_set = {k: v for k, v in self._key_to_conf_set.items() if k[0] == section.key and k[1] == name}
174174

175175

176176
___all__ = [

src/tox/config/of_type.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ def __call__(
5858
def __eq__(self, o: object) -> bool:
5959
return type(self) == type(o) and super().__eq__(o) and self.value == o.value # type: ignore[attr-defined]
6060

61+
def __repr__(self) -> str:
62+
values = ((k, v) for k, v in vars(self).items() if v is not None)
63+
return f"{type(self).__name__}({', '.join(f'{k}={v}' for k, v in values)})"
64+
6165

6266
_PLACE_HOLDER = object()
6367

@@ -111,7 +115,7 @@ def __call__(
111115
return cast(T, self._cache)
112116

113117
def __repr__(self) -> str:
114-
values = ((k, v) for k, v in vars(self).items() if k != "post_process" and v is not None)
118+
values = ((k, v) for k, v in vars(self).items() if k not in {"post_process", "_cache"} and v is not None)
115119
return f"{type(self).__name__}({', '.join(f'{k}={v}' for k, v in values)})"
116120

117121
def __eq__(self, o: object) -> bool:

src/tox/config/sets.py

+8-10
Original file line numberDiff line numberDiff line change
@@ -94,19 +94,17 @@ def _add_conf(self, keys: Sequence[str], definition: ConfigDefinition[V]) -> Con
9494
key = keys[0]
9595
if key in self._defined:
9696
self._on_duplicate_conf(key, definition)
97-
else:
98-
self._keys[key] = None
99-
for item in keys:
100-
self._alias[item] = key
101-
for key in keys:
102-
self._defined[key] = definition
97+
98+
self._keys[key] = None
99+
for item in keys:
100+
self._alias[item] = key
101+
for key in keys:
102+
self._defined[key] = definition
103103
return definition
104104

105105
def _on_duplicate_conf(self, key: str, definition: ConfigDefinition[V]) -> None:
106-
earlier = self._defined[key]
107-
if definition != earlier: # pragma: no branch
108-
msg = f"config {key} already defined"
109-
raise ValueError(msg)
106+
msg = f"duplicate configuration definition for {self.name}:\nhas: {self._defined[key]}\nnew: {definition}"
107+
raise ValueError(msg)
110108

111109
def __getitem__(self, item: str) -> Any:
112110
"""

src/tox/execute/pep517_backend.py

+1
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ def close(self) -> None:
9191
execute.process.wait(timeout=0.1) # pragma: no cover
9292
except TimeoutExpired: # pragma: no cover
9393
execute.process.terminate() # pragma: no cover # if does not stop on its own kill it
94+
self._local_execute = None
9495
self.is_alive = False
9596

9697

src/tox/session/env_select.py

+9-7
Original file line numberDiff line numberDiff line change
@@ -239,12 +239,12 @@ def _env_name_to_active(self) -> dict[str, bool]:
239239
def _defined_envs(self) -> dict[str, _ToxEnvInfo]: # noqa: C901, PLR0912
240240
# The problem of classifying run/package environments:
241241
# There can be two type of tox environments: run or package. Given a tox environment name there's no easy way to
242-
# find out which it is. Intuitively a run environment is any environment that's not used for packaging by
243-
# another run environment. To find out what are the packaging environments for a run environment you have to
244-
# first construct it. This implies a two phase solution: construct all environments and query their packaging
245-
# environments. The run environments are the ones not marked as of packaging type. This requires being able
246-
# to change tox environments type, if it was earlier discovered as a run environment and is marked as packaging
247-
# we need to redefine it, e.g. when it shows up in config as [testenv:.package] and afterwards by a run env is
242+
# find out which it is. Intuitively, a run environment is any environment not used for packaging by another run
243+
# environment. To find out what are the packaging environments for a run environment, you have to first
244+
# construct it. This implies a two-phase solution: construct all environments and query their packaging
245+
# environments. The run environments are the ones not marked as of packaging type. This requires being able to
246+
# change tox environments types, if it was earlier discovered as a run environment and is marked as packaging,
247+
# we need to redefine it. E.g., when it shows up in config as [testenv:.package] and afterward by a run env is
248248
# marked as package_env.
249249

250250
if self._defined_envs_ is None: # noqa: PLR1702
@@ -267,7 +267,7 @@ def _defined_envs(self) -> dict[str, _ToxEnvInfo]: # noqa: C901, PLR0912
267267
try:
268268
run_env.package_env = self._build_pkg_env(pkg_name_type, name, env_name_to_active)
269269
except Exception as exception: # noqa: BLE001
270-
# if it's not a run environment, wait to see if ends up being a packaging one -> rollback
270+
# if it's not a run environment, wait to see if ends up being a packaging one -> rollback
271271
failed[name] = exception
272272
for key in self._pkg_env_counter - start_package_env_use_counter:
273273
del self._defined_envs_[key]
@@ -320,6 +320,7 @@ def _build_run_env(self, name: str) -> RunToxEnv | None:
320320
journal = self._journal.get_env_journal(name)
321321
args = ToxEnvCreateArgs(env_conf, self._state.conf.core, self._state.conf.options, journal, self._log_handler)
322322
run_env = runner(args)
323+
run_env.register_config()
323324
self._manager.tox_add_env_config(env_conf, self._state)
324325
return run_env
325326

@@ -363,6 +364,7 @@ def _get_package_env(self, packager: str, name: str, is_active: bool) -> Package
363364
journal = self._journal.get_env_journal(name)
364365
args = ToxEnvCreateArgs(pkg_conf, self._state.conf.core, self._state.conf.options, journal, self._log_handler)
365366
pkg_env: PackageToxEnv = package_type(args)
367+
pkg_env.register_config()
366368
self._defined_envs_[name] = _ToxEnvInfo(pkg_env, is_active)
367369
self._manager.tox_add_env_config(pkg_conf, self._state)
368370
return pkg_env

src/tox/tox_env/api.py

-2
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,6 @@ def __init__(self, create_args: ToxEnvCreateArgs) -> None:
6868
self._interrupted = False
6969
self._log_id = 0
7070

71-
self.register_config()
72-
7371
@property
7472
def cache(self) -> Info:
7573
return Info(self.env_dir)

src/tox/tox_env/python/virtual_env/package/pyproject.py

+16-7
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,19 @@ def __init__(self, create_args: ToxEnvCreateArgs) -> None:
107107
self._package_dependencies: list[Requirement] | None = None
108108
self._package_name: str | None = None
109109
self._pkg_lock = RLock() # can build only one package at a time
110-
self.root = self.conf["package_root"]
111110
self._package_paths: set[Path] = set()
111+
self._root: Path | None = None
112+
113+
@property
114+
def root(self) -> Path:
115+
if self._root is None:
116+
self._root = self.conf["package_root"]
117+
return self._root
118+
119+
@root.setter
120+
def root(self, value: Path) -> None:
121+
self._root = value
122+
self._frontend_ = None # force recreating the frontend with new root
112123

113124
@staticmethod
114125
def id() -> str:
@@ -117,8 +128,7 @@ def id() -> str:
117128
@property
118129
def _frontend(self) -> Pep517VirtualEnvFrontend:
119130
if self._frontend_ is None:
120-
fresh = cast(bool, self.conf["fresh_subprocess"])
121-
self._frontend_ = Pep517VirtualEnvFrontend(self.root, self, fresh_subprocess=fresh)
131+
self._frontend_ = Pep517VirtualEnvFrontend(self.root, self)
122132
return self._frontend_
123133

124134
def register_config(self) -> None:
@@ -140,7 +150,7 @@ def register_config(self) -> None:
140150
self.conf.add_config(
141151
keys=["fresh_subprocess"],
142152
of_type=bool,
143-
default=False,
153+
default=self._frontend.backend.split(".")[0] == "setuptools",
144154
desc="create a fresh subprocess for every backend request",
145155
)
146156

@@ -377,10 +387,9 @@ def id() -> str:
377387

378388

379389
class Pep517VirtualEnvFrontend(Frontend):
380-
def __init__(self, root: Path, env: Pep517VenvPackager, *, fresh_subprocess: bool) -> None:
390+
def __init__(self, root: Path, env: Pep517VenvPackager) -> None:
381391
super().__init__(*Frontend.create_args_from_folder(root))
382392
self._tox_env = env
383-
self._fresh_subprocess = fresh_subprocess
384393
self._backend_executor_: LocalSubProcessPep517Executor | None = None
385394
into: dict[str, Any] = {}
386395

@@ -435,7 +444,7 @@ def _send_msg(
435444
if outcome is not None: # pragma: no branch
436445
outcome.assert_success()
437446
finally:
438-
if self._fresh_subprocess:
447+
if self._tox_env.conf["fresh_subprocess"]:
439448
self.backend_executor.close()
440449

441450
def _unexpected_response( # noqa: PLR0913

tests/config/test_sets.py

+13-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import re
34
from collections import OrderedDict
45
from pathlib import Path
56
from typing import TYPE_CHECKING, Callable, Dict, Optional, Set, TypeVar
@@ -125,14 +126,24 @@ def test_config_dynamic_repr(conf_builder: ConfBuilder) -> None:
125126
def test_config_redefine_constant_fail(conf_builder: ConfBuilder) -> None:
126127
config_set = conf_builder("path = path")
127128
config_set.add_constant(keys="path", desc="desc", value="value")
128-
with pytest.raises(ValueError, match="config path already defined"):
129+
msg = (
130+
"duplicate configuration definition for py39:\n"
131+
"has: ConfigConstantDefinition(keys=('path',), desc=desc, value=value)\n"
132+
"new: ConfigConstantDefinition(keys=('path',), desc=desc2, value=value)"
133+
)
134+
with pytest.raises(ValueError, match=re.escape(msg)):
129135
config_set.add_constant(keys="path", desc="desc2", value="value")
130136

131137

132138
def test_config_redefine_dynamic_fail(conf_builder: ConfBuilder) -> None:
133139
config_set = conf_builder("path = path")
134140
config_set.add_config(keys="path", of_type=str, default="default_1", desc="path")
135-
with pytest.raises(ValueError, match="config path already defined"):
141+
msg = (
142+
"duplicate configuration definition for py39:\n"
143+
"has: ConfigDynamicDefinition(keys=('path',), desc=path, of_type=<class 'str'>, default=default_1)\n"
144+
"new: ConfigDynamicDefinition(keys=('path',), desc=path, of_type=<class 'str'>, default=default_2)"
145+
)
146+
with pytest.raises(ValueError, match=re.escape(msg)):
136147
config_set.add_config(keys="path", of_type=str, default="default_2", desc="path")
137148

138149

tests/session/cmd/test_sequential.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,7 @@ def test_result_json_sequential(
9494
assert "result" not in log_report["testenvs"][".pkg"]
9595

9696
assert packaging_setup[-1][0] in {0, None}
97-
assert packaging_setup[-1][1] == "_exit"
98-
assert packaging_setup[:-1] == [
97+
assert packaging_setup == [
9998
(0, "install_requires"),
10099
(None, "_optional_hooks"),
101100
(None, "get_requires_for_build_wheel"),
@@ -303,7 +302,6 @@ def test_skip_develop_mode(tox_project: ToxProjectCreator, demo_pkg_setuptools:
303302
(".pkg", "install_requires_for_build_editable"),
304303
(".pkg", "build_editable"),
305304
("py", "install_package"),
306-
(".pkg", "_exit"),
307305
]
308306
assert calls == expected
309307

tests/tox_env/python/virtual_env/package/test_package_cmd_builder.py

-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ def test_tox_install_pkg_sdist(tox_project: ToxProjectCreator, pkg_with_extras_p
7171
(".pkg_external_sdist_meta", "prepare_metadata_for_build_wheel", []),
7272
("py", "install_package_deps", deps),
7373
("py", "install_package", ["--force-reinstall", "--no-deps", str(pkg_with_extras_project_sdist)]),
74-
(".pkg_external_sdist_meta", "_exit", []),
7574
]
7675

7776

tests/tox_env/python/virtual_env/test_setuptools.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,5 @@ def test_setuptools_package(
5151
assert len(py_messages) == 5, "\n".join(py_messages) # 1 install wheel + 3 command + 1 final report
5252

5353
package_messages = [i for i in result if ".pkg: " in i]
54-
# 1 optional hooks + 1 install requires + 1 build requires + 1 build meta + 1 build isolated + 1 exit
55-
assert len(package_messages) == 6, "\n".join(package_messages)
54+
# 1 optional hooks + 1 install requires + 1 build requires + 1 build meta + 1 build isolated
55+
assert len(package_messages) == 5, "\n".join(package_messages)

0 commit comments

Comments
 (0)