Skip to content

Commit c5013f3

Browse files
authored
Better handling of build backend without editable support and add --exit-and-dump-after flag (#2597)
Resolves #2567 Resolves #2595
1 parent 4c77457 commit c5013f3

File tree

15 files changed

+75
-32
lines changed

15 files changed

+75
-32
lines changed

docs/changelog/2595.bugfix.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix leaking backend processes when the build backend does not support editable wheels and fix failure when multiple
2+
environments exist that have a build backend that does not support editable wheels - by :user:`gaborbernat`.

docs/changelog/2595.feature.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add ``--exit-and-dump-after`` flag that allows automatically killing tox if does not finish within the passed seconds,
2+
and dump the thread stacks (useful to debug tox when it seemingly hangs) - by :user:`gaborbernat`.

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ dependencies = [
2929
"pluggy>=1",
3030
"pyproject-api>=1.2.1",
3131
'tomli>=2.0.1; python_version < "3.11"',
32-
"virtualenv>=20.17",
33-
"filelock>=3.8.1",
32+
"virtualenv>=20.17.1",
33+
"filelock>=3.8.2",
3434
'importlib-metadata>=5.1; python_version < "3.8"',
3535
'typing-extensions>=4.4; python_version < "3.8"',
3636
]

src/tox/config/cli/parser.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ def is_colored(self) -> bool:
132132
""":return: flag indicating if the output is colored or not"""
133133
return cast(bool, self.colored == "yes")
134134

135+
exit_and_dump_after: int
136+
135137

136138
ArgumentArgs = Tuple[Tuple[str, ...], Optional[Type[Any]], Dict[str, Any]]
137139

@@ -305,9 +307,21 @@ def add_color_flags(parser: ArgumentParser) -> None:
305307
)
306308

307309

