Skip to content

Commit f834f40

Browse files
committed
move METADATA reading to starlark and add extra tests for the requirement spec parsing
1 parent ab47095 commit f834f40

File tree

6 files changed

+136
-61
lines changed

6 files changed

+136
-61
lines changed

python/private/pypi/pep508_requirement.bzl

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
load("//python/private:normalize_name.bzl", "normalize_name")
1919

20-
_STRIP = ["(", " ", ">", "=", "<", "~", "!"]
20+
_STRIP = ["(", " ", ">", "=", "<", "~", "!", "@"]
2121

2222
def requirement(spec):
2323
"""Parse a PEP508 requirement line
@@ -28,15 +28,18 @@ def requirement(spec):
2828
Returns:
2929
A struct with the information.
3030
"""
31+
spec = spec.strip()
3132
requires, _, maybe_hashes = spec.partition(";")
3233
marker, _, _ = maybe_hashes.partition("--hash")
3334
requires, _, extras_unparsed = requires.partition("[")
35+
extras_unparsed, _, _ = extras_unparsed.partition("]")
3436
for char in _STRIP:
3537
requires, _, _ = requires.partition(char)
36-
extras = extras_unparsed.strip("]").split(",")
38+
extras = extras_unparsed.replace(" ", "").split(",")
39+
name = requires.strip(" ")
3740

3841
return struct(
39-
name = normalize_name(requires.strip(" ")),
42+
name = normalize_name(name).replace("_", "-"),
4043
marker = marker.strip(" "),
4144
extras = extras,
4245
)

