summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBernát Gábor <bgabor8@bloomberg.net>2021-08-15 08:55:52 +0100
committerGitHub <noreply@github.com>2021-08-15 08:55:52 +0100
commit4a020e715ca17cf2e04413a8491fbc535dd62b0d (patch)
treee2778489cc360b0a74e2ea84586cd02769ee9a36
parentc8f8f502a609e7dc6670192a399517e8aad9f010 (diff)
downloadtox-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.rst1
-rw-r--r--docs/config.rst8
-rw-r--r--src/tox/config/loader/ini/replace.py16
-rw-r--r--src/tox/config/main.py21
-rw-r--r--src/tox/pytest.py8
-rw-r--r--src/tox/session/cmd/exec_.py14
-rw-r--r--src/tox/tox_env/runner.py6
-rw-r--r--tests/config/test_main.py28
-rw-r--r--tests/session/cmd/test_exec_.py2
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, "")