Skip to content

Commit 4ef3b4c

Browse files
committed
Fix compile on -r relative paths outside of cwd
When `-r` is used to pass a path, we are normalizing with `pathlib.Path.relative_to`. This fails when the input is not a subpath of the current working directory. In Python 3.12+ pathlib supports this usage (`walk_up=True`), but on older Pythons we need to fallback to using `os.path.relpath`. In order to support this usage and give a clear indication of how we should upgrade when older versions are dropped, a small compat wrapper is here defined, `_relative_to_walk_up` which uses `relative_to(..., walk_up=True)` when it is available, and only uses `os.path.relpath` when necessary. After we drop Python 3.11 in a few years, the helper can be removed. A new regression test reproduces #2231 and is fixed with this patch. As part of writing that test, I wanted to leverage the `TestFilesCollection` helper, so this was extended in a small way to support usage without an explicit name.
1 parent 934b46a commit 4ef3b4c

File tree

3 files changed

+68
-3
lines changed

3 files changed

+68
-3
lines changed

changelog.d/2231.bugfix.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix `pip-compile` handling of relative path includes which are not subpaths of
2+
the current working directory.

piptools/_compat/pip_compat.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
from __future__ import annotations
22

33
import optparse
4+
import os.path
45
import pathlib
6+
import sys
57
import urllib.parse
68
from dataclasses import dataclass
79
from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Set, cast
@@ -152,7 +154,7 @@ def _relativize_comes_from_location(original_comes_from: str, /) -> str:
152154
return f"{prefix} {file_path.as_posix()}"
153155

154156
# make it relative to the current working dir
155-
suffix = file_path.relative_to(pathlib.Path.cwd()).as_posix()
157+
suffix = _relative_to_walk_up(file_path, pathlib.Path.cwd()).as_posix()
156158
return f"{prefix}{space_sep}{suffix}"
157159

158160

@@ -210,3 +212,18 @@ def get_dev_pkgs() -> set[str]:
210212
from pip._internal.commands.freeze import _dev_pkgs
211213

212214
return cast(Set[str], _dev_pkgs())
215+
216+
217+
def _relative_to_walk_up(path: pathlib.Path, start: pathlib.Path) -> pathlib.Path:
218+
"""
219+
Compute a relative path allowing for the input to not be a subpath of the start.
220+
221+
This is a compatibility helper for ``pathlib.Path.relative_to(..., walk_up=True)``
222+
on all Python versions. (``walk_up: bool`` is Python 3.12+)
223+
"""
224+
# prefer `pathlib.Path.relative_to` where available
225+
if sys.version_info >= (3, 12):
226+
return path.relative_to(start, walk_up=True)
227+
228+
str_result = os.path.relpath(path, start=start)
229+
return pathlib.Path(str_result)

tests/test_cli_compile.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,11 @@ class TestFilesCollection:
6565
"""
6666

6767
# the name for the collection of files
68-
name: str
68+
name: str = "<unnamed test file collection>"
6969
# static or computed contents
70-
contents: dict[str, str | typing.Callable[[pathlib.Path], str]]
70+
contents: dict[str, str | typing.Callable[[pathlib.Path], str]] = dataclasses.field(
71+
default_factory=dict
72+
)
7173

7274
def __str__(self) -> str:
7375
return self.name
@@ -4047,6 +4049,50 @@ def test_second_order_requirements_relative_path_in_separate_dir(
40474049
)
40484050

40494051

4052+
def test_second_order_requirements_can_be_in_parent_of_cwd(
4053+
pip_conf,
4054+
runner,
4055+
tmp_path,
4056+
monkeypatch,
4057+
pip_produces_absolute_paths,
4058+
):
4059+
"""
4060+
Test handling of ``-r`` includes when the included requirements file is in the
4061+
parent of the current working directory.
4062+
"""
4063+
test_files_collection = TestFilesCollection(
4064+
contents={
4065+
"subdir1/requirements.in": "-r ../requirements2.in\n",
4066+
"requirements2.in": "small-fake-a\n",
4067+
}
4068+
)
4069+
test_files_collection.populate(tmp_path)
4070+
4071+
with monkeypatch.context() as revertable_ctx:
4072+
# cd into the subdir where the initial requirements are
4073+
revertable_ctx.chdir(tmp_path / "subdir1")
4074+
out = runner.invoke(
4075+
cli,
4076+
[
4077+
"--output-file",
4078+
"-",
4079+
"--quiet",
4080+
"--no-header",
4081+
"--no-emit-options",
4082+
"-r",
4083+
"requirements.in",
4084+
],
4085+
)
4086+
4087+
assert out.exit_code == 0
4088+
assert out.stdout == dedent(
4089+
"""\
4090+
small-fake-a==0.2
4091+
# via -r ../requirements2.in
4092+
"""
4093+
)
4094+
4095+
40504096
@pytest.mark.parametrize(
40514097
"input_path_absolute", (True, False), ids=("absolute-input", "relative-input")
40524098
)

0 commit comments

Comments
 (0)