Skip to content

Commit 174fd08

Browse files
Add support for exec subcommand (#2142)
* Add support for exec subcommand Run arbitrary commands within your tox environment. Don't abuse it. Signed-off-by: Bernát Gábor <[email protected]> * Update docs/changelog/1790.feature.rst Co-authored-by: Jürgen Gmach <[email protected]> * reindent Signed-off-by: Bernát Gábor <[email protected]> Co-authored-by: Jürgen Gmach <[email protected]>
1 parent 233c493 commit 174fd08

File tree

8 files changed

+88
-17
lines changed

8 files changed

+88
-17
lines changed

docs/changelog/1790.feature.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add ``exec`` subcommand that allows users to run an arbitrary command within the tox environment (without needing to
2+
modify their configuration) - by :user:`gaborbernat`.

src/tox/plugin/manager.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from tox.config.loader import api as loader_api
99
from tox.config.sets import ConfigSet
1010
from tox.session import state
11-
from tox.session.cmd import depends, devenv, legacy, list_env, quickstart, show_config, version_flag
11+
from tox.session.cmd import depends, devenv, exec_, legacy, list_env, quickstart, show_config, version_flag
1212
from tox.session.cmd.run import parallel, sequential
1313
from tox.tox_env import package as package_api
1414
from tox.tox_env.python.virtual_env import runner
@@ -31,6 +31,7 @@ def __init__(self) -> None:
3131
api,
3232
legacy,
3333
version_flag,
34+
exec_,
3435
quickstart,
3536
show_config,
3637
devenv,

src/tox/session/cmd/devenv.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,10 @@
1212

1313
@impl
1414
def tox_add_option(parser: ToxParser) -> None:
15-
our = parser.add_command(
16-
"devenv",
17-
["d"],
18-
"sets up a development environment at ENVDIR based on the tox configuration specified ",
19-
devenv,
20-
)
15+
help_msg = "sets up a development environment at ENVDIR based on the tox configuration specified "
16+
our = parser.add_command("devenv", ["d"], help_msg, devenv)
2117
our.add_argument("devenv_path", metavar="path", default=Path("venv").absolute(), nargs="?")
22-
env_list_flag(our, default=CliEnv("py"))
18+
env_list_flag(our, default=CliEnv("py"), multiple=False)
2319
env_run_create_flags(our, mode="devenv")
2420

2521

src/tox/session/cmd/exec_.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""
2+
Execute a command in a tox environment.
3+
"""
4+
from tox.config.cli.parser import ToxParser
5+
from tox.config.loader.memory import MemoryLoader
6+
from tox.config.types import Command
7+
from tox.plugin import impl
8+
from tox.report import HandledError
9+
from tox.session.cmd.run.common import env_run_create_flags
10+
from tox.session.cmd.run.sequential import run_sequential
11+
from tox.session.common import CliEnv, env_list_flag
12+
from tox.session.state import State
13+
14+
15+
@impl
16+
def tox_add_option(parser: ToxParser) -> None:
17+
our = parser.add_command("exec", ["e"], "execute an arbitrary command within a tox environment", exec_)
18+
our.epilog = "For example: tox exec -e py39 -- python --version"
19+
env_list_flag(our, default=CliEnv("py"), multiple=False)
20+
env_run_create_flags(our, mode="exec")
21+
22+
23+
def exec_(state: State) -> int:
24+
if not state.conf.pos_args:
25+
raise HandledError("You must specify a command as positional arguments, use -- <command>")
26+
env_list = list(state.env_list(everything=False))
27+
if len(env_list) != 1:
28+
raise HandledError(f"exactly one target environment allowed in exec mode but found {', '.join(env_list)}")
29+
loader = MemoryLoader( # these configuration values are loaded from in-memory always (no file conf)
30+
commands_pre=[],
31+
commands=[Command(list(state.conf.pos_args))],
32+
commands_post=[],
33+
)
34+
state.conf.get_env(env_list[0], loaders=[loader])
35+
return run_sequential(state)

src/tox/session/common.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,10 @@ def use_default_list(self) -> bool:
3939
return len(list(self)) == 0
4040

4141

42-
def env_list_flag(parser: ArgumentParser, default: Optional[CliEnv] = None) -> None:
43-
parser.add_argument(
44-
"-e",
45-
dest="env",
46-
help="tox environment(s) to run (ALL -> all environments, not set -> <env_list>)",
47-
default=CliEnv() if default is None else default,
48-
type=CliEnv,
42+
def env_list_flag(parser: ArgumentParser, default: Optional[CliEnv] = None, multiple: bool = True) -> None:
43+
help_msg = (
44+
"tox environment(s) to run (ALL -> all environments, not set -> <env_list>)"
45+
if multiple
46+
else "tox environment to run"
4947
)
48+
parser.add_argument("-e", dest="env", help=help_msg, default=CliEnv() if default is None else default, type=CliEnv)

tests/config/cli/conftest.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from tox.session.cmd.depends import depends
66
from tox.session.cmd.devenv import devenv
7+
from tox.session.cmd.exec_ import exec_
78
from tox.session.cmd.legacy import legacy
89
from tox.session.cmd.list_env import list_env
910
from tox.session.cmd.quickstart import quickstart
@@ -32,4 +33,6 @@ def core_handlers() -> Dict[str, Callable[[State], int]]:
3233
"depends": depends,
3334
"le": legacy,
3435
"legacy": legacy,
36+
"e": exec_,
37+
"exec": exec_,
3538
}

tests/session/cmd/test_exec_.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import sys
2+
from typing import List
3+
4+
import pytest
5+
6+
from tox.pytest import ToxProjectCreator
7+
8+
9+
@pytest.mark.parametrize("trail", [[], ["--"]], ids=["no_posargs", "empty_posargs"])
10+
def test_exec_fail_no_posargs(tox_project: ToxProjectCreator, trail: List[str]) -> None:
11+
outcome = tox_project({"tox.ini": ""}).run("e", "-e", "py39,py38", *trail)
12+
outcome.assert_failed()
13+
msg = "ROOT: HandledError| You must specify a command as positional arguments, use -- <command>\n"
14+
outcome.assert_out_err(msg, "")
15+
16+
17+
def test_exec_fail_multiple_target(tox_project: ToxProjectCreator) -> None:
18+
outcome = tox_project({"tox.ini": ""}).run("e", "-e", "py39,py38", "--", "py")
19+
outcome.assert_failed()
20+
msg = "ROOT: HandledError| exactly one target environment allowed in exec mode but found py39, py38\n"
21+
outcome.assert_out_err(msg, "")
22+
23+
24+
@pytest.mark.parametrize("exit_code", [1, 0])
25+
def test_exec(tox_project: ToxProjectCreator, exit_code: int) -> None: # noqa: U100
26+
prj = tox_project({"tox.ini": "[testenv]\npackage=skip"})
27+
py_cmd = f"import sys; print(sys.version); raise SystemExit({exit_code})"
28+
outcome = prj.run("e", "-e", "py", "--", "python", "-c", py_cmd)
29+
if exit_code:
30+
outcome.assert_failed()
31+
else:
32+
outcome.assert_success()
33+
assert sys.version in outcome.out

whitelist.txt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ canonicalize
2020
capfd
2121
caplog
2222
capsys
23-
cfg
23+
Cfg
2424
changelog
2525
chardet
2626
chdir
@@ -67,13 +67,14 @@ entrypoints
6767
envs
6868
epilog
6969
eq
70-
eval
70+
Eval
7171
exc
7272
exe
7373
executables
7474
expr
7575
extlinks
7676
extractall
77+
fallbacks
7778
favicon
7879
filelock
7980
fixup
@@ -147,6 +148,7 @@ platformdirs
147148
pluggy
148149
Popen
149150
pos
151+
posargs
150152
posix
151153
prepend
152154
prereleases

0 commit comments

Comments
 (0)