Skip to content

Commit f2436f2

Browse files
committed
Always convert paths in 'comes_from' to posix
In order to guarantee more consistent outputs, especially in the case of mixed paths with forward- and back-slashes, this changeset updates the `comes_from` handling to *always* apply a level of normalization. Paths passed back through the `parse_requirements()` wrapper will now always be formatted as posix paths.
1 parent 1c6da33 commit f2436f2

2 files changed

Lines changed: 37 additions & 37 deletions

File tree

piptools/_compat/pip_compat.py

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -86,22 +86,20 @@ def parse_requirements(
8686
isolated: bool = False,
8787
comes_from_stdin: bool = False,
8888
) -> Iterator[InstallRequirement]:
89-
# the `comes_from` data will be rewritten based on a number of conditions
90-
#
91-
# None do not rewrite
92-
# callable programmatic rewrite
93-
# str fixed rewrite
94-
rewrite_comes_from: str | Callable[[str], str] | None
89+
# the `comes_from` data will be rewritten in different ways in different conditions
90+
# each rewrite rule is expressible as a str->str function or as a static replacement
91+
rewrite_comes_from: Callable[[str], str] | str
9592

9693
if comes_from_stdin:
9794
# if data is coming from stdin, then `comes_from="-r -"`
9895
rewrite_comes_from = "-r -"
9996
elif pathlib.Path(filename).is_absolute():
100-
rewrite_comes_from = None
97+
# if the input path is absolute, just normalize paths to posix-style
98+
rewrite_comes_from = _normalize_comes_from_location
10199
else:
102100
# if the input was a relative path, set the rewrite rule to rewrite
103101
# absolute paths to be relative
104-
rewrite_comes_from = _rewrite_absolute_comes_from_location
102+
rewrite_comes_from = _relativize_comes_from_location
105103

106104
for parsed_req in _parse_requirements(
107105
filename, session, finder=finder, options=options, constraint=constraint
@@ -114,14 +112,16 @@ def parse_requirements(
114112
file_link._url = parsed_req.requirement
115113
install_req.link = file_link
116114
install_req = copy_install_requirement(install_req)
115+
117116
if isinstance(rewrite_comes_from, str):
118117
install_req.comes_from = rewrite_comes_from
119-
elif rewrite_comes_from is not None:
118+
else:
120119
install_req.comes_from = rewrite_comes_from(install_req.comes_from)
120+
121121
yield install_req
122122

123123

124-
def _rewrite_absolute_comes_from_location(original_comes_from: str, /) -> str:
124+
def _relativize_comes_from_location(original_comes_from: str, /) -> str:
125125
"""
126126
This is the rewrite rule used when ``-r`` or ``-c`` appears in
127127
``comes_from`` data with an absolute path.
@@ -138,15 +138,34 @@ def _rewrite_absolute_comes_from_location(original_comes_from: str, /) -> str:
138138

139139
file_path = pathlib.Path(suffix)
140140

141-
# if the path was not absolute, bail out
141+
# if the path was not absolute, normalize to posix-style and finish processing
142142
if not file_path.is_absolute():
143-
return original_comes_from
143+
return f"{prefix} {file_path.as_posix()}"
144144

145145
# make it relative to the current working dir
146146
suffix = file_path.relative_to(pathlib.Path.cwd()).as_posix()
147147
return f"{prefix} {suffix}"
148148

149149

150+
def _normalize_comes_from_location(original_comes_from: str, /) -> str:
151+
"""
152+
This is the rewrite rule when ``-r`` or ``-c`` appears in ``comes_from``
153+
data and the input path was absolute, meaning we should not relativize the locations.
154+
155+
Instead, we apply minimal normalization, ensuring that posix-style paths are used.
156+
"""
157+
# require `-r` or `-c` as the source
158+
if not original_comes_from.startswith(("-r ", "-c ")):
159+
return original_comes_from
160+
161+
# split on the space
162+
prefix, _, suffix = original_comes_from.partition(" ")
163+
164+
# convert to a posix-style path
165+
suffix = pathlib.Path(suffix).as_posix()
166+
return f"{prefix} {suffix}"
167+
168+
150169
def create_wheel_cache(cache_dir: str, format_control: str | None = None) -> WheelCache:
151170
kwargs: dict[str, str | None] = {"cache_dir": cache_dir}
152171
if PIP_VERSION[:2] <= (23, 0):

tests/test_cli_compile.py

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1967,7 +1967,7 @@ def test_many_inputs_includes_all_annotations(pip_conf, runner, tmp_path, num_in
19671967
"small-fake-a==0.1",
19681968
" # via",
19691969
]
1970-
+ [f" # -r {req_in}" for req_in in req_ins]
1970+
+ [f" # -r {req_in.as_posix()}" for req_in in req_ins]
19711971
)
19721972
+ "\n"
19731973
)
@@ -3931,23 +3931,12 @@ def test_second_order_requirements_path_handling(
39313931
# the input path is given on the CLI as absolute or relative
39323932
# and this determines the expected output path as well
39333933
if input_path_absolute:
3934-
input_path = str(tmp_path / "requirements.in")
3935-
output_path = str(tmp_path / "requirements2.in")
3934+
input_path = (tmp_path / "requirements.in").as_posix()
3935+
output_path = (tmp_path / "requirements2.in").as_posix()
39363936
else:
39373937
input_path = "requirements.in"
39383938
output_path = "requirements2.in"
39393939

3940-
# on older pip versions, in which pip `comes_from` data is not normalized,
3941-
# override the expected output_path
3942-
#
3943-
# the second-order requirement will be preserved verbatim if the both paths
3944-
# are absolute on these pip versions
3945-
both_paths_are_absolute = (
3946-
input_path_absolute and test_files_collection.name == "absolute_include"
3947-
)
3948-
if not pip_produces_absolute_paths and both_paths_are_absolute:
3949-
output_path = (tmp_path / "requirements.in").read_text().partition(" ")[-1]
3950-
39513940
with monkeypatch.context() as m:
39523941
m.chdir(tmp_path)
39533942

@@ -4027,18 +4016,10 @@ def test_second_order_requirements_relative_path_in_separate_dir(
40274016
if not pip_produces_absolute_paths:
40284017
# traverse upwards to the root tmp dir, and append the output path to that
40294018
# similar to pathlib.Path.relative_to(..., walk_up=True)
4030-
#
4031-
# the join must be done while preserving the original path (which may be posix-style)
4032-
# and the relative path part will be made in platform-specific path syntax
4033-
# but then the two parts will be joined with a forward-slash...
4034-
#
4035-
# on Windows, this can result in mixed slashes, e.g.
4036-
# `D:\tempstorage\abcdefg\sbudir/requirements2.in`
4037-
#
4038-
# this is the pip behavior, and we inherit it
40394019
relative_segments = len(pathlib.Path(input_path).parents) - 1
4040-
relpath = str(pathlib.Path(input_path).parent / ("../" * relative_segments))
4041-
output_path = f"{relpath}/{output_path}"
4020+
output_path = (
4021+
pathlib.Path(input_path).parent / ("../" * relative_segments) / output_path
4022+
).as_posix()
40424023

40434024
with monkeypatch.context() as m:
40444025
m.chdir(tmp_path)

0 commit comments

Comments
 (0)