Skip to content

Commit 4ec1e80

Browse files
docs,tests: Clarify how py_wheel.strip_path_prefixes works; add test case (#3027)
Include a minor change to `arcname_from` to support cases where the distribution_prefix is the empty string. Fixes #3017 --------- Co-authored-by: Richard Levasseur <[email protected]>
1 parent aab2650 commit 4ec1e80

File tree

4 files changed

+103
-15
lines changed

4 files changed

+103
-15
lines changed

python/private/py_wheel.bzl

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,15 @@ _other_attrs = {
217217
),
218218
"strip_path_prefixes": attr.string_list(
219219
default = [],
220-
doc = "path prefixes to strip from files added to the generated package",
220+
doc = """\
221+
Path prefixes to strip from files added to the generated package.
222+
Prefixes are checked **in order** and only the **first match** will be used.
223+
224+
For example:
225+
+ `["foo", "foo/bar/baz"]` will strip `"foo/bar/baz/file.py"` to `"bar/baz/file.py"`
226+
+ `["foo/bar/baz", "foo"]` will strip `"foo/bar/baz/file.py"` to `"file.py"` and
227+
`"foo/file2.py"` to `"file2.py"`
228+
""",
221229
),
222230
"summary": attr.string(
223231
doc = "A one-line summary of what the distribution does",

tests/tools/BUILD.bazel

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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+
load("//python:py_test.bzl", "py_test")
15+
16+
licenses(["notice"])
17+
18+
py_test(
19+
name = "wheelmaker_test",
20+
size = "small",
21+
srcs = ["wheelmaker_test.py"],
22+
deps = ["//tools:wheelmaker"],
23+
)

tests/tools/wheelmaker_test.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import unittest
2+
3+
import tools.wheelmaker as wheelmaker
4+
5+
6+
class ArcNameFromTest(unittest.TestCase):
7+
def test_arcname_from(self) -> None:
8+
# (name, distribution_prefix, strip_path_prefixes, want) tuples
9+
checks = [
10+
("a/b/c/file.py", "", [], "a/b/c/file.py"),
11+
("a/b/c/file.py", "", ["a"], "/b/c/file.py"),
12+
("a/b/c/file.py", "", ["a/b/"], "c/file.py"),
13+
# only first found is used and it's not cumulative.
14+
("a/b/c/file.py", "", ["a/", "b/"], "b/c/file.py"),
15+
# Examples from docs
16+
("foo/bar/baz/file.py", "", ["foo", "foo/bar/baz"], "/bar/baz/file.py"),
17+
("foo/bar/baz/file.py", "", ["foo/bar/baz", "foo"], "/file.py"),
18+
("foo/file2.py", "", ["foo/bar/baz", "foo"], "/file2.py"),
19+
# Files under the distribution prefix (eg mylib-1.0.0-dist-info)
20+
# are unmodified
21+
("mylib-0.0.1-dist-info/WHEEL", "mylib", [], "mylib-0.0.1-dist-info/WHEEL"),
22+
("mylib/a/b/c/WHEEL", "mylib", ["mylib"], "mylib/a/b/c/WHEEL"),
23+
]
24+
for name, prefix, strip, want in checks:
25+
with self.subTest(
26+
name=name,
27+
distribution_prefix=prefix,
28+
strip_path_prefixes=strip,
29+
want=want,
30+
):
31+
got = wheelmaker.arcname_from(
32+
name=name, distribution_prefix=prefix, strip_path_prefixes=strip
33+
)
34+
self.assertEqual(got, want)
35+
36+
37+
if __name__ == "__main__":
38+
unittest.main()

tools/wheelmaker.py

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import stat
2525
import sys
2626
import zipfile
27+
from collections.abc import Iterable
2728
from pathlib import Path
2829

2930
_ZIP_EPOCH = (1980, 1, 1, 0, 0, 0)
@@ -98,6 +99,30 @@ def normalize_pep440(version):
9899
return str(packaging.version.Version(f"0+{sanitized}"))
99100

100101

102+
def arcname_from(
103+
name: str, distribution_prefix: str, strip_path_prefixes: Sequence[str] = ()
104+
) -> str:
105+
"""Return the within-archive name for a given file path name.
106+
107+
Prefixes to strip are checked in order and only the first match will be used.
108+
109+
Args:
110+
name: The file path eg 'mylib/a/b/c/file.py'
111+
distribution_prefix: The
112+
strip_path_prefixes: Remove these prefixes from names.
113+
"""
114+
# Always use unix path separators.
115+
normalized_arcname = name.replace(os.path.sep, "/")
116+
# Don't manipulate names filenames in the .distinfo or .data directories.
117+
if distribution_prefix and normalized_arcname.startswith(distribution_prefix):
118+
return normalized_arcname
119+
for prefix in strip_path_prefixes:
120+
if normalized_arcname.startswith(prefix):
121+
return normalized_arcname[len(prefix) :]
122+
123+
return normalized_arcname
124+
125+
101126
class _WhlFile(zipfile.ZipFile):
102127
def __init__(
103128
self,
@@ -126,18 +151,6 @@ def data_path(self, basename):
126151
def add_file(self, package_filename, real_filename):
127152
"""Add given file to the distribution."""
128153

129-
def arcname_from(name):
130-
# Always use unix path separators.
131-
normalized_arcname = name.replace(os.path.sep, "/")
132-
# Don't manipulate names filenames in the .distinfo or .data directories.
133-
if normalized_arcname.startswith(self._distribution_prefix):
134-
return normalized_arcname
135-
for prefix in self._strip_path_prefixes:
136-
if normalized_arcname.startswith(prefix):
137-
return normalized_arcname[len(prefix) :]
138-
139-
return normalized_arcname
140-
141154
if os.path.isdir(real_filename):
142155
directory_contents = os.listdir(real_filename)
143156
for file_ in directory_contents:
@@ -147,7 +160,11 @@ def arcname_from(name):
147160
)
148161
return
149162

150-
arcname = arcname_from(package_filename)
163+
arcname = arcname_from(
164+
package_filename,
165+
distribution_prefix=self._distribution_prefix,
166+
strip_path_prefixes=self._strip_path_prefixes,
167+
)
151168
zinfo = self._zipinfo(arcname)
152169

153170
# Write file to the zip archive while computing the hash and length
@@ -569,7 +586,9 @@ def get_new_requirement_line(reqs_text, extra):
569586
else:
570587
return f"Requires-Dist: {req.name}{req_extra_deps}{req.specifier}; {req.marker}"
571588
else:
572-
return f"Requires-Dist: {req.name}{req_extra_deps}{req.specifier}; {extra}".strip(" ;")
589+
return f"Requires-Dist: {req.name}{req_extra_deps}{req.specifier}; {extra}".strip(
590+
" ;"
591+
)
573592

574593
for meta_line in metadata.splitlines():
575594
if not meta_line.startswith("Requires-Dist: "):

0 commit comments

Comments
 (0)