Skip to content

Commit 6295ea0

Browse files
antoniojkimAntonio Kim
andauthored
feat(wheel): Add support for add_path_prefix (#3679)
This change is being made to support prepending a prefix to the file paths in the wheel. This is useful for customizing the import path for the package. For example, if your code is implemented in `src/module` you may want to package this code for distribution as `namespace/module` so that the import is ```python import namespace.module ``` I think ideally, this should have been implemented as `remap_path_prefix` which is a map which specifies which prefixes should be changed and what to change them to. However, seeing as `strip_path_prefixes` already exists, I thought the simpler thing to do here was to just add the `add_path_prefix` argument. Implements #515 ## Tests Tested by building and manually inspecting the wheels generated by ``` $ bazel build //examples/wheel/... ``` More specifically ``` $ bazel build //examples/wheel:custom_prefix_package_root INFO: Analyzed target //examples/wheel:custom_prefix_package_root (1 packages loaded, 4 targets configured). INFO: Found 1 target... Target //examples/wheel:custom_prefix_package_root up-to-date: bazel-bin/examples/wheel/examples_custom_prefix_package_root-0.0.1-py3-none-any.whl INFO: Elapsed time: 0.357s, Critical Path: 0.01s INFO: 2 processes: 3 action cache hit, 1 disk cache hit, 1 internal. $ uv pip install bazel-bin/examples/wheel/examples_custom_prefix_package_root-0.0.1-py3-none-any.whl Installed 1 package in 3ms + examples-custom-prefix-package-root==0.0.1 $ python -c "import custom_prefix.wheel" ``` --------- Co-authored-by: Antonio Kim <antoniok@antoniojkim.com>
1 parent bd7696a commit 6295ea0

6 files changed

Lines changed: 187 additions & 39 deletions

File tree

.bazelrc.deleted_packages

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ common --deleted_packages=gazelle/manifest/hasher
2727
common --deleted_packages=gazelle/manifest/test
2828
common --deleted_packages=gazelle/modules_mapping
2929
common --deleted_packages=gazelle/python
30-
common --deleted_packages=gazelle/pythonconfig
3130
common --deleted_packages=gazelle/python/private
31+
common --deleted_packages=gazelle/pythonconfig
3232
common --deleted_packages=tests/integration/compile_pip_requirements
3333
common --deleted_packages=tests/integration/compile_pip_requirements_test_from_external_repo
3434
common --deleted_packages=tests/integration/custom_commands

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,8 @@ Other changes:
138138
venvs.
139139
* {obj}`PyExecutableInfo.venv_interpreter_runfiles`, and
140140
{obj}`PyExecutableInfo.venv_interpreter_symlinks` adde
141+
* (wheel) Add support for `add_path_prefix` argument in `py_wheel` which can be
142+
used to prepend a prefix to the files in the wheel.
141143

142144
{#v1-9-0}
143145
## [1.9.0] - 2026-02-21

examples/wheel/BUILD.bazel

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,25 @@ py_wheel(
230230
],
231231
)
232232

233+
# An example of how to change the wheel package root directory using 'add_path_prefix'.
234+
py_wheel(
235+
name = "custom_prefix_package_root",
236+
add_path_prefix = "custom_prefix",
237+
# Package data. We're building "examples_custom_prefix_package_root-0.0.1-py3-none-any.whl"
238+
distribution = "examples_custom_prefix_package_root",
239+
entry_points = {
240+
"console_scripts": ["main = foo.bar:baz"],
241+
},
242+
python_tag = "py3",
243+
strip_path_prefixes = [
244+
"examples",
245+
],
246+
version = "0.0.1",
247+
deps = [
248+
":example_pkg",
249+
],
250+
)
251+
233252
py_wheel(
234253
name = "python_requires_in_a_package",
235254
distribution = "example_python_requires_in_a_package",

python/private/py_wheel.bzl

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,22 @@ entry_points, e.g. `{'console_scripts': ['main = examples.wheel.main:main']}`.
170170
}
171171

172172
_other_attrs = {
173+
"add_path_prefix": attr.string(
174+
default = "",
175+
doc = """\
176+
Path prefix to prepend to files added to the generated package.
177+
This prefix will be prepended **after** the paths are first stripped of the prefixes
178+
specified in `strip_path_prefixes`.
179+
180+
For example:
181+
+ `"foo/" will prepend to `"bar/baz/file.py"` as `"foo/bar/baz/file.py"`
182+
+ `"foo_" will prepend to `"bar/baz/file.py"` as `"foo_bar/baz/file.py"`
183+
+ `stripping ["bar/"] and adding "foo/" will change `"bar/baz/file.py"` to `"foo/baz/file.py"`
184+
:::{versionadded} VERSION_NEXT_FEATURE
185+
The {attr}`add_path_prefix` attribute was added.
186+
:::
187+
""",
188+
),
173189
"author": attr.string(
174190
doc = "A string specifying the author of the package.",
175191
default = "",
@@ -389,6 +405,7 @@ def _py_wheel_impl(ctx):
389405
args.add("--out", outfile)
390406
args.add("--name_file", name_file)
391407
args.add_all(ctx.attr.strip_path_prefixes, format_each = "--strip_path_prefix=%s")
408+
args.add("--path_prefix", ctx.attr.add_path_prefix)
392409

393410
# Pass workspace status files if stamping is enabled
394411
if is_stamping_enabled(ctx.attr):

tests/tools/wheelmaker_test.py

Lines changed: 91 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import io
22
import unittest
3+
from dataclasses import dataclass, field
34

45
import tools.wheelmaker as wheelmaker
56

@@ -34,41 +35,108 @@ def test_quote_all_false_leaves_simple_filenames_unquoted(self) -> None:
3435
def test_quote_all_quotes_filenames_with_commas(self) -> None:
3536
"""Filenames with commas are always quoted, regardless of quote_all_filenames."""
3637
whl = self._make_whl_file(quote_all=True)
37-
self.assertEqual(whl._quote_filename("foo,bar/baz.py"), '"foo,bar/baz.py"')
38+
self.assertEqual(
39+
whl._quote_filename("foo,bar/baz.py"), '"foo,bar/baz.py"'
40+
)
3841

3942
whl = self._make_whl_file(quote_all=False)
40-
self.assertEqual(whl._quote_filename("foo,bar/baz.py"), '"foo,bar/baz.py"')
43+
self.assertEqual(
44+
whl._quote_filename("foo,bar/baz.py"), '"foo,bar/baz.py"'
45+
)
46+
47+
48+
@dataclass
49+
class ArcNameTestCase:
50+
name: str
51+
expected: str
52+
distribution_prefix: str = ""
53+
strip_path_prefixes: list[str] = field(default_factory=list)
54+
add_path_prefix: str = ""
4155

4256

4357
class ArcNameFromTest(unittest.TestCase):
4458
def test_arcname_from(self) -> None:
45-
# (name, distribution_prefix, strip_path_prefixes, want) tuples
46-
checks = [
47-
("a/b/c/file.py", "", [], "a/b/c/file.py"),
48-
("a/b/c/file.py", "", ["a"], "/b/c/file.py"),
49-
("a/b/c/file.py", "", ["a/b/"], "c/file.py"),
59+
test_cases = [
60+
ArcNameTestCase(name="a/b/c/file.py", expected="a/b/c/file.py"),
61+
ArcNameTestCase(
62+
name="a/b/c/file.py",
63+
strip_path_prefixes=["a"],
64+
expected="/b/c/file.py",
65+
),
66+
ArcNameTestCase(
67+
name="a/b/c/file.py",
68+
strip_path_prefixes=["a/b/"],
69+
expected="c/file.py",
70+
),
5071
# only first found is used and it's not cumulative.
51-
("a/b/c/file.py", "", ["a/", "b/"], "b/c/file.py"),
72+
ArcNameTestCase(
73+
name="a/b/c/file.py",
74+
strip_path_prefixes=["a/", "b/"],
75+
expected="b/c/file.py",
76+
),
5277
# Examples from docs
53-
("foo/bar/baz/file.py", "", ["foo", "foo/bar/baz"], "/bar/baz/file.py"),
54-
("foo/bar/baz/file.py", "", ["foo/bar/baz", "foo"], "/file.py"),
55-
("foo/file2.py", "", ["foo/bar/baz", "foo"], "/file2.py"),
78+
ArcNameTestCase(
79+
name="foo/bar/baz/file.py",
80+
strip_path_prefixes=["foo", "foo/bar/baz"],
81+
expected="/bar/baz/file.py",
82+
),
83+
ArcNameTestCase(
84+
name="foo/bar/baz/file.py",
85+
strip_path_prefixes=["foo/bar/baz", "foo"],
86+
expected="/file.py",
87+
),
88+
ArcNameTestCase(
89+
name="foo/file2.py",
90+
strip_path_prefixes=["foo/bar/baz", "foo"],
91+
expected="/file2.py",
92+
),
5693
# Files under the distribution prefix (eg mylib-1.0.0-dist-info)
5794
# are unmodified
58-
("mylib-0.0.1-dist-info/WHEEL", "mylib", [], "mylib-0.0.1-dist-info/WHEEL"),
59-
("mylib/a/b/c/WHEEL", "mylib", ["mylib"], "mylib/a/b/c/WHEEL"),
95+
ArcNameTestCase(
96+
name="mylib-0.0.1-dist-info/WHEEL",
97+
distribution_prefix="mylib",
98+
expected="mylib-0.0.1-dist-info/WHEEL",
99+
),
100+
ArcNameTestCase(
101+
name="mylib/a/b/c/WHEEL",
102+
distribution_prefix="mylib",
103+
strip_path_prefixes=["mylib"],
104+
expected="mylib/a/b/c/WHEEL",
105+
),
106+
# Check that prefixes are added
107+
ArcNameTestCase(
108+
name="a/b/c/file.py",
109+
add_path_prefix="namespace/",
110+
expected="namespace/a/b/c/file.py",
111+
),
112+
ArcNameTestCase(
113+
name="a/b/c/file.py",
114+
strip_path_prefixes=["a"],
115+
add_path_prefix="namespace",
116+
expected="namespace/b/c/file.py",
117+
),
118+
ArcNameTestCase(
119+
name="a/b/c/file.py",
120+
strip_path_prefixes=["a/b/"],
121+
add_path_prefix="namespace_",
122+
expected="namespace_c/file.py",
123+
),
60124
]
61-
for name, prefix, strip, want in checks:
125+
for test_case in test_cases:
62126
with self.subTest(
63-
name=name,
64-
distribution_prefix=prefix,
65-
strip_path_prefixes=strip,
66-
want=want,
127+
name=test_case.name,
128+
distribution_prefix=test_case.distribution_prefix,
129+
strip_path_prefixes=test_case.strip_path_prefixes,
130+
add_path_prefix=test_case.add_path_prefix,
131+
want=test_case.expected,
67132
):
68133
got = wheelmaker.arcname_from(
69-
name=name, distribution_prefix=prefix, strip_path_prefixes=strip
134+
name=test_case.name,
135+
distribution_prefix=test_case.distribution_prefix,
136+
strip_path_prefixes=test_case.strip_path_prefixes,
137+
add_path_prefix=test_case.add_path_prefix,
70138
)
71-
self.assertEqual(got, want)
139+
self.assertEqual(got, test_case.expected)
72140

73141

74142
class GetNewRequirementLineTest(unittest.TestCase):
@@ -77,7 +145,9 @@ def test_requirement(self):
77145
self.assertEqual(result, "Requires-Dist: requests>=2.0")
78146

79147
def test_requirement_and_extra(self):
80-
result = wheelmaker.get_new_requirement_line("requests>=2.0", "extra=='dev'")
148+
result = wheelmaker.get_new_requirement_line(
149+
"requests>=2.0", "extra=='dev'"
150+
)
81151
self.assertEqual(result, "Requires-Dist: requests>=2.0; extra=='dev'")
82152

83153
def test_requirement_with_url(self):

0 commit comments

Comments
 (0)