Skip to content

Commit bda710c

Browse files
aignasrickeylev
andauthored
fix(pypi): pass requirements without env markers to the whl_library (#2488)
With this change the environment markers from the requirements.txt files no longer end up in the whl_library definitions. I am reusing a function that already is parsing each requirement line for `sha256` values and added logic to extract the `marker` at that point. This means that the change is also trivial to backport to the `WORKSPACE` and the logic in the extension becomes simpler and we don't rely only on integration tests. Expected changes to the users: * If they have vendored pip requirements in `WORKSPACE`, those will be reformatted and the env markers will be removed. * The `MODULE.bazel.lock` file will be likewise reformatted if users are not using `--experimental_index_url`. Also, the env markers will not be passed in the `requirement`. * `bazel query 'deps("@pypi//foo")'` should start working in more cases. Fixes #2450. --------- Co-authored-by: Richard Levasseur <[email protected]>
1 parent e823657 commit bda710c

File tree

9 files changed

+160
-83
lines changed

9 files changed

+160
-83
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ Unreleased changes template.
6060
### Fixed
6161
* (py_wheel) Use the default shell environment when building wheels to allow
6262
toolchains that search PATH to be used for the wheel builder tool.
63+
* (pypi) The requirement argument parsed to `whl_library` will now not have env
64+
marker information allowing `bazel query` to work in cases where the `whl` is
65+
available for all of the platforms and the sdist can be built. This fix is
66+
for both WORKSPACE and `bzlmod` setups.
67+
Fixes [#2450](https://github.com/bazelbuild/rules_python/issues/2450).
6368

6469
{#v0-0-0-added}
6570
### Added

examples/pip_parse_vendored/requirements.bzl

+5-5
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,11 @@ all_data_requirements = [
3333
]
3434

3535
_packages = [
36-
("my_project_pip_deps_vendored_certifi", "certifi==2023.7.22 --hash=sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082 --hash=sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"),
37-
("my_project_pip_deps_vendored_charset_normalizer", "charset-normalizer==2.1.1 --hash=sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845 --hash=sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"),
38-
("my_project_pip_deps_vendored_idna", "idna==3.4 --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"),
39-
("my_project_pip_deps_vendored_requests", "requests==2.28.1 --hash=sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983 --hash=sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"),
40-
("my_project_pip_deps_vendored_urllib3", "urllib3==1.26.13 --hash=sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc --hash=sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"),
36+
("my_project_pip_deps_vendored_certifi", "certifi==2023.7.22 --hash=sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082 --hash=sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"),
37+
("my_project_pip_deps_vendored_charset_normalizer", "charset-normalizer==2.1.1 --hash=sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845 --hash=sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"),
38+
("my_project_pip_deps_vendored_idna", "idna==3.4 --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"),
39+
("my_project_pip_deps_vendored_requests", "requests==2.28.1 --hash=sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983 --hash=sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"),
40+
("my_project_pip_deps_vendored_urllib3", "urllib3==1.26.13 --hash=sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc --hash=sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"),
4141
]
4242
_config = {
4343
"download_only": False,

python/private/pypi/extension.bzl

+2-2
Original file line numberDiff line numberDiff line change
@@ -321,10 +321,10 @@ def _create_whl_repos(
321321
for requirement in requirements:
322322
is_exposed = is_exposed or requirement.is_exposed
323323
if get_index_urls:
324-
logger.warn(lambda: "falling back to pip for installing the right file for {}".format(requirement.requirement_line))
324+
logger.warn(lambda: "falling back to pip for installing the right file for {}".format(requirement.srcs.requirement_line))
325325

326326
args = dict(whl_library_args) # make a copy
327-
args["requirement"] = requirement.requirement_line
327+
args["requirement"] = requirement.srcs.requirement_line
328328
if requirement.extra_pip_args:
329329
args["extra_pip_args"] = requirement.extra_pip_args
330330

python/private/pypi/index_sources.bzl

+27-12
Original file line numberDiff line numberDiff line change
@@ -26,28 +26,43 @@ def index_sources(line):
2626
line(str): The requirements.txt entry.
2727
2828
Returns:
29-
A struct with shas attribute containing a list of shas to download from pypi_index.
29+
A struct with shas attribute containing:
30+
* `shas` - list[str]; shas to download from pypi_index.
31+
* `version` - str; version of the package.
32+
* `marker` - str; the marker expression, as per PEP508 spec.
33+
* `requirement` - str; a requirement line without the marker. This can
34+
be given to `pip` to install a package.
3035
"""
36+
line = line.replace("\\", " ")
3137
head, _, maybe_hashes = line.partition(";")
3238
_, _, version = head.partition("==")
3339
version = version.partition(" ")[0].strip()
3440

35-
if "@" in head:
36-
shas = []
37-
else:
38-
maybe_hashes = maybe_hashes or line
39-
shas = [
40-
sha.strip()
41-
for sha in maybe_hashes.split("--hash=sha256:")[1:]
42-
]
41+
marker, _, _ = maybe_hashes.partition("--hash=")
42+
maybe_hashes = maybe_hashes or line
43+
shas = [
44+
sha.strip()
45+
for sha in maybe_hashes.split("--hash=sha256:")[1:]
46+
]
4347

48+
marker = marker.strip()
4449
if head == line:
45-
head = line.partition("--hash=")[0].strip()
50+
requirement = line.partition("--hash=")[0].strip()
4651
else:
47-
head = head + ";" + maybe_hashes.partition("--hash=")[0].strip()
52+
requirement = head.strip()
53+
54+
requirement_line = "{} {}".format(
55+
requirement,
56+
" ".join(["--hash=sha256:{}".format(sha) for sha in shas]),
57+
).strip()
58+
if "@" in head:
59+
requirement = requirement_line
60+
shas = []
4861

4962
return struct(
50-
requirement = line if not shas else head,
63+
requirement = requirement,
64+
requirement_line = requirement_line,
5165
version = version,
5266
shas = sorted(shas),
67+
marker = marker,
5368
)

python/private/pypi/parse_requirements.bzl

+15-10
Original file line numberDiff line numberDiff line change
@@ -74,16 +74,22 @@ def parse_requirements(
7474
logger: repo_utils.logger or None, a simple struct to log diagnostic messages.
7575
7676
Returns:
77-
A tuple where the first element a dict of dicts where the first key is
78-
the normalized distribution name (with underscores) and the second key
79-
is the requirement_line, then value and the keys are structs with the
80-
following attributes:
81-
* distribution: The non-normalized distribution name.
82-
* srcs: The Simple API downloadable source list.
83-
* requirement_line: The original requirement line.
84-
* target_platforms: The list of target platforms that this package is for.
85-
* is_exposed: A boolean if the package should be exposed via the hub
77+
{type}`dict[str, list[struct]]` where the key is the distribution name and the struct
78+
contains the following attributes:
79+
* `distribution`: {type}`str` The non-normalized distribution name.
80+
* `srcs`: {type}`struct` The parsed requirement line for easier Simple
81+
API downloading (see `index_sources` return value).
82+
* `target_platforms`: {type}`list[str]` Target platforms that this package is for.
83+
The format is `cp3{minor}_{os}_{arch}`.
84+
* `is_exposed`: {type}`bool` `True` if the package should be exposed via the hub
8685
repository.
86+
* `extra_pip_args`: {type}`list[str]` pip args to use in case we are
87+
not using the bazel downloader to download the archives. This should
88+
be passed to {obj}`whl_library`.
89+
* `whls`: {type}`list[struct]` The list of whl entries that can be
90+
downloaded using the bazel downloader.
91+
* `sdist`: {type}`list[struct]` The sdist that can be downloaded using
92+
the bazel downloader.
8793
8894
The second element is extra_pip_args should be passed to `whl_library`.
8995
"""
@@ -209,7 +215,6 @@ def parse_requirements(
209215
struct(
210216
distribution = r.distribution,
211217
srcs = r.srcs,
212-
requirement_line = r.requirement_line,
213218
target_platforms = sorted(target_platforms),
214219
extra_pip_args = r.extra_pip_args,
215220
whls = whls,

python/private/pypi/pip_repository.bzl

+1-1
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def _pip_repository_impl(rctx):
101101
if not r:
102102
continue
103103
options = options or r.extra_pip_args
104-
selected_requirements[name] = r.requirement_line
104+
selected_requirements[name] = r.srcs.requirement_line
105105

106106
bzl_packages = sorted(selected_requirements.keys())
107107

tests/pypi/extension/extension_tests.bzl

+17-8
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ def _mock_mctx(*modules, environ = {}, read = None):
2828
name = "unittest",
2929
arch = "exotic",
3030
),
31-
read = read or (lambda _: "simple==0.0.1 --hash=sha256:deadbeef --hash=sha256:deadbaaf"),
31+
read = read or (lambda _: """\
32+
simple==0.0.1 \
33+
--hash=sha256:deadbeef \
34+
--hash=sha256:deadbaaf"""),
3235
modules = [
3336
struct(
3437
name = modules[0].name,
@@ -262,7 +265,8 @@ def _test_simple_with_markers(env):
262265
read = lambda x: {
263266
"universal.txt": """\
264267
torch==2.4.1+cpu ; platform_machine == 'x86_64'
265-
torch==2.4.1 ; platform_machine != 'x86_64'
268+
torch==2.4.1 ; platform_machine != 'x86_64' \
269+
--hash=sha256:deadbeef
266270
""",
267271
}[x],
268272
),
@@ -313,13 +317,13 @@ torch==2.4.1 ; platform_machine != 'x86_64'
313317
"dep_template": "@pypi//{name}:{target}",
314318
"python_interpreter_target": "unit_test_interpreter_target",
315319
"repo": "pypi_315",
316-
"requirement": "torch==2.4.1 ; platform_machine != 'x86_64'",
320+
"requirement": "torch==2.4.1 --hash=sha256:deadbeef",
317321
},
318322
"pypi_315_torch_linux_x86_64_osx_x86_64_windows_x86_64": {
319323
"dep_template": "@pypi//{name}:{target}",
320324
"python_interpreter_target": "unit_test_interpreter_target",
321325
"repo": "pypi_315",
322-
"requirement": "torch==2.4.1+cpu ; platform_machine == 'x86_64'",
326+
"requirement": "torch==2.4.1+cpu",
323327
},
324328
})
325329
pypi.whl_mods().contains_exactly({})
@@ -351,16 +355,19 @@ def _test_download_only_multiple(env):
351355
--implementation=cp
352356
--abi=cp315
353357
354-
simple==0.0.1 --hash=sha256:deadbeef
355-
extra==0.0.1 --hash=sha256:deadb00f
358+
simple==0.0.1 \
359+
--hash=sha256:deadbeef
360+
extra==0.0.1 \
361+
--hash=sha256:deadb00f
356362
""",
357363
"requirements.osx_aarch64.txt": """\
358364
--platform=macosx_10_9_arm64
359365
--python-version=315
360366
--implementation=cp
361367
--abi=cp315
362368
363-
simple==0.0.3 --hash=sha256:deadbaaf
369+
simple==0.0.3 \
370+
--hash=sha256:deadbaaf
364371
""",
365372
}[x],
366373
),
@@ -473,7 +480,9 @@ def _test_simple_get_index(env):
473480
),
474481
read = lambda x: {
475482
"requirements.txt": """
476-
simple==0.0.1 --hash=sha256:deadbeef --hash=sha256:deadb00f
483+
simple==0.0.1 \
484+
--hash=sha256:deadbeef \
485+
--hash=sha256:deadb00f
477486
some_pkg==0.0.1
478487
""",
479488
}[x],

tests/pypi/index_sources/index_sources_tests.bzl

+45-17
Original file line numberDiff line numberDiff line change
@@ -20,34 +20,62 @@ load("//python/private/pypi:index_sources.bzl", "index_sources") # buildifier:
2020
_tests = []
2121

2222
def _test_no_simple_api_sources(env):
23-
inputs = [
24-
"foo==0.0.1",
25-
"foo==0.0.1 @ https://someurl.org",
26-
"foo==0.0.1 @ https://someurl.org --hash=sha256:deadbeef",
27-
"foo==0.0.1 @ https://someurl.org; python_version < 2.7 --hash=sha256:deadbeef",
28-
]
29-
for input in inputs:
23+
inputs = {
24+
"foo==0.0.1": struct(
25+
requirement = "foo==0.0.1",
26+
marker = "",
27+
),
28+
"foo==0.0.1 @ https://someurl.org": struct(
29+
requirement = "foo==0.0.1 @ https://someurl.org",
30+
marker = "",
31+
),
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+
marker = "",
35+
),
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",
38+
marker = "python_version < \"2.7\"",
39+
),
40+
}
41+
for input, want in inputs.items():
3042
got = index_sources(input)
3143
env.expect.that_collection(got.shas).contains_exactly([])
3244
env.expect.that_str(got.version).equals("0.0.1")
45+
env.expect.that_str(got.requirement).equals(want.requirement)
46+
env.expect.that_str(got.requirement_line).equals(got.requirement)
47+
env.expect.that_str(got.marker).equals(want.marker)
3348

3449
_tests.append(_test_no_simple_api_sources)
3550

3651
def _test_simple_api_sources(env):
3752
tests = {
38-
"foo==0.0.2 --hash=sha256:deafbeef --hash=sha256:deadbeef": [
39-
"deadbeef",
40-
"deafbeef",
41-
],
42-
"foo[extra]==0.0.2; (python_version < 2.7 or something_else == \"@\") --hash=sha256:deafbeef --hash=sha256:deadbeef": [
43-
"deadbeef",
44-
"deafbeef",
45-
],
53+
"foo==0.0.2 --hash=sha256:deafbeef --hash=sha256:deadbeef": struct(
54+
shas = [
55+
"deadbeef",
56+
"deafbeef",
57+
],
58+
marker = "",
59+
requirement = "foo==0.0.2",
60+
requirement_line = "foo==0.0.2 --hash=sha256:deafbeef --hash=sha256:deadbeef",
61+
),
62+
"foo[extra]==0.0.2; (python_version < 2.7 or extra == \"@\") --hash=sha256:deafbeef --hash=sha256:deadbeef": struct(
63+
shas = [
64+
"deadbeef",
65+
"deafbeef",
66+
],
67+
marker = "(python_version < 2.7 or extra == \"@\")",
68+
requirement = "foo[extra]==0.0.2",
69+
requirement_line = "foo[extra]==0.0.2 --hash=sha256:deafbeef --hash=sha256:deadbeef",
70+
),
4671
}
47-
for input, want_shas in tests.items():
72+
for input, want in tests.items():
4873
got = index_sources(input)
49-
env.expect.that_collection(got.shas).contains_exactly(want_shas)
74+
env.expect.that_collection(got.shas).contains_exactly(want.shas)
5075
env.expect.that_str(got.version).equals("0.0.2")
76+
env.expect.that_str(got.requirement).equals(want.requirement)
77+
env.expect.that_str(got.requirement_line).equals(want.requirement_line)
78+
env.expect.that_str(got.marker).equals(want.marker)
5179

5280
_tests.append(_test_simple_api_sources)
5381

0 commit comments

Comments
 (0)