Skip to content

Commit 5a8f6c4

Browse files
authored
feat(pypi): support direct urls for wheels in bazel downloader (#2655)
This PR adds support for installing wheels via direct urls in the requirements lock file: ``` foo==0.0.1 @ https://someurl.org/package.whl bar==0.0.1 @ https://someurl.org/package.tar.gz ``` This is to improve parity between bazel downloader and pip behavior. Before this change, direct urls used fallback to pip install. Partially addresses #2363 as it does not add support for git urls.
1 parent 8f51731 commit 5a8f6c4

File tree

5 files changed

+167
-7
lines changed

5 files changed

+167
-7
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ Unreleased changes template.
8888
GitHub releases page metadata published by the `uv` project.
8989
* (pypi) An extra argument to add the interpreter lib dir to `LDFLAGS` when
9090
building wheels from `sdist`.
91+
* (pypi) Direct HTTP urls for wheels and sdists are now supported when using
92+
{obj}`experimental_index_url` (bazel downloader).
93+
Partially fixes [#2363](https://github.com/bazelbuild/rules_python/issues/2363).
9194

9295
{#v0-0-0-removed}
9396
### Removed

python/private/pypi/index_sources.bzl

+6-1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def index_sources(line):
3232
* `marker` - str; the marker expression, as per PEP508 spec.
3333
* `requirement` - str; a requirement line without the marker. This can
3434
be given to `pip` to install a package.
35+
* `url` - str; URL if the requirement specifies a direct URL, empty string otherwise.
3536
"""
3637
line = line.replace("\\", " ")
3738
head, _, maybe_hashes = line.partition(";")
@@ -55,14 +56,18 @@ def index_sources(line):
5556
requirement,
5657
" ".join(["--hash=sha256:{}".format(sha) for sha in shas]),
5758
).strip()
59+
60+
url = ""
5861
if "@" in head:
5962
requirement = requirement_line
60-
shas = []
63+
_, _, url_and_rest = requirement.partition("@")
64+
url = url_and_rest.strip().partition(" ")[0].strip()
6165

6266
return struct(
6367
requirement = requirement,
6468
requirement_line = requirement_line,
6569
version = version,
6670
shas = sorted(shas),
6771
marker = marker,
72+
url = url,
6873
)

python/private/pypi/parse_requirements.bzl

+17
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,23 @@ def _add_dists(*, requirement, index_urls, logger = None):
292292
index_urls: The result of simpleapi_download.
293293
logger: A logger for printing diagnostic info.
294294
"""
295+
296+
# Handle direct URLs in requirements
297+
if requirement.srcs.url:
298+
url = requirement.srcs.url
299+
_, _, filename = url.rpartition("/")
300+
direct_url_dist = struct(
301+
url = url,
302+
filename = filename,
303+
sha256 = requirement.srcs.shas[0] if requirement.srcs.shas else "",
304+
yanked = False,
305+
)
306+
307+
if filename.endswith(".whl"):
308+
return [direct_url_dist], None
309+
else:
310+
return [], direct_url_dist
311+
295312
if not index_urls:
296313
return [], None
297314

tests/pypi/index_sources/index_sources_tests.bzl

+20-5
Original file line numberDiff line numberDiff line change
@@ -24,27 +24,39 @@ def _test_no_simple_api_sources(env):
2424
"foo==0.0.1": struct(
2525
requirement = "foo==0.0.1",
2626
marker = "",
27+
url = "",
2728
),
2829
"foo==0.0.1 @ https://someurl.org": struct(
2930
requirement = "foo==0.0.1 @ https://someurl.org",
3031
marker = "",
32+
url = "https://someurl.org",
3133
),
32-
"foo==0.0.1 @ https://someurl.org --hash=sha256:deadbeef": struct(
33-
requirement = "foo==0.0.1 @ https://someurl.org --hash=sha256:deadbeef",
34+
"foo==0.0.1 @ https://someurl.org/package.whl": struct(
35+
requirement = "foo==0.0.1 @ https://someurl.org/package.whl",
3436
marker = "",
37+
url = "https://someurl.org/package.whl",
3538
),
36-
"foo==0.0.1 @ https://someurl.org; python_version < \"2.7\"\\ --hash=sha256:deadbeef": struct(
37-
requirement = "foo==0.0.1 @ https://someurl.org --hash=sha256:deadbeef",
39+
"foo==0.0.1 @ https://someurl.org/package.whl --hash=sha256:deadbeef": struct(
40+
requirement = "foo==0.0.1 @ https://someurl.org/package.whl --hash=sha256:deadbeef",
41+
marker = "",
42+
url = "https://someurl.org/package.whl",
43+
shas = ["deadbeef"],
44+
),
45+
"foo==0.0.1 @ https://someurl.org/package.whl; python_version < \"2.7\"\\ --hash=sha256:deadbeef": struct(
46+
requirement = "foo==0.0.1 @ https://someurl.org/package.whl --hash=sha256:deadbeef",
3847
marker = "python_version < \"2.7\"",
48+
url = "https://someurl.org/package.whl",
49+
shas = ["deadbeef"],
3950
),
4051
}
4152
for input, want in inputs.items():
4253
got = index_sources(input)
43-
env.expect.that_collection(got.shas).contains_exactly([])
54+
env.expect.that_collection(got.shas).contains_exactly(want.shas if hasattr(want, "shas") else [])
4455
env.expect.that_str(got.version).equals("0.0.1")
4556
env.expect.that_str(got.requirement).equals(want.requirement)
4657
env.expect.that_str(got.requirement_line).equals(got.requirement)
4758
env.expect.that_str(got.marker).equals(want.marker)
59+
env.expect.that_str(got.url).equals(want.url)
4860

4961
_tests.append(_test_no_simple_api_sources)
5062

@@ -58,6 +70,7 @@ def _test_simple_api_sources(env):
5870
marker = "",
5971
requirement = "foo==0.0.2",
6072
requirement_line = "foo==0.0.2 --hash=sha256:deafbeef --hash=sha256:deadbeef",
73+
url = "",
6174
),
6275
"foo[extra]==0.0.2; (python_version < 2.7 or extra == \"@\") --hash=sha256:deafbeef --hash=sha256:deadbeef": struct(
6376
shas = [
@@ -67,6 +80,7 @@ def _test_simple_api_sources(env):
6780
marker = "(python_version < 2.7 or extra == \"@\")",
6881
requirement = "foo[extra]==0.0.2",
6982
requirement_line = "foo[extra]==0.0.2 --hash=sha256:deafbeef --hash=sha256:deadbeef",
83+
url = "",
7084
),
7185
}
7286
for input, want in tests.items():
@@ -76,6 +90,7 @@ def _test_simple_api_sources(env):
7690
env.expect.that_str(got.requirement).equals(want.requirement)
7791
env.expect.that_str(got.requirement_line).equals(want.requirement_line)
7892
env.expect.that_str(got.marker).equals(want.marker)
93+
env.expect.that_str(got.url).equals(want.url)
7994

8095
_tests.append(_test_simple_api_sources)
8196

tests/pypi/parse_requirements/parse_requirements_tests.bzl

+121-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ foo==0.0.1 \
2626
--hash=sha256:deadb00f
2727
""",
2828
"requirements_direct": """\
29-
foo[extra] @ https://some-url
29+
foo[extra] @ https://some-url/package.whl
30+
bar @ https://example.org/bar-1.0.whl --hash=sha256:deadbeef
31+
baz @ https://test.com/baz-2.0.whl; python_version < "3.8" --hash=sha256:deadb00f
32+
qux @ https://example.org/qux-1.0.tar.gz --hash=sha256:deadbe0f
3033
""",
3134
"requirements_extra_args": """\
3235
--index-url=example.org
@@ -106,6 +109,7 @@ def _test_simple(env):
106109
requirement_line = "foo[extra]==0.0.1 --hash=sha256:deadbeef",
107110
shas = ["deadbeef"],
108111
version = "0.0.1",
112+
url = "",
109113
),
110114
target_platforms = [
111115
"linux_x86_64",
@@ -124,6 +128,110 @@ def _test_simple(env):
124128

125129
_tests.append(_test_simple)
126130

131+
def _test_direct_urls(env):
132+
got = parse_requirements(
133+
ctx = _mock_ctx(),
134+
requirements_by_platform = {
135+
"requirements_direct": ["linux_x86_64"],
136+
},
137+
)
138+
env.expect.that_dict(got).contains_exactly({
139+
"bar": [
140+
struct(
141+
distribution = "bar",
142+
extra_pip_args = [],
143+
sdist = None,
144+
is_exposed = True,
145+
srcs = struct(
146+
marker = "",
147+
requirement = "bar @ https://example.org/bar-1.0.whl --hash=sha256:deadbeef",
148+
requirement_line = "bar @ https://example.org/bar-1.0.whl --hash=sha256:deadbeef",
149+
shas = ["deadbeef"],
150+
version = "",
151+
url = "https://example.org/bar-1.0.whl",
152+
),
153+
target_platforms = ["linux_x86_64"],
154+
whls = [struct(
155+
url = "https://example.org/bar-1.0.whl",
156+
filename = "bar-1.0.whl",
157+
sha256 = "deadbeef",
158+
yanked = False,
159+
)],
160+
),
161+
],
162+
"baz": [
163+
struct(
164+
distribution = "baz",
165+
extra_pip_args = [],
166+
sdist = None,
167+
is_exposed = True,
168+
srcs = struct(
169+
marker = "python_version < \"3.8\"",
170+
requirement = "baz @ https://test.com/baz-2.0.whl --hash=sha256:deadb00f",
171+
requirement_line = "baz @ https://test.com/baz-2.0.whl --hash=sha256:deadb00f",
172+
shas = ["deadb00f"],
173+
version = "",
174+
url = "https://test.com/baz-2.0.whl",
175+
),
176+
target_platforms = ["linux_x86_64"],
177+
whls = [struct(
178+
url = "https://test.com/baz-2.0.whl",
179+
filename = "baz-2.0.whl",
180+
sha256 = "deadb00f",
181+
yanked = False,
182+
)],
183+
),
184+
],
185+
"foo": [
186+
struct(
187+
distribution = "foo",
188+
extra_pip_args = [],
189+
sdist = None,
190+
is_exposed = True,
191+
srcs = struct(
192+
marker = "",
193+
requirement = "foo[extra] @ https://some-url/package.whl",
194+
requirement_line = "foo[extra] @ https://some-url/package.whl",
195+
shas = [],
196+
version = "",
197+
url = "https://some-url/package.whl",
198+
),
199+
target_platforms = ["linux_x86_64"],
200+
whls = [struct(
201+
url = "https://some-url/package.whl",
202+
filename = "package.whl",
203+
sha256 = "",
204+
yanked = False,
205+
)],
206+
),
207+
],
208+
"qux": [
209+
struct(
210+
distribution = "qux",
211+
extra_pip_args = [],
212+
sdist = struct(
213+
url = "https://example.org/qux-1.0.tar.gz",
214+
filename = "qux-1.0.tar.gz",
215+
sha256 = "deadbe0f",
216+
yanked = False,
217+
),
218+
is_exposed = True,
219+
srcs = struct(
220+
marker = "",
221+
requirement = "qux @ https://example.org/qux-1.0.tar.gz --hash=sha256:deadbe0f",
222+
requirement_line = "qux @ https://example.org/qux-1.0.tar.gz --hash=sha256:deadbe0f",
223+
shas = ["deadbe0f"],
224+
version = "",
225+
url = "https://example.org/qux-1.0.tar.gz",
226+
),
227+
target_platforms = ["linux_x86_64"],
228+
whls = [],
229+
),
230+
],
231+
})
232+
233+
_tests.append(_test_direct_urls)
234+
127235
def _test_extra_pip_args(env):
128236
got = parse_requirements(
129237
ctx = _mock_ctx(),
@@ -145,6 +253,7 @@ def _test_extra_pip_args(env):
145253
requirement_line = "foo[extra]==0.0.1 --hash=sha256:deadbeef",
146254
shas = ["deadbeef"],
147255
version = "0.0.1",
256+
url = "",
148257
),
149258
target_platforms = [
150259
"linux_x86_64",
@@ -182,6 +291,7 @@ def _test_dupe_requirements(env):
182291
requirement_line = "foo[extra,extra_2]==0.0.1 --hash=sha256:deadbeef",
183292
shas = ["deadbeef"],
184293
version = "0.0.1",
294+
url = "",
185295
),
186296
target_platforms = ["linux_x86_64"],
187297
whls = [],
@@ -211,6 +321,7 @@ def _test_multi_os(env):
211321
requirement_line = "bar==0.0.1 --hash=sha256:deadb00f",
212322
shas = ["deadb00f"],
213323
version = "0.0.1",
324+
url = "",
214325
),
215326
target_platforms = ["windows_x86_64"],
216327
whls = [],
@@ -228,6 +339,7 @@ def _test_multi_os(env):
228339
requirement_line = "foo==0.0.3 --hash=sha256:deadbaaf",
229340
shas = ["deadbaaf"],
230341
version = "0.0.3",
342+
url = "",
231343
),
232344
target_platforms = ["linux_x86_64"],
233345
whls = [],
@@ -243,6 +355,7 @@ def _test_multi_os(env):
243355
requirement_line = "foo[extra]==0.0.2 --hash=sha256:deadbeef",
244356
shas = ["deadbeef"],
245357
version = "0.0.2",
358+
url = "",
246359
),
247360
target_platforms = ["windows_x86_64"],
248361
whls = [],
@@ -282,6 +395,7 @@ def _test_multi_os_legacy(env):
282395
requirement_line = "bar==0.0.1 --hash=sha256:deadb00f",
283396
shas = ["deadb00f"],
284397
version = "0.0.1",
398+
url = "",
285399
),
286400
target_platforms = ["cp39_linux_x86_64"],
287401
whls = [],
@@ -299,6 +413,7 @@ def _test_multi_os_legacy(env):
299413
requirement_line = "foo==0.0.1 --hash=sha256:deadbeef",
300414
shas = ["deadbeef"],
301415
version = "0.0.1",
416+
url = "",
302417
),
303418
target_platforms = ["cp39_linux_x86_64"],
304419
whls = [],
@@ -314,6 +429,7 @@ def _test_multi_os_legacy(env):
314429
requirement = "foo==0.0.3",
315430
shas = ["deadbaaf"],
316431
version = "0.0.3",
432+
url = "",
317433
),
318434
target_platforms = ["cp39_osx_aarch64"],
319435
whls = [],
@@ -367,6 +483,7 @@ def _test_env_marker_resolution(env):
367483
requirement_line = "bar==0.0.1 --hash=sha256:deadbeef",
368484
shas = ["deadbeef"],
369485
version = "0.0.1",
486+
url = "",
370487
),
371488
target_platforms = ["cp311_linux_super_exotic", "cp311_windows_x86_64"],
372489
whls = [],
@@ -384,6 +501,7 @@ def _test_env_marker_resolution(env):
384501
requirement_line = "foo[extra]==0.0.1 --hash=sha256:deadbeef",
385502
shas = ["deadbeef"],
386503
version = "0.0.1",
504+
url = "",
387505
),
388506
target_platforms = ["cp311_windows_x86_64"],
389507
whls = [],
@@ -419,6 +537,7 @@ def _test_different_package_version(env):
419537
requirement_line = "foo==0.0.1 --hash=sha256:deadb00f",
420538
shas = ["deadb00f"],
421539
version = "0.0.1",
540+
url = "",
422541
),
423542
target_platforms = ["linux_x86_64"],
424543
whls = [],
@@ -434,6 +553,7 @@ def _test_different_package_version(env):
434553
requirement_line = "foo==0.0.1+local --hash=sha256:deadbeef",
435554
shas = ["deadbeef"],
436555
version = "0.0.1+local",
556+
url = "",
437557
),
438558
target_platforms = ["linux_x86_64"],
439559
whls = [],

0 commit comments

Comments
 (0)