Skip to content

Commit 2f6667f

Browse files
authored
Add Provision Arguments to ToxParser (#3246)
1 parent 380d2e2 commit 2f6667f

File tree

6 files changed

+91
-75
lines changed

6 files changed

+91
-75
lines changed

docs/changelog/3190.bugfix.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add provision arguments to ToxParser to fix crash when provisioning new tox environment without list-dependencies by :user:`seyidaniels`

src/tox/config/cli/parser.py

+70-1
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
import argparse
66
import logging
77
import os
8+
import random
89
import sys
9-
from argparse import SUPPRESS, Action, ArgumentDefaultsHelpFormatter, ArgumentParser, Namespace
10+
from argparse import SUPPRESS, Action, ArgumentDefaultsHelpFormatter, ArgumentError, ArgumentParser, Namespace
1011
from pathlib import Path
1112
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Literal, Optional, Sequence, Tuple, Type, TypeVar, cast
1213

1314
from tox.config.loader.str_convert import StrConvert
1415
from tox.plugin import NAME
16+
from tox.util.ci import is_ci
1517

1618
from .env_var import get_env_var
1719
from .ini import IniConfig
@@ -178,8 +180,75 @@ def add_command(
178180
excl_group = group.add_mutually_exclusive_group(**e_kwargs)
179181
for a_args, _, a_kwargs in arguments:
180182
excl_group.add_argument(*a_args, **a_kwargs)
183+
self._add_provision_arguments(sub_parser)
181184
return sub_parser
182185

186+
def _add_provision_arguments(self, sub_parser: ToxParser) -> None: # noqa: PLR6301
187+
sub_parser.add_argument(
188+
"--result-json",
189+
dest="result_json",
190+
metavar="path",
191+
of_type=Path,
192+
default=None,
193+
help="write a JSON file with detailed information about all commands and results involved",
194+
)
195+
196+
class SeedAction(Action):
197+
def __call__(
198+
self,
199+
parser: ArgumentParser, # noqa: ARG002
200+
namespace: Namespace,
201+
values: str | Sequence[Any] | None,
202+
option_string: str | None = None, # noqa: ARG002
203+
) -> None:
204+
if values == "notset":
205+
result = None
206+
else:
207+
try:
208+
result = int(cast(str, values))
209+
if result <= 0:
210+
msg = "must be greater than zero"
211+
raise ValueError(msg) # noqa: TRY301
212+
except ValueError as exc:
213+
raise ArgumentError(self, str(exc)) from exc
214+
setattr(namespace, self.dest, result)
215+
216+
if os.environ.get("PYTHONHASHSEED", "random") != "random":
217+
hashseed_default = int(os.environ["PYTHONHASHSEED"])
218+
else:
219+
hashseed_default = random.randint(1, 1024 if sys.platform == "win32" else 4294967295) # noqa: S311
220+
sub_parser.add_argument(
221+
"--hashseed",
222+
metavar="SEED",
223+
help="set PYTHONHASHSEED to SEED before running commands. Defaults to a random integer in the range "
224+
"[1, 4294967295] ([1, 1024] on Windows). Passing 'notset' suppresses this behavior.",
225+
action=SeedAction,
226+
of_type=Optional[int], # type: ignore[arg-type]
227+
default=hashseed_default,
228+
dest="hash_seed",
229+
)
230+
sub_parser.add_argument(
231+
"--discover",
232+
dest="discover",
233+
nargs="+",
234+
metavar="path",
235+
help="for Python discovery first try the Python executables under these paths",
236+
default=[],
237+
)
238+
list_deps = sub_parser.add_mutually_exclusive_group()
239+
list_deps.add_argument(
240+
"--list-dependencies",
241+
action="store_true",
242+
default=is_ci(),
243+
help="list the dependencies installed during environment setup",
244+
)
245+
list_deps.add_argument(
246+
"--no-list-dependencies",
247+
action="store_false",
248+
dest="list_dependencies",
249+
help="never list the dependencies installed during environment setup",
250+
)
251+
183252
def add_argument_group(self, *args: Any, **kwargs: Any) -> Any:
184253
result = super().add_argument_group(*args, **kwargs)
185254
if self.of_cmd is None and args not in {("positional arguments",), ("optional arguments",)}:

src/tox/plugin/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""
22
tox uses `pluggy <https://pluggy.readthedocs.io/en/stable/>`_ to customize the default behavior. It provides an
3-
extension mechanism for plugin management an calling hooks.
3+
extension mechanism for plugin management by calling hooks.
44
55
Pluggy discovers a plugin by looking up for entry-points named ``tox``, for example in a pyproject.toml:
66

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

+1-71
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44

55
import logging
66
import os
7-
import random
8-
import sys
97
import time
108
from argparse import Action, ArgumentError, ArgumentParser, Namespace
119
from concurrent.futures import CancelledError, Future, ThreadPoolExecutor, as_completed
@@ -21,7 +19,6 @@
2119
from tox.journal import write_journal
2220
from tox.session.cmd.run.single import ToxEnvRunResult, run_one
2321
from tox.tox_env.runner import RunToxEnv
24-
from tox.util.ci import is_ci
2522
from tox.util.graph import stable_topological_sort
2623
from tox.util.spinner import MISS_DURATION, Spinner
2724

@@ -62,17 +59,8 @@ def __call__(
6259
setattr(namespace, self.dest, path)
6360

6461

65-
def env_run_create_flags(parser: ArgumentParser, mode: str) -> None: # noqa: C901
62+
def env_run_create_flags(parser: ArgumentParser, mode: str) -> None:
6663
# mode can be one of: run, run-parallel, legacy, devenv, config
67-
if mode not in {"config", "depends"}:
68-
parser.add_argument(
69-
"--result-json",
70-
dest="result_json",
71-
metavar="path",
72-
of_type=Path,
73-
default=None,
74-
help="write a JSON file with detailed information about all commands and results involved",
75-
)
7664
if mode not in {"devenv", "depends"}:
7765
parser.add_argument(
7866
"-s",
@@ -114,71 +102,13 @@ def env_run_create_flags(parser: ArgumentParser, mode: str) -> None: # noqa: C9
114102
help="install package in development mode",
115103
dest="develop",
116104
)
117-
if mode != "depends":
118-
119-
class SeedAction(Action):
120-
def __call__(
121-
self,
122-
parser: ArgumentParser, # noqa: ARG002
123-
namespace: Namespace,
124-
values: str | Sequence[Any] | None,
125-
option_string: str | None = None, # noqa: ARG002
126-
) -> None:
127-
if values == "notset":
128-
result = None
129-
else:
130-
try:
131-
result = int(cast(str, values))
132-
if result <= 0:
133-
msg = "must be greater than zero"
134-
raise ValueError(msg) # noqa: TRY301
135-
except ValueError as exc:
136-
raise ArgumentError(self, str(exc)) from exc
137-
setattr(namespace, self.dest, result)
138-
139-
if os.environ.get("PYTHONHASHSEED", "random") != "random":
140-
hashseed_default = int(os.environ["PYTHONHASHSEED"])
141-
else:
142-
hashseed_default = random.randint(1, 1024 if sys.platform == "win32" else 4294967295) # noqa: S311
143-
144-
parser.add_argument(
145-
"--hashseed",
146-
metavar="SEED",
147-
help="set PYTHONHASHSEED to SEED before running commands. Defaults to a random integer in the range "
148-
"[1, 4294967295] ([1, 1024] on Windows). Passing 'notset' suppresses this behavior.",
149-
action=SeedAction,
150-
of_type=Optional[int],
151-
default=hashseed_default,
152-
dest="hash_seed",
153-
)
154-
parser.add_argument(
155-
"--discover",
156-
dest="discover",
157-
nargs="+",
158-
metavar="path",
159-
help="for Python discovery first try the Python executables under these paths",
160-
default=[],
161-
)
162105
if mode != "depends":
163106
parser.add_argument(
164107
"--no-recreate-pkg",
165108
dest="no_recreate_pkg",
166109
help="if recreate is set do not recreate packaging tox environment(s)",
167110
action="store_true",
168111
)
169-
list_deps = parser.add_mutually_exclusive_group()
170-
list_deps.add_argument(
171-
"--list-dependencies",
172-
action="store_true",
173-
default=is_ci(),
174-
help="list the dependencies installed during environment setup",
175-
)
176-
list_deps.add_argument(
177-
"--no-list-dependencies",
178-
action="store_false",
179-
dest="list_dependencies",
180-
help="never list the dependencies installed during environment setup",
181-
)
182112
if mode not in {"devenv", "config", "depends"}:
183113
parser.add_argument(
184114
"--skip-pkg-install",

tests/test_provision.py

+16
Original file line numberDiff line numberDiff line change
@@ -229,3 +229,19 @@ def test_provision_conf_file(tox_project: ToxProjectCreator, tmp_path: Path, rel
229229
conf_path = str(Path(project.path.name) / "tox.ini") if relative_path else str(project.path / "tox.ini")
230230
result = project.run("c", "--conf", conf_path, "-e", "py", from_cwd=tmp_path)
231231
result.assert_success()
232+
233+
234+
@pytest.mark.parametrize("subcommand", ["r", "p", "de", "l", "d", "c", "q", "e", "le"])
235+
def test_provision_default_arguments_exists(tox_project: ToxProjectCreator, subcommand: str) -> None:
236+
ini = r"""
237+
[tox]
238+
requires =
239+
tox<4.14
240+
[testenv]
241+
package = skip
242+
"""
243+
project = tox_project({"tox.ini": ini})
244+
project.patch_execute(lambda r: 0 if "install" in r.run_id else None)
245+
outcome = project.run(subcommand)
246+
for argument in ["result_json", "hash_seed", "discover", "list_dependencies"]:
247+
assert hasattr(outcome.state.conf.options, argument)

tests/tox_env/python/test_python_api.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ def test_python_hash_seed_from_env_and_disable(tox_project: ToxProjectCreator) -
255255

256256
@pytest.mark.parametrize("in_ci", [True, False])
257257
def test_list_installed_deps(in_ci: bool, tox_project: ToxProjectCreator, mocker: MockerFixture) -> None:
258-
mocker.patch("tox.session.cmd.run.common.is_ci", return_value=in_ci)
258+
mocker.patch("tox.config.cli.parser.is_ci", return_value=in_ci)
259259
result = tox_project({"tox.ini": "[testenv]\nskip_install = true"}).run("r", "-e", "py")
260260
if in_ci:
261261
assert "pip==" in result.out
@@ -271,7 +271,7 @@ def test_list_installed_deps_explicit_cli(
271271
tox_project: ToxProjectCreator,
272272
mocker: MockerFixture,
273273
) -> None:
274-
mocker.patch("tox.session.cmd.run.common.is_ci", return_value=in_ci)
274+
mocker.patch("tox.config.cli.parser.is_ci", return_value=in_ci)
275275
result = tox_project({"tox.ini": "[testenv]\nskip_install = true"}).run(list_deps, "r", "-e", "py")
276276
if list_deps == "--list-dependencies":
277277
assert "pip==" in result.out

0 commit comments

Comments
 (0)