Skip to content

Commit e782528

Browse files
authored
Merge branch 'main' into martani/gazelle-pyi-deps
2 parents 80d215f + 4ec1e80 commit e782528

File tree

7 files changed

+160
-26
lines changed

7 files changed

+160
-26
lines changed

python/private/py_library.bzl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,7 @@ def py_library_impl(ctx, *, semantics):
161161
imports = []
162162
venv_symlinks = []
163163

164-
package, version_str = _get_package_and_version(ctx)
165-
imports, venv_symlinks = _get_imports_and_venv_symlinks(ctx, semantics, package, version_str)
164+
imports, venv_symlinks = _get_imports_and_venv_symlinks(ctx, semantics)
166165

167166
cc_info = semantics.get_cc_info_for_library(ctx)
168167
py_info, deps_transitive_sources, builtins_py_info = create_py_info(
@@ -241,10 +240,11 @@ def _get_package_and_version(ctx):
241240
version.normalize(version_str), # will have no dashes either
242241
)
243242

244-
def _get_imports_and_venv_symlinks(ctx, semantics, package, version_str):
243+
def _get_imports_and_venv_symlinks(ctx, semantics):
245244
imports = depset()
246245
venv_symlinks = []
247246
if VenvsSitePackages.is_enabled(ctx):
247+
package, version_str = _get_package_and_version(ctx)
248248
venv_symlinks = _get_venv_symlinks(ctx, package, version_str)
249249
else:
250250
imports = collect_imports(ctx, semantics)

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",

python/private/pypi/namespace_pkgs.bzl

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,25 +59,32 @@ def get_files(*, srcs, ignored_dirnames = [], root = None):
5959

6060
return sorted([d for d in dirs if d not in ignored])
6161

62-
def create_inits(**kwargs):
62+
def create_inits(*, srcs, ignored_dirnames = [], root = None, copy_file = copy_file, **kwargs):
6363
"""Create init files and return the list to be included `py_library` srcs.
6464
6565
Args:
66-
**kwargs: passed to {obj}`get_files`.
66+
srcs: {type}`src` a list of files to be passed to {bzl:obj}`py_library`
67+
as `srcs` and `data`. This is usually a result of a {obj}`glob`.
68+
ignored_dirnames: {type}`str` a list of patterns to ignore.
69+
root: {type}`str` the prefix to use as the root.
70+
copy_file: the `copy_file` rule to copy files in build context.
71+
**kwargs: passed to {obj}`copy_file`.
6772
6873
Returns:
6974
{type}`list[str]` to be included as part of `py_library`.
7075
"""
71-
srcs = []
72-
for out in get_files(**kwargs):
76+
ret = []
77+
for i, out in enumerate(get_files(srcs = srcs, ignored_dirnames = ignored_dirnames, root = root)):
7378
src = "{}/__init__.py".format(out)
74-
srcs.append(srcs)
79+
ret.append(src)
7580

7681
copy_file(
77-
name = "_cp_{}_namespace".format(out),
82+
# For the target name, use a number instead of trying to convert an output
83+
# path into a valid label.
84+
name = "_cp_{}_namespace".format(i),
7885
src = _TEMPLATE,
7986
out = src,
8087
**kwargs
8188
)
8289

83-
return srcs
90+
return ret

tests/pypi/namespace_pkgs/namespace_pkgs_tests.bzl

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
""
22

33
load("@rules_testing//lib:analysis_test.bzl", "test_suite")
4-
load("//python/private/pypi:namespace_pkgs.bzl", "get_files") # buildifier: disable=bzl-visibility
4+
load("//python/private/pypi:namespace_pkgs.bzl", "create_inits", "get_files") # buildifier: disable=bzl-visibility
55

66
_tests = []
77

@@ -160,6 +160,45 @@ def test_skips_ignored_directories(env):
160160

161161
_tests.append(test_skips_ignored_directories)
162162

163+
def _test_create_inits(env):
164+
srcs = [
165+
"nested/root/foo/bar/biz.py",
166+
"nested/root/foo/bee/boo.py",
167+
"nested/root/foo/buu/__init__.py",
168+
"nested/root/foo/buu/bii.py",
169+
]
170+
copy_file_calls = []
171+
template = Label("//python/private/pypi:namespace_pkg_tmpl.py")
172+
173+
got = create_inits(
174+
srcs = srcs,
175+
root = "nested/root",
176+
copy_file = lambda **kwargs: copy_file_calls.append(kwargs),
177+
)
178+
env.expect.that_collection(got).contains_exactly([
179+
call["out"]
180+
for call in copy_file_calls
181+
])
182+
env.expect.that_collection(copy_file_calls).contains_exactly([
183+
{
184+
"name": "_cp_0_namespace",
185+
"out": "nested/root/foo/__init__.py",
186+
"src": template,
187+
},
188+
{
189+
"name": "_cp_1_namespace",
190+
"out": "nested/root/foo/bar/__init__.py",
191+
"src": template,
192+
},
193+
{
194+
"name": "_cp_2_namespace",
195+
"out": "nested/root/foo/bee/__init__.py",
196+
"src": template,
197+
},
198+
])
199+
200+
_tests.append(_test_create_inits)
201+
163202
def namespace_pkgs_test_suite(name):
164203
test_suite(
165204
name = name,

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)