summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/changelog/1792.bugfix.rst2
-rw-r--r--docs/changelog/1828.bugfix.rst2
-rw-r--r--src/tox/tox_env/python/req_file.py22
-rw-r--r--tests/tox_env/python/test_req_file.py23
4 files changed, 43 insertions, 6 deletions
diff --git a/docs/changelog/1792.bugfix.rst b/docs/changelog/1792.bugfix.rst
new file mode 100644
index 00000000..550172ba
--- /dev/null
+++ b/docs/changelog/1792.bugfix.rst
@@ -0,0 +1,2 @@
+When specifying requirements/editable/constraint paths within ``deps`` escape space, unless already escaped to support
+running specifying transitive requirements files within deps - by :user:`gaborbernat`.
diff --git a/docs/changelog/1828.bugfix.rst b/docs/changelog/1828.bugfix.rst
new file mode 100644
index 00000000..96ce0b8a
--- /dev/null
+++ b/docs/changelog/1828.bugfix.rst
@@ -0,0 +1,2 @@
+Raise ``ValueError`` with descriptive message when a requirements file specified does not exist
+- by :user:`gaborbernat`.
diff --git a/src/tox/tox_env/python/req_file.py b/src/tox/tox_env/python/req_file.py
index 2796e492..cd690646 100644
--- a/src/tox/tox_env/python/req_file.py
+++ b/src/tox/tox_env/python/req_file.py
@@ -56,13 +56,21 @@ class RequirementsFile:
],
}
- def __init__(self, raw: str, allow_short_req_file: bool = True, root: Optional[Path] = None) -> None:
+ def __init__(self, raw: str, within_tox_ini: bool = True, root: Optional[Path] = None) -> None:
self._root = Path().cwd() if root is None else root
- if allow_short_req_file: # patch for tox<4 supporting requirement/constraint files via -rreq.txt/-creq.txt
+ if within_tox_ini: # patch the content coming from tox.ini
lines: List[str] = []
for line in raw.splitlines():
+ # for tox<4 supporting requirement/constraint files via -rreq.txt/-creq.txt
if len(line) >= 3 and (line.startswith("-r") or line.startswith("-c")) and not line[2].isspace():
line = f"{line[:2]} {line[2:]}"
+ # escape spaces
+ escape_for = ("-c", "--constraint", "-r", "--requirement", "-f", "--find-links" "-e", "--editable")
+ escape_match = next((e for e in escape_for if line.startswith(e) and line[len(e)].isspace()), None)
+ if escape_match is not None:
+ # escape not already escaped spaces
+ escaped = re.sub(r"(?<!\\)(\s)", r"\\\1", line[len(escape_match) + 1 :])
+ line = f"{line[:len(escape_match)]} {escaped}"
lines.append(line)
adjusted = "\n".join(lines)
raw = f"{adjusted}\n" if raw.endswith("\\\n") else adjusted # preserve trailing newline if input has it
@@ -86,7 +94,7 @@ class RequirementsFile:
if not line:
continue
if line.startswith("-"): # handle flags
- words = re.findall(r"\S+", line)
+ words = [i for i in re.split(r"(?<!\\)\s", line) if i]
first = words[0]
if first in self.VALID_OPTIONS["no_arg"]:
if len(words) != 1:
@@ -98,10 +106,14 @@ class RequirementsFile:
raise ValueError(line)
else:
if first in ("-r", "--requirement", "-c", "--constraint"):
- path = Path(line[len(first) + 1 :].strip())
+ raw_path = line[len(first) + 1 :].strip()
+ unescaped_path = re.sub(r"\\(\s)", r"\1", raw_path)
+ path = Path(unescaped_path)
if not path.is_absolute():
path = ini_dir / path
- req_file = RequirementsFile(path.read_text(), allow_short_req_file=False, root=self.root)
+ if not path.exists():
+ raise ValueError(f"requirement file path {str(path)!r} does not exist")
+ req_file = RequirementsFile(path.read_text(), within_tox_ini=False, root=self.root)
result.extend(req_file.validate_and_expand())
else:
result.append(" ".join(words))
diff --git a/tests/tox_env/python/test_req_file.py b/tests/tox_env/python/test_req_file.py
index 970c47cb..cb112178 100644
--- a/tests/tox_env/python/test_req_file.py
+++ b/tests/tox_env/python/test_req_file.py
@@ -126,7 +126,6 @@ def test_requirements_txt_transitive(tmp_path: Path, flag: str) -> None:
"raw",
[
"--pre something",
- "-r one two",
"--missing",
"-k",
"magic+https://git.example.com/MyProject#egg=MyProject",
@@ -138,6 +137,12 @@ def test_bad_line(raw: str) -> None:
req.validate_and_expand()
+def test_requirements_file_missing() -> None:
+ req = RequirementsFile("-r one two")
+ with pytest.raises(ValueError, match="requirement file path '.*one two' does not exist"):
+ req.validate_and_expand()
+
+
def test_legacy_requirement_file(tmp_path: Path, monkeypatch: MonkeyPatch) -> None:
monkeypatch.chdir(tmp_path)
(tmp_path / "requirement.txt").write_text("a")
@@ -160,3 +165,19 @@ def test_constraint_txt_expanded(tmp_path: Path, flag: str) -> None:
assert req.validate_and_expand() == ["magic", "magical"]
with req.with_file() as filename:
assert filename.read_text() == f"{flag} other.txt"
+
+
+@pytest.mark.parametrize("escape_upfront", [True, False])
+def test_req_path_with_space(tmp_path: Path, escape_upfront: bool) -> None:
+ req_file = tmp_path / "a b"
+ req_file.write_text("c")
+ path = f"-r {str(req_file)}"
+ if escape_upfront:
+ path = f'{path[:-len("a b")]}a\\ b'
+ req = RequirementsFile(path)
+
+ # must be escaped within the requirements file
+ assert "a\\ b" in str(req)
+
+ # but still unroll during transitive dependencies
+ assert req.validate_and_expand() == ["c"]