Skip to content

Commit 1f565b1

Browse files
authored
Better handling of packaging env creation (#1807)
1 parent 05c23cb commit 1f565b1

34 files changed

+537
-362
lines changed

docs/changelog/1782.bugfix.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fixed a bug that crashed tox where calling tox with the recreate flag and when multiple environments were reusing the
2+
same package - by :user:`gaborbernat`.

docs/changelog/1804.bugfix.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
Rework how we handle python packaging environments:
2+
3+
- the base packaging environment changed from ``.package`` to ``.pkg``,
4+
- merged the ``sdist``, ``wheel`` and ``dev`` separate packaging implementations into one, and internally dynamically
5+
pick the one that's needed,
6+
- the base packaging environment always uses the same python environment as tox is installed into,
7+
- the base packaging environment is used to get the metadata of the project (via PEP-517) and to build ``sdist`` and
8+
``dev`` packages,
9+
- for building wheels introduced a new per env configurable option ``wheel_build_env``, if the target python major/minor
10+
and implementation for the run tox environment and the base package tox environment matches set this to ``.pkg``,
11+
otherwise this is ``.pkg-{implementation}{major}{minor}``,
12+
- internally now packaging environments can create further packaging environments they are responsible of managing,
13+
- updated ``depends`` to use the packaging logic,
14+
- add support skip missing interpreters for depends and show config,
15+
16+
by :user:`gaborbernat`.

src/tox/config/sets.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class ConfigSet:
3232
"""A set of configuration that belong together (such as a tox environment settings, core tox settings)"""
3333

3434
def __init__(self, conf: "Config", name: Optional[str]):
35-
self.name = name
35+
self._name = name
3636
self._conf = conf
3737
self.loaders: List[Loader[Any]] = []
3838
self._defined: Dict[str, ConfigDefinition[Any]] = {}
@@ -51,13 +51,13 @@ def add_config(
5151
Add configuration value.
5252
"""
5353
keys_ = self._make_keys(keys)
54-
definition = ConfigDynamicDefinition(keys_, desc, self.name, of_type, default, post_process)
54+
definition = ConfigDynamicDefinition(keys_, desc, self._name, of_type, default, post_process)
5555
result = self._add_conf(keys_, definition)
5656
return cast(ConfigDynamicDefinition[V], result)
5757

5858
def add_constant(self, keys: Union[str, Sequence[str]], desc: str, value: V) -> ConfigConstantDefinition[V]:
5959
keys_ = self._make_keys(keys)
60-
definition = ConfigConstantDefinition(keys_, desc, self.name, value)
60+
definition = ConfigConstantDefinition(keys_, desc, self._name, value)
6161
result = self._add_conf(keys_, definition)
6262
return cast(ConfigConstantDefinition[V], result)
6363

@@ -70,7 +70,7 @@ def _add_conf(self, keys: Sequence[str], definition: ConfigDefinition[V]) -> Con
7070
if key in self._defined:
7171
earlier = self._defined[key]
7272
# core definitions may be defined multiple times as long as all their options match, first defined wins
73-
if self.name is None and definition == earlier:
73+
if self._name is None and definition == earlier:
7474
definition = earlier
7575
else:
7676
raise ValueError(f"config {key} already defined")
@@ -93,7 +93,7 @@ def load(self, item: str, chain: Optional[List[str]] = None) -> Any:
9393
return config_definition(self._conf, item, self.loaders, chain)
9494

9595
def __repr__(self) -> str:
96-
values = (v for v in (f"name={self.name!r}" if self.name else "", f"loaders={self.loaders!r}") if v)
96+
values = (v for v in (f"name={self._name!r}" if self._name else "", f"loaders={self.loaders!r}") if v)
9797
return f"{self.__class__.__name__}({', '.join(values)})"
9898

9999
def __iter__(self) -> Iterator[str]:
@@ -158,6 +158,10 @@ def set_env_post_process(values: SetEnv, config: "Config") -> SetEnv:
158158
post_process=set_env_post_process,
159159
)
160160

161+
@property
162+
def name(self) -> str:
163+
return self._name # type: ignore
164+
161165

162166
__all__ = (
163167
"ConfigSet",

src/tox/plugin/manager.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from tox.tox_env import package as package_api
1414
from tox.tox_env.api import ToxEnv
1515
from tox.tox_env.python.virtual_env import runner
16-
from tox.tox_env.python.virtual_env.package.artifact import dev, sdist, wheel
16+
from tox.tox_env.python.virtual_env.package import api
1717
from tox.tox_env.register import REGISTER, ToxEnvRegister
1818

1919
from . import NAME, spec
@@ -28,9 +28,7 @@ def __init__(self) -> None:
2828
loader_api,
2929
provision,
3030
runner,
31-
dev,
32-
sdist,
33-
wheel,
31+
api,
3432
legacy,
3533
version_flag,
3634
quickstart,

src/tox/pytest.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,16 +193,17 @@ def cmd(self) -> Sequence[str]:
193193

194194
@contextmanager
195195
def _execute_call(
196-
executor: Execute, out_err: OutErr, request: ExecuteRequest, show: bool
196+
self: ToxEnv, executor: Execute, out_err: OutErr, request: ExecuteRequest, show: bool # noqa
197197
) -> Iterator[ExecuteStatus]:
198198
exit_code = handle(request)
199199
if exit_code is not None:
200200
executor = MockExecute(colored=executor._colored, exit_code=exit_code) # noqa
201-
with original_execute_call(executor, out_err, request, show) as status:
201+
with original_execute_call(self, executor, out_err, request, show) as status:
202202
yield status
203203

204204
original_execute_call = ToxEnv._execute_call # noqa
205-
return self.mocker.patch.object(ToxEnv, "_execute_call", side_effect=_execute_call)
205+
result = self.mocker.patch.object(ToxEnv, "_execute_call", side_effect=_execute_call, autospec=True)
206+
return result
206207

207208
@property
208209
def structure(self) -> Dict[str, Any]:

src/tox/session/cmd/depends.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,22 @@ def tox_add_option(parser: ToxParser) -> None:
1717

1818

1919
def depends(state: State) -> int:
20-
to_run_list = list(state.env_list())
20+
to_run_list = list(state.env_list(everything=True))
2121
order, todo = run_order(state, to_run_list)
2222
print(f"Execution order: {', '.join(order)}")
2323

2424
deps: Dict[str, List[str]] = {k: [o for o in order if o in v] for k, v in todo.items()}
2525
deps["ALL"] = to_run_list
2626

2727
def _handle(at: int, env: str) -> None:
28+
if env not in order and env != "ALL": # skipped envs
29+
return
2830
print(" " * at, end="")
2931
print(env, end="")
3032
if env != "ALL":
31-
pkg_env = state.tox_env(env).package_env
32-
if pkg_env is not None:
33-
print(f" ~ {pkg_env.conf['env_name']}", end="")
33+
names = " | ".join(e.conf.name for e in state.tox_env(env).package_envs())
34+
if names:
35+
print(f" ~ {names}", end="")
3436
print("")
3537
at += 1
3638
for dep in deps[env]:

src/tox/session/cmd/run/common.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from tox.session.cmd.run.single import ToxEnvRunResult, run_one
1818
from tox.session.state import State
1919
from tox.tox_env.api import ToxEnv
20+
from tox.tox_env.errors import Skip
2021
from tox.tox_env.runner import RunToxEnv
2122
from tox.util.graph import stable_topological_sort
2223
from tox.util.spinner import MISS_DURATION, Spinner
@@ -230,7 +231,7 @@ def _queue_and_wait(
230231
envs_to_run_generator = ready_to_run_envs(state, to_run_list, completed)
231232

232233
def _run(tox_env: RunToxEnv) -> ToxEnvRunResult:
233-
spinner.add(cast(str, tox_env.conf.name))
234+
spinner.add(tox_env.conf.name)
234235
return run_one(tox_env, options.recreate, options.no_test, suspend_display=live is False)
235236

236237
try:
@@ -256,7 +257,7 @@ def _run(tox_env: RunToxEnv) -> ToxEnvRunResult:
256257
result: ToxEnvRunResult = future.result()
257258
except CancelledError:
258259
tox_env_done.teardown()
259-
name = cast(str, tox_env_done.conf.name)
260+
name = tox_env_done.conf.name
260261
result = ToxEnvRunResult(name=name, skipped=False, code=-3, outcomes=[], duration=MISS_DURATION)
261262
results.append(result)
262263
completed.add(result.name)
@@ -304,8 +305,13 @@ def ready_to_run_envs(state: State, to_run: List[str], completed: Set[str]) -> I
304305

305306
def run_order(state: State, to_run: List[str]) -> Tuple[List[str], Dict[str, Set[str]]]:
306307
to_run_set = set(to_run)
307-
todo: Dict[str, Set[str]] = {
308-
env: (to_run_set & set(cast(EnvList, state.tox_env(env).conf["depends"]).envs)) for env in to_run
309-
}
308+
todo: Dict[str, Set[str]] = {}
309+
for env in to_run:
310+
try:
311+
run_env = state.tox_env(env)
312+
except Skip:
313+
continue
314+
depends = set(cast(EnvList, run_env.conf["depends"]).envs)
315+
todo[env] = to_run_set & depends
310316
order = stable_topological_sort(todo)
311317
return order, todo

src/tox/session/cmd/run/single.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class ToxEnvRunResult(NamedTuple):
2525

2626
def run_one(tox_env: RunToxEnv, recreate: bool, no_test: bool, suspend_display: bool) -> ToxEnvRunResult:
2727
start_one = time.monotonic()
28-
name = cast(str, tox_env.conf.name)
28+
name = tox_env.conf.name
2929
with tox_env.display_context(suspend_display):
3030
skipped, code, outcomes = _evaluate(tox_env, recreate, no_test)
3131
duration = time.monotonic() - start_one

src/tox/session/cmd/show_config.py

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@
33
"""
44

55
from textwrap import indent
6-
from typing import Iterable, List
6+
from typing import Iterable, List, Set
77

88
from tox.config.cli.parser import ToxParser
99
from tox.config.loader.stringify import stringify
1010
from tox.config.sets import ConfigSet
1111
from tox.plugin.impl import impl
1212
from tox.session.common import env_list_flag
1313
from tox.session.state import State
14+
from tox.tox_env.api import ToxEnv
15+
from tox.tox_env.errors import Skip
1416

1517

1618
@impl
@@ -27,20 +29,40 @@ def tox_add_option(parser: ToxParser) -> None:
2729

2830

2931
def show_config(state: State) -> int:
30-
show_core = state.options.env.all or state.options.show_core
3132
keys: List[str] = state.options.list_keys_only
32-
# environments may define core configuration flags, so we must exhaust first the environments to tell the core part
33-
envs = list(state.env_list(everything=False))
34-
for at, name in enumerate(envs):
35-
tox_env = state.tox_env(name)
36-
print(f"[testenv:{name}]")
33+
is_first = True
34+
selected = state.options.env
35+
36+
def _print_env(tox_env: ToxEnv) -> None:
37+
nonlocal is_first
38+
if is_first:
39+
is_first = False
40+
else:
41+
print("")
42+
print(f"[testenv:{tox_env.conf.name}]")
3743
if not keys:
3844
print(f"type = {type(tox_env).__name__}")
3945
print_conf(tox_env.conf, keys)
40-
if show_core or at + 1 != len(envs):
41-
print("")
42-
# no print core
43-
if show_core:
46+
47+
envs = list(state.env_list(everything=True))
48+
done_pkg_envs: Set[str] = set()
49+
for name in envs:
50+
try:
51+
run_env = state.tox_env(name)
52+
except Skip:
53+
run_env = state.tox_env(name) # get again to get the temporary state
54+
if run_env.conf.name in selected:
55+
_print_env(run_env)
56+
for pkg_env in run_env.package_envs():
57+
if pkg_env.conf.name in done_pkg_envs:
58+
continue
59+
done_pkg_envs.add(pkg_env.conf.name)
60+
if pkg_env.conf.name in selected:
61+
_print_env(pkg_env)
62+
63+
# environments may define core configuration flags, so we must exhaust first the environments to tell the core part
64+
if selected.all or state.options.show_core:
65+
print("")
4466
print("[tox]")
4567
print_conf(state.conf.core, keys)
4668
return 0

src/tox/session/common.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ def __eq__(self, other: Any) -> bool:
2727
def __ne__(self, other: Any) -> bool:
2828
return not (self == other)
2929

30+
def __contains__(self, item: str) -> bool:
31+
return self.all or (self._names is not None and item in self._names)
32+
3033

3134
def env_list_flag(parser: ArgumentParser, default: Optional[CliEnv] = None) -> None:
3235
parser.add_argument(

src/tox/session/state.py

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def __init__(
3232
self.args = args
3333

3434
self._run_env: Dict[str, RunToxEnv] = {}
35-
self._pkg_env: Dict[str, PackageToxEnv] = {}
35+
self._pkg_env: Dict[str, Tuple[str, PackageToxEnv]] = {}
3636
self._pkg_env_discovered: Set[str] = set()
3737

3838
self.journal: Journal = Journal(getattr(options, "result_json", None) is not None)
@@ -86,29 +86,33 @@ def _build_run_env(self, env_conf: EnvConfigSet) -> None:
8686
from tox.tox_env.register import REGISTER
8787

8888
builder = REGISTER.runner(runner)
89-
name = cast(str, env_conf.name)
89+
name = env_conf.name
9090
journal = self.journal.get_env_journal(name)
9191
env: RunToxEnv = builder(env_conf, self.conf.core, self.options, journal, self.log_handler)
9292
self._run_env[name] = env
9393
self._build_package_env(env)
9494

9595
def _build_package_env(self, env: RunToxEnv) -> None:
96-
pkg_env_gen = env.set_package_env()
97-
try:
98-
name, packager = next(pkg_env_gen)
99-
except StopIteration:
100-
pass
101-
else:
102-
with self.log_handler.with_context(name):
103-
package_tox_env = self._get_package_env(packager, name)
104-
try:
105-
pkg_env_gen.send(package_tox_env)
106-
except StopIteration:
107-
pass
96+
pkg_env_gen = env.create_package_env()
97+
while True:
98+
try:
99+
name, packager = next(pkg_env_gen)
100+
except StopIteration:
101+
return
102+
else:
103+
with self.log_handler.with_context(name):
104+
package_tox_env = self._get_package_env(packager, name)
105+
try:
106+
pkg_env_gen.send(package_tox_env)
107+
except StopIteration:
108+
return
108109

109110
def _get_package_env(self, packager: str, name: str) -> PackageToxEnv:
110111
if name in self._pkg_env: # if already created reuse
111-
pkg_tox_env: PackageToxEnv = self._pkg_env[name]
112+
old, pkg_tox_env = self._pkg_env[name]
113+
if old != packager: # pragma: no branch # same env name is used by different packaging: dpkg vs virtualenv
114+
msg = f"{name} is already defined as a {old}, cannot be {packager} too" # pragma: no cover
115+
raise HandledError(msg) # pragma: no cover
112116
else:
113117
from tox.tox_env.register import REGISTER
114118

@@ -119,7 +123,7 @@ def _get_package_env(self, packager: str, name: str) -> PackageToxEnv:
119123
pkg_conf = self.conf.get_env(name, package=True)
120124
journal = self.journal.get_env_journal(name)
121125
pkg_tox_env = package_type(pkg_conf, self.conf.core, self.options, journal, self.log_handler)
122-
self._pkg_env[name] = pkg_tox_env
126+
self._pkg_env[name] = packager, pkg_tox_env
123127
return pkg_tox_env
124128

125129

src/tox/tox_env/api.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ def __init__(
4949
self.clean_done = False
5050
self._execute_statuses: Dict[int, ExecuteStatus] = {}
5151
self._interrupted = False
52+
self.skipped = False
5253

5354
def interrupt(self) -> None:
5455
logging.warning("interrupt tox environment: %s", self.conf.name)
@@ -153,7 +154,7 @@ def setup(self) -> None:
153154
if eq is False and old is not None: # recreate if already created and not equals
154155
logging.warning(f"env type changed from {old} to {conf}, will recreate")
155156
raise Recreate # recreate if already exists and type changed
156-
self.setup_done, self.clean_done = True, False
157+
self.setup_done = True
157158
finally:
158159
self._handle_env_tmp_dir()
159160

@@ -270,10 +271,9 @@ def execute_async(
270271
if self.journal and execute_status.outcome is not None:
271272
self.journal.add_execute(execute_status.outcome, run_id)
272273

273-
@staticmethod
274274
@contextmanager
275275
def _execute_call(
276-
executor: Execute, out_err: OutErr, request: ExecuteRequest, show: bool
276+
self, executor: Execute, out_err: OutErr, request: ExecuteRequest, show: bool
277277
) -> Iterator[ExecuteStatus]:
278278
with executor.call(
279279
request=request,
@@ -306,7 +306,7 @@ def close_and_read_out_err(self) -> Optional[Tuple[bytes, bytes]]:
306306

307307
@contextmanager
308308
def log_context(self) -> Iterator[None]:
309-
with self.log_handler.with_context(cast(str, self.conf.name)):
309+
with self.log_handler.with_context(self.conf.name):
310310
yield
311311

312312
def teardown(self) -> None:

src/tox/tox_env/info.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ def reset(self) -> None:
4747
self._content = {}
4848

4949
def _write(self) -> None:
50+
self._path.parent.mkdir(parents=True, exist_ok=True)
5051
self._path.write_text(json.dumps(self._content, indent=2))
5152

5253

0 commit comments

Comments
 (0)