python/private/pypi/whl_installer/wheel_installer.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -120,14 +120,8 @@ def _extract_wheel(
120120
if not enable_implicit_namespace_pkgs:
121121
_setup_namespace_pkg_compatibility(installation_dir)
122122

123-
requires_dist = whl.metadata.get_all("Requires-Dist", [])
124-
abi = f"cp{sys.version_info.major}{sys.version_info.minor}"
125123
metadata = {
126-
"name": whl.name,
127-
"version": whl.version,
128124
"python_version": sys.version.partition(" ")[0],
129-
"requires_dist": requires_dist,
130-
"abi": abi,
131125
"entry_points": [
132126
{
133127
"name": name,
@@ -137,7 +131,6 @@ def _extract_wheel(
137131
for name, (module, attribute) in sorted(whl.entry_points().items())
138132
],
139133
}
140-
print(metadata)
141134

142135
with open(os.path.join(installation_dir, "metadata.json"), "w") as f:
143136
json.dump(metadata, f)

python/private/pypi/whl_library.bzl

Lines changed: 67 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,61 @@ def _create_repository_execution_environment(rctx, python_interpreter, logger =
228228
env[_CPPFLAGS] = " ".join(cppflags)
229229
return env
230230

231+
def _read_and_parse_required_metadata(rctx, logger):
232+
contents = []
233+
for entry in rctx.path("site-packages").readdir():
234+
if not entry.basename.endswith(".dist-info"):
235+
continue
236+
237+
if not entry.is_dir:
238+
continue
239+
240+
metadata_file = entry.get_child("METADATA")
241+
242+
if not metadata_file.exists:
243+
logger.fail("The METADATA file for the wheel could not be found")
244+
return None
245+
246+
contents.extend(rctx.read(metadata_file).split("\n"))
247+
break
248+
249+
single_value_fields = {
250+
"License: ": "license",
251+
"Name: ": "name",
252+
"Version: ": "version",
253+
}
254+
requires_dist = "Requires-Dist: "
255+
parsed = {}
256+
for line in contents:
257+
if not line or line.startswith("Dynamic"):
258+
# Stop parsing on first empty line
259+
break
260+
261+
found_prefix = None
262+
for prefix in single_value_fields:
263+
if line.startswith(prefix):
264+
found_prefix = prefix
265+
break
266+
267+
if found_prefix:
268+
key = single_value_fields.pop(found_prefix)
269+
_, _, value = line.partition(found_prefix)
270+
parsed[key] = value.strip()
271+
continue
272+
273+
if not line.startswith(requires_dist):
274+
continue
275+
276+
_, _, value = line.partition(requires_dist)
277+
parsed.setdefault("requires_dist", []).append(value.strip(" "))
278+
279+
return struct(
280+
name = parsed["name"],
281+
version = parsed["version"],
282+
license = parsed.get("license"),
283+
requires_dist = parsed.get("requires_dist", []),
284+
)
285+
231286
def _whl_library_impl(rctx):
232287
logger = repo_utils.logger(rctx)
233288
python_interpreter = pypi_repo_utils.resolve_python_interpreter(
@@ -400,7 +455,7 @@ def _whl_library_impl(rctx):
400455
#
401456
# This means that whl_library_targets will have to accept the following args:
402457
# * name - the name of the package in the METADATA.
403-
# * requires_dist - the list of METADATA RequiresDist.
458+
# * requires_dist - the list of METADATA Requires-Dist.
404459
# * platforms - the list of target platforms. The target_platforms
405460
# should come from the hub repo via a 'load' statement so that they don't
406461
# need to be passed as an argument to `whl_library`.
@@ -419,20 +474,19 @@ def _whl_library_impl(rctx):
419474
# * group_name, group_deps - this info can stay in the hub repository so that
420475
# it is piped at the analysis time and changing the requirement groups does
421476
# cause to re-fetch the deps.
477+
whl_metadata = _read_and_parse_required_metadata(rctx, logger)
478+
python_version = metadata["python_version"]
479+
480+
# TODO @aignas 2025-04-09: this will later be removed when loaded through the hub
481+
major_minor, _, _ = python_version.rpartition(".")
422482
package_deps = deps(
423-
# TODO @aignas 2025-04-04: get the following from manually parsing
424-
# METADATA to avoid Python dependency:
425-
# * name of the package
426-
# * version of the package
427-
# * RequiresDist
428-
# * ProvidesExtras
429-
name = metadata["name"],
430-
requires_dist = metadata["requires_dist"],
483+
name = whl_metadata.name,
484+
requires_dist = whl_metadata.requires_dist,
431485
platforms = target_platforms or [
432-
"{}_{}".format(metadata["abi"], host_platform(rctx)),
486+
"cp{}_{}".format(major_minor.replace(".", ""), host_platform(rctx)),
433487
],
434488
extras = requirement(rctx.attr.requirement).extras,
435-
host_python_version = metadata["python_version"],
489+
host_python_version = python_version,
436490
)
437491

438492
build_file_contents = generate_whl_library_build_bazel(
@@ -444,8 +498,8 @@ def _whl_library_impl(rctx):
444498
group_deps = rctx.attr.group_deps,
445499
data_exclude = rctx.attr.pip_data_exclude,
446500
tags = [
447-
"pypi_name=" + metadata["name"],
448-
"pypi_version=" + metadata["version"],
501+
"pypi_name=" + whl_metadata.name,
502+
"pypi_version=" + whl_metadata.version,
449503
],
450504
entry_points = entry_points,
451505
annotation = None if not rctx.attr.annotation else struct(**json.decode(rctx.read(rctx.attr.annotation))),

tests/pypi/pep508/BUILD.bazel

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
load(":deps_tests.bzl", "deps_test_suite")
22
load(":evaluate_tests.bzl", "evaluate_test_suite")
3+
load(":requirement_tests.bzl", "requirement_test_suite")
34

45
evaluate_test_suite(
56
name = "evaluate_tests",
@@ -8,3 +9,7 @@ evaluate_test_suite(
89
deps_test_suite(
910
name = "deps_tests",
1011
)
12+
13+
requirement_test_suite(
14+
name = "requirement_tests",
15+
)
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Copyright 2025 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
"""Tests for parsing the requirement specifier."""
15+
16+
load("@rules_testing//lib:test_suite.bzl", "test_suite")
17+
load("//python/private/pypi:pep508_requirement.bzl", "requirement") # buildifier: disable=bzl-visibility
18+
19+
_tests = []
20+
21+
def _test_all(env):
22+
cases = [
23+
("name[foo]", ("name", ["foo"])),
24+
("name[ Foo123 ]", ("name", ["Foo123"])),
25+
(" name1[ foo ] ", ("name1", ["foo"])),
26+
("Name[foo]", ("name", ["foo"])),
27+
("name_foo[bar]", ("name-foo", ["bar"])),
28+
(
29+
"name [fred,bar] @ http://foo.com ; python_version=='2.7'",
30+
("name", ["fred", "bar"]),
31+
),
32+
(
33+
"name[quux, strange];python_version<'2.7' and platform_version=='2'",
34+
("name", ["quux", "strange"]),
35+
),
36+
(
37+
"name; (os_name=='a' or os_name=='b') and os_name=='c'",
38+
("name", None),
39+
),
40+
(
41+
"name@http://foo.com",
42+
("name", None),
43+
),
44+
]
45+
46+
for case, expected in cases:
47+
got = requirement(case)
48+
env.expect.that_str(got.name).equals(expected[0])
49+
if expected[1] != None:
50+
env.expect.that_collection(got.extras).contains_exactly(expected[1])
51+
52+
_tests.append(_test_all)
53+
54+
def requirement_test_suite(name): # buildifier: disable=function-docstring
55+
test_suite(
56+
name = name,
57+
basic_tests = _tests,
58+
)

tests/pypi/whl_installer/wheel_installer_test.py

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -22,39 +22,6 @@
2222
from python.private.pypi.whl_installer import wheel_installer
2323

2424

25-
class TestRequirementExtrasParsing(unittest.TestCase):
26-
def test_parses_requirement_for_extra(self) -> None:
27-
cases = [
28-
("name[foo]", ("name", frozenset(["foo"]))),
29-
("name[ Foo123 ]", ("name", frozenset(["Foo123"]))),
30-
(" name1[ foo ] ", ("name1", frozenset(["foo"]))),
31-
("Name[foo]", ("name", frozenset(["foo"]))),
32-
("name_foo[bar]", ("name-foo", frozenset(["bar"]))),
33-
(
34-
"name [fred,bar] @ http://foo.com ; python_version=='2.7'",
35-
("name", frozenset(["fred", "bar"])),
36-
),
37-
(
38-
"name[quux, strange];python_version<'2.7' and platform_version=='2'",
39-
("name", frozenset(["quux", "strange"])),
40-
),
41-
(
42-
"name; (os_name=='a' or os_name=='b') and os_name=='c'",
43-
(None, None),
44-
),
45-
(
46-
"name@http://foo.com",
47-
(None, None),
48-
),
49-
]
50-
51-
for case, expected in cases:
52-
with self.subTest():
53-
self.assertTupleEqual(
54-
wheel_installer._parse_requirement_for_extra(case), expected
55-
)
56-
57-
5825
class TestWhlFilegroup(unittest.TestCase):
5926
def setUp(self) -> None:
6027
self.wheel_name = "example_minimal_package-0.0.1-py3-none-any.whl"
@@ -91,13 +58,8 @@ def test_wheel_exists(self) -> None:
9158
metadata_file_content = json.load(metadata_file)
9259

9360
want = dict(
94-
abi="cp311",
95-
version="0.0.1",
96-
name="example-minimal-package",
9761
entry_points=[],
98-
extras=[],
9962
python_version="3.11.11",
100-
requires_dist=[],
10163
)
10264
self.assertEqual(want, metadata_file_content)
10365

0 commit comments

Comments
 (0)