Skip to content

Commit 4ed19e0

Browse files
committed
refactor: reimplement writing namespace pkgs in Starlark
With this PR I would like to facilitate the implementation of the venv layouts because we can in theory take the srcs and the data within the py_library and then use the expand_template to write the extra Python files if the namespace_pkgs flag is enabled. The old Python code has been removed and the extra generated files are written out with bazel_skylib write_file as a POC. This could be optimized by writing once and then doing symlinks to the original instead of writing the files, but I am not sure how this would behave, maybe fine? The implicit namespace_pkg init files are included to py_library if the site-packages config flag is set to false and I think this may help with continuing the implementation. NOTE, it seems that the pycross code that we have is using the namespace_pkg Python code, so that will be removed later in one PR. Work towards #2156
1 parent d6af2b7 commit 4ed19e0

14 files changed

+311
-260
lines changed

python/config_settings/BUILD.bazel

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,15 @@ string_flag(
217217
visibility = ["//visibility:public"],
218218
)
219219

220+
config_setting(
221+
name = "is_venvs_site_packages",
222+
flag_values = {
223+
":venvs_site_packages": VenvsSitePackages.YES,
224+
},
225+
# NOTE: Only public because it is used in whl_library repos.
226+
visibility = ["//visibility:public"],
227+
)
228+
220229
define_pypi_internal_flags(
221230
name = "define_pypi_internal_flags",
222231
)

python/private/pypi/BUILD.bazel

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ package(default_visibility = ["//:__subpackages__"])
1818

1919
licenses(["notice"])
2020

