Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit c5c03b2

Browse files
authored
feat: Add support for python-wheel data directory (#1801)
Fixes #1777 * Adds `data_files` attribute to `py_wheel` rule. * Minimal validation of the data-files target directories per [specification](https://packaging.python.org/en/latest/specifications/binary-distribution-format/#installing-a-wheel-distribution-1-0-py32-none-any-whl) * Added two tests. * Added example
1 parent 5ed8d3f commit c5c03b2

File tree

7 files changed

+178
-27
lines changed

7 files changed

+178
-27
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ A brief description of the categories of changes:
4040
_default_ visibility of generated targets. See the [docs][python_default_visibility]
4141
for details.
4242

43+
* (wheel) Add support for `data_files` attributes in py_wheel rule
44+
([#1777](https://github.com/bazelbuild/rules_python/issues/1777))
45+
4346
[0.XX.0]: https://github.com/bazelbuild/rules_python/releases/tag/0.XX.0
4447
[python_default_visibility]: gazelle/README.md#directive-python_default_visibility
4548

examples/wheel/BUILD.bazel

+15
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,20 @@ py_wheel(
312312
deps = [":example_pkg"],
313313
)
314314

315+
# Package just a specific py_libraries, without their dependencies
316+
py_wheel(
317+
name = "minimal_data_files",
318+
testonly = True, # Set this to verify the generated .dist target doesn't break things
319+
320+
# Re-using some files already checked into the repo.
321+
data_files = {
322+
"//examples/wheel:NOTICE": "scripts/NOTICE",
323+
"README.md": "data/target/path/README.md",
324+
},
325+
distribution = "minimal_data_files",
326+
version = "0.0.1",
327+
)
328+
315329
py_test(
316330
name = "wheel_test",
317331
srcs = ["wheel_test.py"],
@@ -321,6 +335,7 @@ py_test(
321335
":custom_package_root_multi_prefix_reverse_order",
322336
":customized",
323337
":filename_escaping",
338+
":minimal_data_files",
324339
":minimal_with_py_library",
325340
":minimal_with_py_library_with_stamp",
326341
":minimal_with_py_package",

examples/wheel/wheel_test.py

+17
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,23 @@ def test_requires_file_and_extra_requires_files(self):
472472
requires,
473473
)
474474

475+
def test_minimal_data_files(self):
476+
filename = self._get_path("minimal_data_files-0.0.1-py3-none-any.whl")
477+
478+
with zipfile.ZipFile(filename) as zf:
479+
self.assertAllEntriesHasReproducibleMetadata(zf)
480+
metadata_file = None
481+
self.assertEqual(
482+
zf.namelist(),
483+
[
484+
"minimal_data_files-0.0.1.dist-info/WHEEL",
485+
"minimal_data_files-0.0.1.dist-info/METADATA",
486+
"minimal_data_files-0.0.1.data/data/target/path/README.md",
487+
"minimal_data_files-0.0.1.data/scripts/NOTICE",
488+
"minimal_data_files-0.0.1.dist-info/RECORD",
489+
]
490+
)
491+
475492

476493
if __name__ == "__main__":
477494
unittest.main()

python/private/py_wheel.bzl

+28
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ See [`py_wheel_dist`](#py_wheel_dist) for more info.
120120

121121
_feature_flags = {}
122122

123+
ALLOWED_DATA_FILE_PREFIX = ("purelib", "platlib", "headers", "scripts", "data")
123124
_requirement_attrs = {
124125
"extra_requires": attr.string_list_dict(
125126
doc = ("A mapping of [extras](https://peps.python.org/pep-0508/#extras) options to lists of requirements (similar to `requires`). This attribute " +
@@ -172,6 +173,11 @@ _other_attrs = {
172173
"classifiers": attr.string_list(
173174
doc = "A list of strings describing the categories for the package. For valid classifiers see https://pypi.org/classifiers",
174175
),
176+
"data_files": attr.label_keyed_string_dict(
177+
doc = ("Any file that is not normally installed inside site-packages goes into the .data directory, named " +
178+
"as the .dist-info directory but with the .data/ extension. Allowed paths: {prefixes}".format(prefixes = ALLOWED_DATA_FILE_PREFIX)),
179+
allow_files = True,
180+
),
175181
"description_content_type": attr.string(
176182
doc = ("The type of contents in description_file. " +
177183
"If not provided, the type will be inferred from the extension of description_file. " +
@@ -473,6 +479,28 @@ def _py_wheel_impl(ctx):
473479
filename + ";" + target_files[0].path,
474480
)
475481

482+
for target, filename in ctx.attr.data_files.items():
483+
target_files = target.files.to_list()
484+
if len(target_files) != 1:
485+
fail(
486+
"Multi-file target listed in data_files %s",
487+
filename,
488+
)
489+
490+
if filename.partition("/")[0] not in ALLOWED_DATA_FILE_PREFIX:
491+
fail(
492+
"The target data file must start with one of these prefixes: '%s'. Target filepath: '%s'" %
493+
(
494+
",".join(ALLOWED_DATA_FILE_PREFIX),
495+
filename,
496+
),
497+
)
498+
other_inputs.extend(target_files)
499+
args.add(
500+
"--data_files",
501+
filename + ";" + target_files[0].path,
502+
)
503+
476504
ctx.actions.run(
477505
mnemonic = "PyWheel",
478506
inputs = depset(direct = other_inputs, transitive = [inputs_to_package]),

python/private/repack_whl.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,9 @@ def main(sys_argv):
150150
logging.debug(f"Found dist-info dir: {distinfo_dir}")
151151
record_path = distinfo_dir / "RECORD"
152152
record_contents = record_path.read_text() if record_path.exists() else ""
153+
distribution_prefix = distinfo_dir.with_suffix("").name
153154

154-
with _WhlFile(args.output, mode="w", distinfo_dir=distinfo_dir) as out:
155+
with _WhlFile(args.output, mode="w", distribution_prefix=distribution_prefix) as out:
155156
for p in _files_to_pack(patched_wheel_dir, record_contents):
156157
rel_path = p.relative_to(patched_wheel_dir)
157158
out.add_file(str(rel_path), p)

tests/py_wheel/py_wheel_tests.bzl

+74
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"""Test for py_wheel."""
1515

1616
load("@rules_testing//lib:analysis_test.bzl", "analysis_test", "test_suite")
17+
load("@rules_testing//lib:truth.bzl", "matching")
1718
load("@rules_testing//lib:util.bzl", rt_util = "util")
1819
load("//python:packaging.bzl", "py_wheel")
1920
load("//python/private:py_wheel_normalize_pep440.bzl", "normalize_pep440") # buildifier: disable=bzl-visibility
@@ -46,6 +47,79 @@ def _test_metadata_impl(env, target):
4647

4748
_tests.append(_test_metadata)
4849

50+
def _test_data(name):
51+
rt_util.helper_target(
52+
py_wheel,
53+
name = name + "_data",
54+
distribution = "mydist_" + name,
55+
version = "0.0.0",
56+
data_files = {
57+
"source_name": "scripts/wheel_name",
58+
},
59+
)
60+
analysis_test(
61+
name = name,
62+
impl = _test_data_impl,
63+
target = name + "_data",
64+
)
65+
66+
def _test_data_impl(env, target):
67+
action = env.expect.that_target(target).action_named(
68+
"PyWheel",
69+
)
70+
action.contains_at_least_args(["--data_files", "scripts/wheel_name;tests/py_wheel/source_name"])
71+
action.contains_at_least_inputs(["tests/py_wheel/source_name"])
72+
73+
_tests.append(_test_data)
74+
75+
def _test_data_bad_path(name):
76+
rt_util.helper_target(
77+
py_wheel,
78+
name = name + "_data",
79+
distribution = "mydist_" + name,
80+
version = "0.0.0",
81+
data_files = {
82+
"source_name": "unsupported_path/wheel_name",
83+
},
84+
)
85+
analysis_test(
86+
name = name,
87+
impl = _test_data_bad_path_impl,
88+
target = name + "_data",
89+
expect_failure = True,
90+
)
91+
92+
def _test_data_bad_path_impl(env, target):
93+
env.expect.that_target(target).failures().contains_predicate(
94+
matching.str_matches("target data file must start with"),
95+
)
96+
97+
_tests.append(_test_data_bad_path)
98+
99+
def _test_data_bad_path_but_right_prefix(name):
100+
rt_util.helper_target(
101+
py_wheel,
102+
name = name + "_data",
103+
distribution = "mydist_" + name,
104+
version = "0.0.0",
105+
data_files = {
106+
"source_name": "scripts2/wheel_name",
107+
},
108+
)
109+
analysis_test(
110+
name = name,
111+
impl = _test_data_bad_path_but_right_prefix_impl,
112+
target = name + "_data",
113+
expect_failure = True,
114+
)
115+
116+
def _test_data_bad_path_but_right_prefix_impl(env, target):
117+
env.expect.that_target(target).failures().contains_predicate(
118+
matching.str_matches("target data file must start with"),
119+
)
120+
121+
_tests.append(_test_data_bad_path_but_right_prefix)
122+
49123
def _test_content_type_from_attr(name):
50124
rt_util.helper_target(
51125
py_wheel,

tools/wheelmaker.py

+39-26
Original file line numberDiff line numberDiff line change
@@ -102,29 +102,33 @@ def __init__(
102102
filename,
103103
*,
104104
mode,
105-
distinfo_dir: str | Path,
105+
distribution_prefix: str,
106106
strip_path_prefixes=None,
107107
compression=zipfile.ZIP_DEFLATED,
108108
**kwargs,
109109
):
110-
self._distinfo_dir: str = Path(distinfo_dir).name
110+
self._distribution_prefix = distribution_prefix
111+
111112
self._strip_path_prefixes = strip_path_prefixes or []
112113
# Entries for the RECORD file as (filename, hash, size) tuples.
113114
self._record = []
114115

115116
super().__init__(filename, mode=mode, compression=compression, **kwargs)
116117

117118
def distinfo_path(self, basename):
118-
return f"{self._distinfo_dir}/{basename}"
119+
return f"{self._distribution_prefix}.dist-info/{basename}"
120+
121+
def data_path(self, basename):
122+
return f"{self._distribution_prefix}.data/{basename}"
119123

120124
def add_file(self, package_filename, real_filename):
121125
"""Add given file to the distribution."""
122126

123127
def arcname_from(name):
124128
# Always use unix path separators.
125129
normalized_arcname = name.replace(os.path.sep, "/")
126-
# Don't manipulate names filenames in the .distinfo directory.
127-
if normalized_arcname.startswith(self._distinfo_dir):
130+
# Don't manipulate names filenames in the .distinfo or .data directories.
131+
if normalized_arcname.startswith(self._distribution_prefix):
128132
return normalized_arcname
129133
for prefix in self._strip_path_prefixes:
130134
if normalized_arcname.startswith(prefix):
@@ -237,11 +241,9 @@ def __init__(
237241
self._wheelname_fragment_distribution_name = escape_filename_distribution_name(
238242
self._name
239243
)
240-
self._distinfo_dir = (
241-
self._wheelname_fragment_distribution_name
242-
+ "-"
243-
+ self._version
244-
+ ".dist-info/"
244+
245+
self._distribution_prefix = (
246+
self._wheelname_fragment_distribution_name + "-" + self._version
245247
)
246248

247249
self._whlfile = None
@@ -250,7 +252,7 @@ def __enter__(self):
250252
self._whlfile = _WhlFile(
251253
self.filename(),
252254
mode="w",
253-
distinfo_dir=self._distinfo_dir,
255+
distribution_prefix=self._distribution_prefix,
254256
strip_path_prefixes=self._strip_path_prefixes,
255257
)
256258
return self
@@ -280,6 +282,9 @@ def disttags(self):
280282
def distinfo_path(self, basename):
281283
return self._whlfile.distinfo_path(basename)
282284

285+
def data_path(self, basename):
286+
return self._whlfile.data_path(basename)
287+
283288
def add_file(self, package_filename, real_filename):
284289
"""Add given file to the distribution."""
285290
self._whlfile.add_file(package_filename, real_filename)
@@ -436,6 +441,12 @@ def parse_args() -> argparse.Namespace:
436441
help="'filename;real_path' pairs listing extra files to include in"
437442
"dist-info directory. Can be supplied multiple times.",
438443
)
444+
contents_group.add_argument(
445+
"--data_files",
446+
action="append",
447+
help="'filename;real_path' pairs listing data files to include in"
448+
"data directory. Can be supplied multiple times.",
449+
)
439450

440451
build_group = parser.add_argument_group("Building requirements")
441452
build_group.add_argument(
@@ -452,25 +463,25 @@ def parse_args() -> argparse.Namespace:
452463
return parser.parse_args(sys.argv[1:])
453464

454465

466+
def _parse_file_pairs(content: List[str]) -> List[List[str]]:
467+
"""
468+
Parse ; delimited lists of files into a 2D list.
469+
"""
470+
return [i.split(";", maxsplit=1) for i in content or []]
471+
472+
455473
def main() -> None:
456474
arguments = parse_args()
457475

458-
if arguments.input_file:
459-
input_files = [i.split(";") for i in arguments.input_file]
460-
else:
461-
input_files = []
462-
463-
if arguments.extra_distinfo_file:
464-
extra_distinfo_file = [i.split(";") for i in arguments.extra_distinfo_file]
465-
else:
466-
extra_distinfo_file = []
476+
input_files = _parse_file_pairs(arguments.input_file)
477+
extra_distinfo_file = _parse_file_pairs(arguments.extra_distinfo_file)
478+
data_files = _parse_file_pairs(arguments.data_files)
467479

468-
if arguments.input_file_list:
469-
for input_file in arguments.input_file_list:
470-
with open(input_file) as _file:
471-
input_file_list = _file.read().splitlines()
472-
for _input_file in input_file_list:
473-
input_files.append(_input_file.split(";"))
480+
for input_file in arguments.input_file_list:
481+
with open(input_file) as _file:
482+
input_file_list = _file.read().splitlines()
483+
for _input_file in input_file_list:
484+
input_files.append(_input_file.split(";"))
474485

475486
all_files = get_files_to_package(input_files)
476487
# Sort the files for reproducible order in the archive.
@@ -570,6 +581,8 @@ def main() -> None:
570581
)
571582

572583
# Sort the files for reproducible order in the archive.
584+
for filename, real_path in sorted(data_files):
585+
maker.add_file(maker.data_path(filename), real_path)
573586
for filename, real_path in sorted(extra_distinfo_file):
574587
maker.add_file(maker.distinfo_path(filename), real_path)
575588

0 commit comments

Comments
 (0)