310+
def add_exit_and_dump_after(parser: ArgumentParser) -> None:
311+
parser.add_argument(
312+
"--exit-and-dump-after",
313+
dest="exit_and_dump_after",
314+
metavar="seconds",
315+
default=0,
316+
type=int,
317+
help="dump tox threads after n seconds and exit the app - useful to debug when tox hangs, 0 means disabled",
318+
)
319+
320+
308321
def add_core_arguments(parser: ArgumentParser) -> None:
309322
add_color_flags(parser)
310323
add_verbosity_flags(parser)
324+
add_exit_and_dump_after(parser)
311325
parser.add_argument(
312326
"-c",
313327
"--conf",

src/tox/execute/pep517_backend.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ def close(self) -> None:
9090
execute.process.wait(timeout=0.1) # pragma: no cover
9191
except TimeoutExpired: # pragma: no cover
9292
execute.process.terminate() # pragma: no cover # if does not stop on its own kill it
93+
self.is_alive = False
9394

9495

9596
class LocalSubProcessPep517ExecuteInstance(ExecuteInstance):

src/tox/run.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Main entry point for tox."""
22
from __future__ import annotations
33

4+
import faulthandler
45
import logging
56
import os
67
import sys
@@ -51,6 +52,8 @@ def setup_state(args: Sequence[str]) -> State:
5152
# parse CLI arguments
5253
options = get_options(*args)
5354
options.parsed.start = start
55+
if options.parsed.exit_and_dump_after:
56+
faulthandler.dump_traceback_later(timeout=options.parsed.exit_and_dump_after, exit=True) # pragma: no cover
5457
# build tox environment config objects
5558
state = State(options, args)
5659
return state

src/tox/tox_env/package.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,21 @@ def __str__(self) -> str:
3333
return str(self.path)
3434

3535

36+
locked = False
37+
38+
3639
def _lock_method(thread_lock: RLock, file_lock: FileLock | None, meth: Callable[..., Any]) -> Callable[..., Any]:
3740
def _func(*args: Any, **kwargs: Any) -> Any:
3841
with thread_lock:
42+
file_locks = False
3943
if file_lock is not None and file_lock.is_locked is False: # file_lock is to lock from other tox processes
4044
file_lock.acquire()
45+
file_locks = True
4146
try:
4247
return meth(*args, **kwargs)
4348
finally:
44-
if file_lock is not None:
45-
file_lock.release()
49+
if file_locks:
50+
cast(FileLock, file_lock).release()
4651

4752
return _func
4853

@@ -94,8 +99,7 @@ def mark_active_run_env(self, run_env: RunToxEnv) -> None:
9499

95100
def teardown_env(self, conf: EnvConfigSet) -> None:
96101
self._envs.remove(conf.name)
97-
has_envs = bool(self._envs)
98-
if not has_envs:
102+
if len(self._envs) == 0:
99103
self._teardown()
100104

101105
@abstractmethod

src/tox/tox_env/python/api.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,9 @@ def extract_base_python(env_name: str) -> str | None:
108108
candidates: list[str] = []
109109
for factor in env_name.split("-"):
110110
spec = PythonSpec.from_string_spec(factor)
111-
if spec.implementation is not None:
112-
if spec.implementation.lower() in INTERPRETER_SHORT_NAMES and env_name is not None:
113-
candidates.append(factor)
111+
impl = spec.implementation or "python"
112+
if impl.lower() in INTERPRETER_SHORT_NAMES and env_name is not None and spec.path is None:
113+
candidates.append(factor)
114114
if candidates:
115115
if len(candidates) > 1:
116116
raise ValueError(f"conflicting factors {', '.join(candidates)} in {env_name}")
@@ -123,18 +123,19 @@ def _validate_base_python(env_name: str, base_pythons: list[str], ignore_base_py
123123
elements.update(env_name.split("-")) # and also any factor
124124
for candidate in elements:
125125
spec_name = PythonSpec.from_string_spec(candidate)
126-
if spec_name.implementation is not None and spec_name.implementation.lower() in ("pypy", "cpython"):
127-
for base_python in base_pythons:
128-
spec_base = PythonSpec.from_string_spec(base_python)
129-
if any(
130-
getattr(spec_base, key) != getattr(spec_name, key)
131-
for key in ("implementation", "major", "minor", "micro", "architecture")
132-
if getattr(spec_base, key) is not None and getattr(spec_name, key) is not None
133-
):
134-
msg = f"env name {env_name} conflicting with base python {base_python}"
135-
if ignore_base_python_conflict:
136-
return [env_name] # ignore the base python settings
137-
raise Fail(msg)
126+
if spec_name.implementation and spec_name.implementation.lower() not in INTERPRETER_SHORT_NAMES:
127+
continue
128+
for base_python in base_pythons:
129+
spec_base = PythonSpec.from_string_spec(base_python)
130+
if any(
131+
getattr(spec_base, key) != getattr(spec_name, key)
132+
for key in ("implementation", "major", "minor", "micro", "architecture")
133+
if getattr(spec_base, key) is not None and getattr(spec_name, key) is not None
134+
):
135+
msg = f"env name {env_name} conflicting with base python {base_python}"
136+
if ignore_base_python_conflict:
137+
return [env_name] # ignore the base python settings
138+
raise Fail(msg)
138139
return base_pythons
139140

140141
@abstractmethod

src/tox/tox_env/python/package.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,10 @@ def child_pkg_envs(self, run_conf: EnvConfigSet) -> Iterator[PackageToxEnv]:
107107
env = self._wheel_build_envs.get(run_conf["wheel_build_env"])
108108
if env is not None and env.name != self.name:
109109
yield env
110+
111+
def _teardown(self) -> None:
112+
for env in self._wheel_build_envs.values():
113+
if env is not self:
114+
with env.display_context(self._has_display_suspended):
115+
env.teardown()
116+
super()._teardown()

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import logging
44
import os
55
import sys
6+
from collections import defaultdict
67
from contextlib import contextmanager
78
from pathlib import Path
89
from threading import RLock
@@ -92,7 +93,7 @@ class Pep517VirtualEnvPackager(PythonPackageToxEnv, VirtualEnv):
9293
def __init__(self, create_args: ToxEnvCreateArgs) -> None:
9394
super().__init__(create_args)
9495
self._frontend_: Pep517VirtualEnvFrontend | None = None
95-
self.builds: set[str] = set()
96+
self.builds: defaultdict[str, list[EnvConfigSet]] = defaultdict(list)
9697
self._distribution_meta: PathDistribution | None = None
9798
self._package_dependencies: list[Requirement] | None = None
9899
self._package_name: str | None = None
@@ -136,7 +137,8 @@ def meta_folder(self) -> Path:
136137

137138
def register_run_env(self, run_env: RunToxEnv) -> Generator[tuple[str, str], PackageToxEnv, None]:
138139
yield from super().register_run_env(run_env)
139-
self.builds.add(run_env.conf["package"])
140+
build_type = run_env.conf["package"]
141+
self.builds[build_type].append(run_env.conf)
140142

141143
def _setup_env(self) -> None:
142144
super()._setup_env()
@@ -169,13 +171,15 @@ def perform_packaging(self, for_env: EnvConfigSet) -> list[Package]:
169171
try:
170172
deps = self._load_deps(for_env)
171173
except BuildEditableNotSupported:
174+
targets = [e for e in self.builds.pop("editable") if e["package"] == "editable"]
175+
names = ", ".join(sorted({t.env_name for t in targets if t.env_name}))
172176
logging.error(
173-
f"package config for {for_env.env_name} is editable, however the build backend {self._frontend.backend}"
177+
f"package config for {names} is editable, however the build backend {self._frontend.backend}"
174178
f" does not support PEP-660, falling back to editable-legacy - change your configuration to it",
175179
)
176-
self.builds.remove("editable")
177-
self.builds.add("editable-legacy")
178-
for_env._defined["package"].value = "editable-legacy" # type: ignore
180+
for env in targets:
181+
env._defined["package"].value = "editable-legacy" # type: ignore
182+
self.builds["editable-legacy"].append(env)
179183
deps = self._load_deps(for_env)
180184
of_type: str = for_env["package"]
181185
if of_type == "editable-legacy":

0 commit comments

Comments
 (0)