diff options
-rw-r--r-- | docs/changelog/1792.bugfix.rst | 2 | ||||
-rw-r--r-- | docs/changelog/1828.bugfix.rst | 2 | ||||
-rw-r--r-- | src/tox/tox_env/python/req_file.py | 22 | ||||
-rw-r--r-- | tests/tox_env/python/test_req_file.py | 23 |
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"] |