21+
exports_files(
22+
srcs = ["namespace_pkg_tmpl.py"],
23+
visibility = ["//visibility:public"],
24+
)
25+
2126
filegroup(
2227
name = "distribution",
2328
srcs = glob(
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# __path__ manipulation added by bazel-contrib/rules_python to support namespace pkgs.
2+
__path__ = __import__("pkgutil").extend_path(__path__, __name__)
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"""Utilities to get where we should write namespace pkg paths."""
2+
3+
load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
4+
5+
_ext = struct(
6+
py = ".py",
7+
pyd = ".pyd",
8+
so = ".so",
9+
pyc = ".pyc",
10+
)
11+
12+
_TEMPLATE = Label("//python/private/pypi:namespace_pkg_tmpl.py")
13+
14+
def _add_all(dirname, dirs):
15+
dir_path = "."
16+
for dir_name in dirname.split("/"):
17+
dir_path = "{}/{}".format(dir_path, dir_name)
18+
dirs[dir_path[2:]] = None
19+
20+
def get_files(*, srcs, ignored_dirnames = [], root = None):
21+
"""Get the list of filenames to write the namespace pkg files.
22+
23+
Args:
24+
srcs: {type}`src` a list of files to be passed to {bzl:obj}`py_library`
25+
as `srcs` and `data`. This is usually a result of a {obj}`glob`.
26+
ignored_dirnames: {type}`str` a list of patterns to ignore.
27+
root: {type}`str` the prefix to use as the root.
28+
29+
Returns:
30+
{type}`src` a list of paths to write the namespace pkg `__init__.py` file.
31+
"""
32+
dirs = {}
33+
ignored = {i: None for i in ignored_dirnames}
34+
35+
if root:
36+
_add_all(root, ignored)
37+
38+
for file in srcs:
39+
dirname, _, filename = file.rpartition("/")
40+
41+
if filename == "__init__.py":
42+
ignored[dirname] = None
43+
dirname, _, _ = dirname.rpartition("/")
44+
elif filename.endswith(_ext.py):
45+
pass
46+
elif filename.endswith(_ext.pyc):
47+
pass
48+
elif filename.endswith(_ext.pyd):
49+
pass
50+
elif filename.endswith(_ext.so):
51+
pass
52+
else:
53+
continue
54+
55+
if dirname in dirs or not dirname:
56+
continue
57+
58+
_add_all(dirname, dirs)
59+
60+
return sorted([d for d in dirs if d not in ignored])
61+
62+
def create_inits(**kwargs):
63+
"""Create init files and return the list to be included `py_library` srcs.
64+
65+
Args:
66+
**kwargs: passed to {obj}`get_files`.
67+
68+
Returns:
69+
{type}`list[str]` to be included as part of `py_library`.
70+
"""
71+
srcs = []
72+
for out in get_files(**kwargs):
73+
src = "{}/__init__.py".format(out)
74+
srcs.append(srcs)
75+
76+
copy_file(
77+
name = "_cp_{}_namespace".format(out),
78+
src = _TEMPLATE,
79+
out = src,
80+
**kwargs
81+
)
82+
83+
return srcs

python/private/pypi/whl_installer/arguments.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,6 @@ def parser(**kwargs: Any) -> argparse.ArgumentParser:
5757
action="store",
5858
help="Additional data exclusion parameters to add to the pip packages BUILD file.",
5959
)
60-
parser.add_argument(
61-
"--enable_implicit_namespace_pkgs",
62-
action="store_true",
63-
help="Disables conversion of implicit namespace packages into pkg-util style packages.",
64-
)
6560
parser.add_argument(
6661
"--environment",
6762
action="store",

python/private/pypi/whl_installer/wheel_installer.py

Lines changed: 1 addition & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
from pip._vendor.packaging.utils import canonicalize_name
2929

30-
from python.private.pypi.whl_installer import arguments, namespace_pkgs, wheel
30+
from python.private.pypi.whl_installer import arguments, wheel
3131

3232

3333
def _configure_reproducible_wheels() -> None:
@@ -77,35 +77,10 @@ def _parse_requirement_for_extra(
7777
return None, None
7878

7979

80-
def _setup_namespace_pkg_compatibility(wheel_dir: str) -> None:
81-
"""Converts native namespace packages to pkgutil-style packages
82-
83-
Namespace packages can be created in one of three ways. They are detailed here:
84-
https://packaging.python.org/guides/packaging-namespace-packages/#creating-a-namespace-package
85-
86-
'pkgutil-style namespace packages' (2) and 'pkg_resources-style namespace packages' (3) works in Bazel, but
87-
'native namespace packages' (1) do not.
88-
89-
We ensure compatibility with Bazel of method 1 by converting them into method 2.
90-
91-
Args:
92-
wheel_dir: the directory of the wheel to convert
93-
"""
94-
95-
namespace_pkg_dirs = namespace_pkgs.implicit_namespace_packages(
96-
wheel_dir,
97-
ignored_dirnames=["%s/bin" % wheel_dir],
98-
)
99-
100-
for ns_pkg_dir in namespace_pkg_dirs:
101-
namespace_pkgs.add_pkgutil_style_namespace_pkg_init(ns_pkg_dir)
102-
103-
10480
def _extract_wheel(
10581
wheel_file: str,
10682
extras: Dict[str, Set[str]],
10783
enable_pipstar: bool,
108-
enable_implicit_namespace_pkgs: bool,
10984
platforms: List[wheel.Platform],
11085
installation_dir: Path = Path("."),
11186
) -> None:
@@ -116,15 +91,11 @@ def _extract_wheel(
11691
installation_dir: the destination directory for installation of the wheel.
11792
extras: a list of extras to add as dependencies for the installed wheel
11893
enable_pipstar: if true, turns off certain operations.
119-
enable_implicit_namespace_pkgs: if true, disables conversion of implicit namespace packages and will unzip as-is
12094
"""
12195

12296
whl = wheel.Wheel(wheel_file)
12397
whl.unzip(installation_dir)
12498

125-
if not enable_implicit_namespace_pkgs:
126-
_setup_namespace_pkg_compatibility(installation_dir)
127-
12899
metadata = {
129100
"entry_points": [
130101
{
@@ -168,7 +139,6 @@ def main() -> None:
168139
wheel_file=whl,
169140
extras=extras,
170141
enable_pipstar=args.enable_pipstar,
171-
enable_implicit_namespace_pkgs=args.enable_implicit_namespace_pkgs,
172142
platforms=arguments.get_platforms(args),
173143
)
174144
return

python/private/pypi/whl_library.bzl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,6 @@ def _parse_optional_attrs(rctx, args, extra_pip_args = None):
173173
json.encode(struct(arg = rctx.attr.pip_data_exclude)),
174174
]
175175

176-
if rctx.attr.enable_implicit_namespace_pkgs:
177-
args.append("--enable_implicit_namespace_pkgs")
178-
179176
env = {}
180177
if rctx.attr.environment != None:
181178
for key, value in rctx.attr.environment.items():
@@ -389,6 +386,8 @@ def _whl_library_impl(rctx):
389386
metadata_name = metadata.name,
390387
metadata_version = metadata.version,
391388
requires_dist = metadata.requires_dist,
389+
# TODO @aignas 2025-05-17: maybe have a build flag for this instead
390+
enable_implicit_namespace_pkgs = rctx.attr.enable_implicit_namespace_pkgs,
392391
# TODO @aignas 2025-04-14: load through the hub:
393392
annotation = None if not rctx.attr.annotation else struct(**json.decode(rctx.read(rctx.attr.annotation))),
394393
data_exclude = rctx.attr.pip_data_exclude,
@@ -457,6 +456,8 @@ def _whl_library_impl(rctx):
457456
name = whl_path.basename,
458457
dep_template = rctx.attr.dep_template or "@{}{{name}}//:{{target}}".format(rctx.attr.repo_prefix),
459458
entry_points = entry_points,
459+
# TODO @aignas 2025-05-17: maybe have a build flag for this instead
460+
enable_implicit_namespace_pkgs = rctx.attr.enable_implicit_namespace_pkgs,
460461
# TODO @aignas 2025-04-14: load through the hub:
461462
dependencies = metadata["deps"],
462463
dependencies_by_platform = metadata["deps_by_platform"],
@@ -580,7 +581,6 @@ attr makes `extra_pip_args` and `download_only` ignored.""",
580581
Label("//python/private/pypi/whl_installer:wheel.py"),
581582
Label("//python/private/pypi/whl_installer:wheel_installer.py"),
582583
Label("//python/private/pypi/whl_installer:arguments.py"),
583-
Label("//python/private/pypi/whl_installer:namespace_pkgs.py"),
584584
] + record_files.values(),
585585
),
586586
"_rule_name": attr.string(default = "whl_library"),

python/private/pypi/whl_library_targets.bzl

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ load(
3030
"WHEEL_FILE_IMPL_LABEL",
3131
"WHEEL_FILE_PUBLIC_LABEL",
3232
)
33+
load(":namespace_pkgs.bzl", "create_inits")
3334
load(":pep508_deps.bzl", "deps")
3435

3536
def whl_library_targets_from_requires(
@@ -113,6 +114,7 @@ def whl_library_targets(
113114
copy_executables = {},
114115
entry_points = {},
115116
native = native,
117+
enable_implicit_namespace_pkgs = False,
116118
rules = struct(
117119
copy_file = copy_file,
118120
py_binary = py_binary,
@@ -153,6 +155,8 @@ def whl_library_targets(
153155
data: {type}`list[str]` A list of labels to include as part of the `data` attribute in `py_library`.
154156
entry_points: {type}`dict[str, str]` The mapping between the script
155157
name and the python file to use. DEPRECATED.
158+
enable_implicit_namespace_pkgs: {type}`boolean` generate __init__.py
159+
files for namespace pkgs.
156160
native: {type}`native` The native struct for overriding in tests.
157161
rules: {type}`struct` A struct with references to rules for creating targets.
158162
"""
@@ -293,6 +297,14 @@ def whl_library_targets(
293297
)
294298

295299
if hasattr(rules, "py_library"):
300+
srcs = native.glob(
301+
["site-packages/**/*.py"],
302+
exclude = srcs_exclude,
303+
# Empty sources are allowed to support wheels that don't have any
304+
# pure-Python code, e.g. pymssql, which is written in Cython.
305+
allow_empty = True,
306+
)
307+
296308
# NOTE: pyi files should probably be excluded because they're carried
297309
# by the pyi_srcs attribute. However, historical behavior included
298310
# them in data and some tools currently rely on that.
@@ -309,23 +321,31 @@ def whl_library_targets(
309321
if item not in _data_exclude:
310322
_data_exclude.append(item)
311323

324+
data = data + native.glob(
325+
["site-packages/**/*"],
326+
exclude = _data_exclude,
327+
)
328+
329+
pyi_srcs = native.glob(
330+
["site-packages/**/*.pyi"],
331+
allow_empty = True,
332+
)
333+
334+
if enable_implicit_namespace_pkgs:
335+
srcs = srcs + getattr(native, "select", select)({
336+
Label("//python/config_settings:is_venvs_site_packages"): [],
337+
"//conditions:default": create_inits(
338+
srcs = srcs + data + pyi_srcs,
339+
ignore_dirnames = [], # If you need to ignore certain folders, you can patch rules_python here to do so.
340+
root = "site-packages",
341+
),
342+
})
343+
312344
rules.py_library(
313345
name = py_library_label,
314-
srcs = native.glob(
315-
["site-packages/**/*.py"],
316-
exclude = srcs_exclude,
317-
# Empty sources are allowed to support wheels that don't have any
318-
# pure-Python code, e.g. pymssql, which is written in Cython.
319-
allow_empty = True,
320-
),
321-
pyi_srcs = native.glob(
322-
["site-packages/**/*.pyi"],
323-
allow_empty = True,
324-
),
325-
data = data + native.glob(
326-
["site-packages/**/*"],
327-
exclude = _data_exclude,
328-
),
346+
srcs = srcs,
347+
pyi_srcs = pyi_srcs,
348+
data = data,
329349
# This makes this directory a top-level in the python import
330350
# search path for anything that depends on this.
331351
imports = ["site-packages"],

tests/pypi/namespace_pkgs/BUILD.bazel

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
load(":namespace_pkgs_tests.bzl", "namespace_pkgs_test_suite")
2+
3+
namespace_pkgs_test_suite(
4+
name = "namespace_pkgs_tests",
5+
)

0 commit comments

Comments
 (0)