diff options
author | Bernát Gábor <bgabor8@bloomberg.net> | 2021-08-15 08:55:52 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-08-15 08:55:52 +0100 |
commit | 4a020e715ca17cf2e04413a8491fbc535dd62b0d (patch) | |
tree | e2778489cc360b0a74e2ea84586cd02769ee9a36 | |
parent | c8f8f502a609e7dc6670192a399517e8aad9f010 (diff) | |
download | tox-git-4a020e715ca17cf2e04413a8491fbc535dd62b0d.tar.gz |
Add support for args_are_paths (#2157)
Signed-off-by: Bernát Gábor <gaborjbernat@gmail.com>
-rw-r--r-- | docs/changelog/2122.feature.rst | 1 | ||||
-rw-r--r-- | docs/config.rst | 8 | ||||
-rw-r--r-- | src/tox/config/loader/ini/replace.py | 16 | ||||
-rw-r--r-- | src/tox/config/main.py | 21 | ||||
-rw-r--r-- | src/tox/pytest.py | 8 | ||||
-rw-r--r-- | src/tox/session/cmd/exec_.py | 14 | ||||
-rw-r--r-- | src/tox/tox_env/runner.py | 6 | ||||
-rw-r--r-- | tests/config/test_main.py | 28 | ||||
-rw-r--r-- | tests/session/cmd/test_exec_.py | 2 |
9 files changed, 89 insertions, 15 deletions
diff --git a/docs/changelog/2122.feature.rst b/docs/changelog/2122.feature.rst new file mode 100644 index 00000000..f73014ff --- /dev/null +++ b/docs/changelog/2122.feature.rst @@ -0,0 +1 @@ +Add support for :ref:`args_are_paths` flag - by :user:`gaborbernat`. diff --git a/docs/config.rst b/docs/config.rst index 9d3f8479..a55b7ba3 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -362,6 +362,14 @@ Run Change to this working directory when executing the test command. If the directory does not exist yet, it will be created (required for Windows to be able to execute any command). +.. conf:: + :keys: args_are_paths + :default: False + + Treat positional arguments passed to tox as file system paths and - if they exist on the filesystem and are in + relative format - rewrite them according to the current and :ref:`change_dir` working directory. This handles + automatically transforming relative paths specified on the CLI to relative paths respective of the commands executing + directory. .. conf:: :keys: ignore_errors diff --git a/src/tox/config/loader/ini/replace.py b/src/tox/config/loader/ini/replace.py index 06facf61..ca8c4a32 100644 --- a/src/tox/config/loader/ini/replace.py +++ b/src/tox/config/loader/ini/replace.py @@ -5,7 +5,8 @@ import os import re import sys from configparser import SectionProxy -from typing import TYPE_CHECKING, Iterator, List, Optional, Sequence, Tuple, Union +from pathlib import Path +from typing import TYPE_CHECKING, Iterator, List, Optional, Tuple, Union from tox.config.loader.stringify import stringify from tox.config.set_env import SetEnv @@ -83,7 +84,7 @@ def _replace_match( elif of_type == "tty": replace_value = replace_tty(args) elif of_type == "posargs": - replace_value = replace_pos_args(args, conf.pos_args) + replace_value = replace_pos_args(conf, current_env, args) else: replace_value = replace_reference(conf, current_env, loader, value, chain) return replace_value @@ -172,7 +173,16 @@ def _config_value_sources( yield value -def replace_pos_args(args: List[str], pos_args: Optional[Sequence[str]]) -> str: +def replace_pos_args(conf: "Config", env_name: Optional[str], args: List[str]) -> str: + to_path: Optional[Path] = None + if env_name is not None: # pragma: no branch + env_conf = conf.get_env(env_name) + try: + if env_conf["args_are_paths"]: # pragma: no branch + to_path = env_conf["change_dir"] + except KeyError: + pass + pos_args = conf.pos_args(to_path) if pos_args is None: replace_value = ":".join(args) # if we use the defaults join back remaining args else: diff --git a/src/tox/config/main.py b/src/tox/config/main.py index b4f0b71b..60d4107c 100644 --- a/src/tox/config/main.py +++ b/src/tox/config/main.py @@ -1,3 +1,4 @@ +import os from collections import OrderedDict, defaultdict from pathlib import Path from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Optional, Sequence, Tuple @@ -35,9 +36,23 @@ class Config: self._core_set: Optional[CoreConfigSet] = None self.register_config_set: Callable[[str, EnvConfigSet], Any] = lambda n, e: None - @property - def pos_args(self) -> Optional[Tuple[str, ...]]: - """:return: positional arguments""" + def pos_args(self, to_path: Optional[Path]) -> Optional[Tuple[str, ...]]: + """ + :param to_path: if not None rewrite relative posargs paths from cwd to to_path + :return: positional argument + """ + if self._pos_args is not None and to_path is not None and Path.cwd() != to_path: + args = [] + to_path_str = os.path.abspath(str(to_path)) # we use os.path to unroll .. in path without resolve + for arg in self._pos_args: + path_arg = Path(arg) + if path_arg.exists() and not path_arg.is_absolute(): + path_arg_str = os.path.abspath(str(path_arg)) # we use os.path to unroll .. in path without resolve + relative = os.path.relpath(path_arg_str, to_path_str) # we use os.path to not fail when not within + args.append(relative) + else: + args.append(arg) + return tuple(args) return self._pos_args @property diff --git a/src/tox/pytest.py b/src/tox/pytest.py index 1516297c..d50dc40e 100644 --- a/src/tox/pytest.py +++ b/src/tox/pytest.py @@ -229,16 +229,16 @@ class ToxProject: return result @contextmanager - def chdir(self) -> Iterator[None]: + def chdir(self, to: Optional[Path] = None) -> Iterator[None]: cur_dir = os.getcwd() - os.chdir(str(self.path)) + os.chdir(str(to or self.path)) try: yield finally: os.chdir(cur_dir) - def run(self, *args: str) -> "ToxRunOutcome": - with self.chdir(): + def run(self, *args: str, from_cwd: Optional[Path] = None) -> "ToxRunOutcome": + with self.chdir(from_cwd): state = None self._capfd.readouterr() # start with a clean state - drain code = None diff --git a/src/tox/session/cmd/exec_.py b/src/tox/session/cmd/exec_.py index fc89e258..248ca5c1 100644 --- a/src/tox/session/cmd/exec_.py +++ b/src/tox/session/cmd/exec_.py @@ -1,6 +1,9 @@ """ Execute a command in a tox environment. """ +from pathlib import Path +from typing import Optional + from tox.config.cli.parser import ToxParser from tox.config.loader.memory import MemoryLoader from tox.config.types import Command @@ -21,15 +24,18 @@ def tox_add_option(parser: ToxParser) -> None: def exec_(state: State) -> int: - if not state.conf.pos_args: - raise HandledError("You must specify a command as positional arguments, use -- <command>") env_list = list(state.env_list(everything=False)) if len(env_list) != 1: raise HandledError(f"exactly one target environment allowed in exec mode but found {', '.join(env_list)}") loader = MemoryLoader( # these configuration values are loaded from in-memory always (no file conf) commands_pre=[], - commands=[Command(list(state.conf.pos_args))], + commands=[], commands_post=[], ) - state.conf.get_env(env_list[0], loaders=[loader]) + conf = state.conf.get_env(env_list[0], loaders=[loader]) + to_path: Optional[Path] = conf["change_dir"] if conf["args_are_paths"] else None + pos_args = state.conf.pos_args(to_path) + if not pos_args: + raise HandledError("You must specify a command as positional arguments, use -- <command>") + loader.raw["commands"] = [Command(list(pos_args))] return run_sequential(state) diff --git a/src/tox/tox_env/runner.py b/src/tox/tox_env/runner.py index 57211427..5483feef 100644 --- a/src/tox/tox_env/runner.py +++ b/src/tox/tox_env/runner.py @@ -69,6 +69,12 @@ class RunToxEnv(ToxEnv, ABC): desc="change to this working directory when executing the test command", ) self.conf.add_config( + keys=["args_are_paths"], + of_type=bool, + default=True, + desc="if True rewrite relative posargs paths from cwd to change_dir", + ) + self.conf.add_config( keys=["ignore_errors"], of_type=bool, default=False, diff --git a/tests/config/test_main.py b/tests/config/test_main.py index ed7b8f4e..f2fa6dc1 100644 --- a/tests/config/test_main.py +++ b/tests/config/test_main.py @@ -1,8 +1,11 @@ +import os + from tests.conftest import ToxIniCreator from tox.config.loader.api import Override from tox.config.loader.memory import MemoryLoader from tox.config.main import Config from tox.config.sets import ConfigSet +from tox.pytest import ToxProjectCreator def test_empty_config_repr(empty_config: Config) -> None: @@ -53,3 +56,28 @@ def test_config_new_source(tox_ini_conf: ToxIniCreator) -> None: conf = main_conf.get_env("py", loaders=[MemoryLoader(c="something_else")]) conf.add_config("c", of_type=str, default="d", desc="desc") assert conf["c"] == "something_else" + + +def test_args_are_paths_when_disabled(tox_project: ToxProjectCreator) -> None: + ini = "[testenv]\npackage=skip\ncommands={posargs}\nargs_are_paths=False" + project = tox_project({"tox.ini": ini, "w": {"a.txt": "a"}}) + args = "magic.py", str(project.path), f"..{os.sep}tox.ini", "..", f"..{os.sep}.." + result = project.run("c", "-e", "py", "-k", "commands", "--", *args, from_cwd=project.path / "w") + result.assert_success() + assert result.out == f"[testenv:py]\ncommands = magic.py {project.path} ..{os.sep}tox.ini .. ..{os.sep}..\n" + + +def test_args_are_paths_when_from_child_dir(tox_project: ToxProjectCreator) -> None: + project = tox_project({"tox.ini": "[testenv]\npackage=skip\ncommands={posargs}", "w": {"a.txt": "a"}}) + args = "magic.py", str(project.path), f"..{os.sep}tox.ini", "..", f"..{os.sep}.." + result = project.run("c", "-e", "py", "-k", "commands", "--", *args, from_cwd=project.path / "w") + result.assert_success() + assert result.out == f"[testenv:py]\ncommands = magic.py {project.path} tox.ini . ..\n" + + +def test_args_are_paths_when_with_change_dir(tox_project: ToxProjectCreator) -> None: + project = tox_project({"tox.ini": "[testenv]\npackage=skip\ncommands={posargs}\nchange_dir=w", "w": {"a.txt": "a"}}) + args = "magic.py", str(project.path), "tox.ini", f"w{os.sep}a.txt", "w", "." + result = project.run("c", "-e", "py", "-k", "commands", "--", *args) + result.assert_success() + assert result.out == f"[testenv:py]\ncommands = magic.py {project.path} ..{os.sep}tox.ini a.txt . ..\n" diff --git a/tests/session/cmd/test_exec_.py b/tests/session/cmd/test_exec_.py index b9d1c085..c805ca6a 100644 --- a/tests/session/cmd/test_exec_.py +++ b/tests/session/cmd/test_exec_.py @@ -8,7 +8,7 @@ from tox.pytest import ToxProjectCreator @pytest.mark.parametrize("trail", [[], ["--"]], ids=["no_posargs", "empty_posargs"]) def test_exec_fail_no_posargs(tox_project: ToxProjectCreator, trail: List[str]) -> None: - outcome = tox_project({"tox.ini": ""}).run("e", "-e", "py39,py38", *trail) + outcome = tox_project({"tox.ini": ""}).run("e", "-e", "py39", *trail) outcome.assert_failed() msg = "ROOT: HandledError| You must specify a command as positional arguments, use -- <command>\n" outcome.assert_out_err(msg, "") |