refactor(whl_library): move bazel file generation to Starlark (#1336)
Before this PR, the `wheel_installer` was doing three things:
1. Downloading the right wheel.
2. Extracting it into the output directory.
3. Generating BUILD.bazel files based on the extracted contents.
This PR is moving the third part into the `whl_library` repository rule
and it has the following benefits:
* We can reduce code duplication and label sanitization functions in
rules_python.
* There are many things that the `wheel_installer` does not care anymore
and we don't need to change less code when extending `whl_library` as
we can now do many things in starlark directly.
* It becomes easier to change the API of how we expose the generated
BUILD.bazel patching because we only need to change the Starlark
functions.
Work towards #1330.
diff --git a/python/pip_install/BUILD.bazel b/python/pip_install/BUILD.bazel
index e8e8633..179fd62 100644
--- a/python/pip_install/BUILD.bazel
+++ b/python/pip_install/BUILD.bazel
@@ -4,7 +4,6 @@
"BUILD.bazel",
"//python/pip_install/private:distribution",
"//python/pip_install/tools/dependency_resolver:distribution",
- "//python/pip_install/tools/lib:distribution",
"//python/pip_install/tools/wheel_installer:distribution",
],
visibility = ["//:__pkg__"],
@@ -22,7 +21,6 @@
name = "py_srcs",
srcs = [
"//python/pip_install/tools/dependency_resolver:py_srcs",
- "//python/pip_install/tools/lib:py_srcs",
"//python/pip_install/tools/wheel_installer:py_srcs",
],
visibility = ["//python/pip_install/private:__pkg__"],
diff --git a/python/pip_install/pip_repository.bzl b/python/pip_install/pip_repository.bzl
index 99d1fb0..1f392ee 100644
--- a/python/pip_install/pip_repository.bzl
+++ b/python/pip_install/pip_repository.bzl
@@ -18,6 +18,7 @@
load("//python:versions.bzl", "WINDOWS_NAME")
load("//python/pip_install:repositories.bzl", "all_requirements")
load("//python/pip_install:requirements_parser.bzl", parse_requirements = "parse")
+load("//python/pip_install/private:generate_whl_library_build_bazel.bzl", "generate_whl_library_build_bazel")
load("//python/pip_install/private:srcs.bzl", "PIP_INSTALL_PY_SRCS")
load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED")
load("//python/private:normalize_name.bzl", "normalize_name")
@@ -27,6 +28,8 @@
COMMAND_LINE_TOOLS_PATH_SLUG = "commandlinetools"
+_WHEEL_ENTRY_POINT_PREFIX = "rules_python_wheel_entry_point"
+
def _construct_pypath(rctx):
"""Helper function to construct a PYTHONPATH.
@@ -663,16 +666,7 @@
"python.pip_install.tools.wheel_installer.wheel_installer",
"--requirement",
rctx.attr.requirement,
- "--repo",
- rctx.attr.repo,
- "--repo-prefix",
- rctx.attr.repo_prefix,
]
- if rctx.attr.annotation:
- args.extend([
- "--annotation",
- rctx.path(rctx.attr.annotation),
- ])
args = _parse_optional_attrs(rctx, args)
@@ -687,8 +681,72 @@
if result.return_code:
fail("whl_library %s failed: %s (%s) error code: '%s'" % (rctx.attr.name, result.stdout, result.stderr, result.return_code))
+ metadata = json.decode(rctx.read("metadata.json"))
+ rctx.delete("metadata.json")
+
+ entry_points = {}
+ for item in metadata["entry_points"]:
+ name = item["name"]
+ module = item["module"]
+ attribute = item["attribute"]
+
+ # There is an extreme edge-case with entry_points that end with `.py`
+ # See: https://github.com/bazelbuild/bazel/blob/09c621e4cf5b968f4c6cdf905ab142d5961f9ddc/src/test/java/com/google/devtools/build/lib/rules/python/PyBinaryConfiguredTargetTest.java#L174
+ entry_point_without_py = name[:-3] + "_py" if name.endswith(".py") else name
+ entry_point_target_name = (
+ _WHEEL_ENTRY_POINT_PREFIX + "_" + entry_point_without_py
+ )
+ entry_point_script_name = entry_point_target_name + ".py"
+
+ rctx.file(
+ entry_point_script_name,
+ _generate_entry_point_contents(module, attribute),
+ )
+ entry_points[entry_point_without_py] = entry_point_script_name
+
+ build_file_contents = generate_whl_library_build_bazel(
+ repo_prefix = rctx.attr.repo_prefix,
+ dependencies = metadata["deps"],
+ data_exclude = rctx.attr.pip_data_exclude,
+ tags = [
+ "pypi_name=" + metadata["name"],
+ "pypi_version=" + metadata["version"],
+ ],
+ entry_points = entry_points,
+ annotation = None if not rctx.attr.annotation else struct(**json.decode(rctx.read(rctx.attr.annotation))),
+ )
+ rctx.file("BUILD.bazel", build_file_contents)
+
return
+def _generate_entry_point_contents(
+ module,
+ attribute,
+ shebang = "#!/usr/bin/env python3"):
+ """Generate the contents of an entry point script.
+
+ Args:
+ module (str): The name of the module to use.
+ attribute (str): The name of the attribute to call.
+ shebang (str, optional): The shebang to use for the entry point python
+ file.
+
+ Returns:
+ str: A string of python code.
+ """
+ contents = """\
+{shebang}
+import sys
+from {module} import {attribute}
+if __name__ == "__main__":
+ sys.exit({attribute}())
+""".format(
+ shebang = shebang,
+ module = module,
+ attribute = attribute,
+ )
+ return contents
+
whl_library_attrs = {
"annotation": attr.label(
doc = (
diff --git a/python/pip_install/private/generate_whl_library_build_bazel.bzl b/python/pip_install/private/generate_whl_library_build_bazel.bzl
new file mode 100644
index 0000000..229a917
--- /dev/null
+++ b/python/pip_install/private/generate_whl_library_build_bazel.bzl
@@ -0,0 +1,224 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Generate the BUILD.bazel contents for a repo defined by a whl_library."""
+
+load("//python/private:normalize_name.bzl", "normalize_name")
+
+_WHEEL_FILE_LABEL = "whl"
+_PY_LIBRARY_LABEL = "pkg"
+_DATA_LABEL = "data"
+_DIST_INFO_LABEL = "dist_info"
+_WHEEL_ENTRY_POINT_PREFIX = "rules_python_wheel_entry_point"
+
+_COPY_FILE_TEMPLATE = """\
+copy_file(
+ name = "{dest}.copy",
+ src = "{src}",
+ out = "{dest}",
+ is_executable = {is_executable},
+)
+"""
+
+_ENTRY_POINT_RULE_TEMPLATE = """\
+py_binary(
+ name = "{name}",
+ srcs = ["{src}"],
+ # This makes this directory a top-level in the python import
+ # search path for anything that depends on this.
+ imports = ["."],
+ deps = ["{pkg}"],
+)
+"""
+
+_BUILD_TEMPLATE = """\
+load("@rules_python//python:defs.bzl", "py_library", "py_binary")
+load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
+
+package(default_visibility = ["//visibility:public"])
+
+filegroup(
+ name = "{dist_info_label}",
+ srcs = glob(["site-packages/*.dist-info/**"], allow_empty = True),
+)
+
+filegroup(
+ name = "{data_label}",
+ srcs = glob(["data/**"], allow_empty = True),
+)
+
+filegroup(
+ name = "{whl_file_label}",
+ srcs = glob(["*.whl"], allow_empty = True),
+ data = {whl_file_deps},
+)
+
+py_library(
+ name = "{name}",
+ srcs = glob(
+ ["site-packages/**/*.py"],
+ exclude={srcs_exclude},
+ # Empty sources are allowed to support wheels that don't have any
+ # pure-Python code, e.g. pymssql, which is written in Cython.
+ allow_empty = True,
+ ),
+ data = {data} + glob(
+ ["site-packages/**/*"],
+ exclude={data_exclude},
+ ),
+ # This makes this directory a top-level in the python import
+ # search path for anything that depends on this.
+ imports = ["site-packages"],
+ deps = {dependencies},
+ tags = {tags},
+)
+"""
+
+def generate_whl_library_build_bazel(
+ repo_prefix,
+ dependencies,
+ data_exclude,
+ tags,
+ entry_points,
+ annotation = None):
+ """Generate a BUILD file for an unzipped Wheel
+
+ Args:
+ repo_prefix: the repo prefix that should be used for dependency lists.
+ dependencies: a list of PyPI packages that are dependencies to the py_library.
+ data_exclude: more patterns to exclude from the data attribute of generated py_library rules.
+ tags: list of tags to apply to generated py_library rules.
+ entry_points: A dict of entry points to add py_binary rules for.
+ annotation: The annotation for the build file.
+
+ Returns:
+ A complete BUILD file as a string
+ """
+
+ additional_content = []
+ data = []
+ srcs_exclude = []
+ data_exclude = [] + data_exclude
+ dependencies = sorted(dependencies)
+ tags = sorted(tags)
+
+ for entry_point, entry_point_script_name in entry_points.items():
+ additional_content.append(
+ _generate_entry_point_rule(
+ name = "{}_{}".format(_WHEEL_ENTRY_POINT_PREFIX, entry_point),
+ script = entry_point_script_name,
+ pkg = ":" + _PY_LIBRARY_LABEL,
+ ),
+ )
+
+ if annotation:
+ for src, dest in annotation.copy_files.items():
+ data.append(dest)
+ additional_content.append(_generate_copy_commands(src, dest))
+ for src, dest in annotation.copy_executables.items():
+ data.append(dest)
+ additional_content.append(
+ _generate_copy_commands(src, dest, is_executable = True),
+ )
+ data.extend(annotation.data)
+ data_exclude.extend(annotation.data_exclude_glob)
+ srcs_exclude.extend(annotation.srcs_exclude_glob)
+ if annotation.additive_build_content:
+ additional_content.append(annotation.additive_build_content)
+
+ _data_exclude = [
+ "**/* *",
+ "**/*.py",
+ "**/*.pyc",
+ "**/*.pyc.*", # During pyc creation, temp files named *.pyc.NNNN are created
+ # RECORD is known to contain sha256 checksums of files which might include the checksums
+ # of generated files produced when wheels are installed. The file is ignored to avoid
+ # Bazel caching issues.
+ "**/*.dist-info/RECORD",
+ ]
+ for item in data_exclude:
+ if item not in _data_exclude:
+ _data_exclude.append(item)
+
+ lib_dependencies = [
+ "@" + repo_prefix + normalize_name(d) + "//:" + _PY_LIBRARY_LABEL
+ for d in dependencies
+ ]
+ whl_file_deps = [
+ "@" + repo_prefix + normalize_name(d) + "//:" + _WHEEL_FILE_LABEL
+ for d in dependencies
+ ]
+
+ contents = "\n".join(
+ [
+ _BUILD_TEMPLATE.format(
+ name = _PY_LIBRARY_LABEL,
+ dependencies = repr(lib_dependencies),
+ data_exclude = repr(_data_exclude),
+ whl_file_label = _WHEEL_FILE_LABEL,
+ whl_file_deps = repr(whl_file_deps),
+ tags = repr(tags),
+ data_label = _DATA_LABEL,
+ dist_info_label = _DIST_INFO_LABEL,
+ entry_point_prefix = _WHEEL_ENTRY_POINT_PREFIX,
+ srcs_exclude = repr(srcs_exclude),
+ data = repr(data),
+ ),
+ ] + additional_content,
+ )
+
+ # NOTE: Ensure that we terminate with a new line
+ return contents.rstrip() + "\n"
+
+def _generate_copy_commands(src, dest, is_executable = False):
+ """Generate a [@bazel_skylib//rules:copy_file.bzl%copy_file][cf] target
+
+ [cf]: https://github.com/bazelbuild/bazel-skylib/blob/1.1.1/docs/copy_file_doc.md
+
+ Args:
+ src (str): The label for the `src` attribute of [copy_file][cf]
+ dest (str): The label for the `out` attribute of [copy_file][cf]
+ is_executable (bool, optional): Whether or not the file being copied is executable.
+ sets `is_executable` for [copy_file][cf]
+
+ Returns:
+ str: A `copy_file` instantiation.
+ """
+ return _COPY_FILE_TEMPLATE.format(
+ src = src,
+ dest = dest,
+ is_executable = is_executable,
+ )
+
+def _generate_entry_point_rule(*, name, script, pkg):
+ """Generate a Bazel `py_binary` rule for an entry point script.
+
+ Note that the script is used to determine the name of the target. The name of
+ entry point targets should be uniuqe to avoid conflicts with existing sources or
+ directories within a wheel.
+
+ Args:
+ name (str): The name of the generated py_binary.
+ script (str): The path to the entry point's python file.
+ pkg (str): The package owning the entry point. This is expected to
+ match up with the `py_library` defined for each repository.
+
+ Returns:
+ str: A `py_binary` instantiation.
+ """
+ return _ENTRY_POINT_RULE_TEMPLATE.format(
+ name = name,
+ src = script.replace("\\", "/"),
+ pkg = pkg,
+ )
diff --git a/python/pip_install/private/srcs.bzl b/python/pip_install/private/srcs.bzl
index f3064a3..e342d90 100644
--- a/python/pip_install/private/srcs.bzl
+++ b/python/pip_install/private/srcs.bzl
@@ -9,10 +9,7 @@
PIP_INSTALL_PY_SRCS = [
"@rules_python//python/pip_install/tools/dependency_resolver:__init__.py",
"@rules_python//python/pip_install/tools/dependency_resolver:dependency_resolver.py",
- "@rules_python//python/pip_install/tools/lib:__init__.py",
- "@rules_python//python/pip_install/tools/lib:annotation.py",
- "@rules_python//python/pip_install/tools/lib:arguments.py",
- "@rules_python//python/pip_install/tools/lib:bazel.py",
+ "@rules_python//python/pip_install/tools/wheel_installer:arguments.py",
"@rules_python//python/pip_install/tools/wheel_installer:namespace_pkgs.py",
"@rules_python//python/pip_install/tools/wheel_installer:wheel.py",
"@rules_python//python/pip_install/tools/wheel_installer:wheel_installer.py",
diff --git a/python/pip_install/tools/lib/BUILD.bazel b/python/pip_install/tools/lib/BUILD.bazel
deleted file mode 100644
index 37a8b09..0000000
--- a/python/pip_install/tools/lib/BUILD.bazel
+++ /dev/null
@@ -1,82 +0,0 @@
-load("//python:defs.bzl", "py_library", "py_test")
-load(":annotations_test_helpers.bzl", "package_annotation", "package_annotations_file")
-
-py_library(
- name = "lib",
- srcs = [
- "annotation.py",
- "arguments.py",
- "bazel.py",
- ],
- visibility = ["//python/pip_install:__subpackages__"],
-)
-
-package_annotations_file(
- name = "mock_annotations",
- annotations = {
- "pkg_a": package_annotation(),
- "pkg_b": package_annotation(
- data_exclude_glob = [
- "*.foo",
- "*.bar",
- ],
- ),
- "pkg_c": package_annotation(
- # The `join` and `strip` here accounts for potential differences
- # in new lines between unix and windows hosts.
- additive_build_content = "\n".join([line.strip() for line in """\
-cc_library(
- name = "my_target",
- hdrs = glob(["**/*.h"]),
- srcs = glob(["**/*.cc"]),
-)
-""".splitlines()]),
- data = [":my_target"],
- ),
- "pkg_d": package_annotation(
- srcs_exclude_glob = ["pkg_d/tests/**"],
- ),
- },
- tags = ["manual"],
-)
-
-py_test(
- name = "annotations_test",
- size = "small",
- srcs = ["annotations_test.py"],
- data = [":mock_annotations"],
- env = {"MOCK_ANNOTATIONS": "$(rootpath :mock_annotations)"},
- deps = [
- ":lib",
- "//python/runfiles",
- ],
-)
-
-py_test(
- name = "arguments_test",
- size = "small",
- srcs = [
- "arguments_test.py",
- ],
- deps = [
- ":lib",
- ],
-)
-
-filegroup(
- name = "distribution",
- srcs = glob(
- ["*"],
- exclude = ["*_test.py"],
- ),
- visibility = ["//python/pip_install:__subpackages__"],
-)
-
-filegroup(
- name = "py_srcs",
- srcs = glob(
- include = ["**/*.py"],
- exclude = ["**/*_test.py"],
- ),
- visibility = ["//python/pip_install:__subpackages__"],
-)
diff --git a/python/pip_install/tools/lib/__init__.py b/python/pip_install/tools/lib/__init__.py
deleted file mode 100644
index bbdfb4c..0000000
--- a/python/pip_install/tools/lib/__init__.py
+++ /dev/null
@@ -1,14 +0,0 @@
-# Copyright 2023 The Bazel Authors. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
diff --git a/python/pip_install/tools/lib/annotation.py b/python/pip_install/tools/lib/annotation.py
deleted file mode 100644
index c980080..0000000
--- a/python/pip_install/tools/lib/annotation.py
+++ /dev/null
@@ -1,129 +0,0 @@
-# Copyright 2023 The Bazel Authors. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import json
-import logging
-from collections import OrderedDict
-from pathlib import Path
-from typing import Any, Dict, List
-
-
-class Annotation(OrderedDict):
- """A python representation of `@rules_python//python:pip.bzl%package_annotation`"""
-
- def __init__(self, content: Dict[str, Any]) -> None:
-
- missing = []
- ordered_content = OrderedDict()
- for field in (
- "additive_build_content",
- "copy_executables",
- "copy_files",
- "data",
- "data_exclude_glob",
- "srcs_exclude_glob",
- ):
- if field not in content:
- missing.append(field)
- continue
- ordered_content.update({field: content.pop(field)})
-
- if missing:
- raise ValueError("Data missing from initial annotation: {}".format(missing))
-
- if content:
- raise ValueError(
- "Unexpected data passed to annotations: {}".format(
- sorted(list(content.keys()))
- )
- )
-
- return OrderedDict.__init__(self, ordered_content)
-
- @property
- def additive_build_content(self) -> str:
- return self["additive_build_content"]
-
- @property
- def copy_executables(self) -> Dict[str, str]:
- return self["copy_executables"]
-
- @property
- def copy_files(self) -> Dict[str, str]:
- return self["copy_files"]
-
- @property
- def data(self) -> List[str]:
- return self["data"]
-
- @property
- def data_exclude_glob(self) -> List[str]:
- return self["data_exclude_glob"]
-
- @property
- def srcs_exclude_glob(self) -> List[str]:
- return self["srcs_exclude_glob"]
-
-
-class AnnotationsMap:
- """A mapping of python package names to [Annotation]"""
-
- def __init__(self, json_file: Path):
- content = json.loads(json_file.read_text())
-
- self._annotations = {pkg: Annotation(data) for (pkg, data) in content.items()}
-
- @property
- def annotations(self) -> Dict[str, Annotation]:
- return self._annotations
-
- def collect(self, requirements: List[str]) -> Dict[str, Annotation]:
- unused = self.annotations
- collection = {}
- for pkg in requirements:
- if pkg in unused:
- collection.update({pkg: unused.pop(pkg)})
-
- if unused:
- logging.warning(
- "Unused annotations: {}".format(sorted(list(unused.keys())))
- )
-
- return collection
-
-
-def annotation_from_str_path(path: str) -> Annotation:
- """Load an annotation from a json encoded file
-
- Args:
- path (str): The path to a json encoded file
-
- Returns:
- Annotation: The deserialized annotations
- """
- json_file = Path(path)
- content = json.loads(json_file.read_text())
- return Annotation(content)
-
-
-def annotations_map_from_str_path(path: str) -> AnnotationsMap:
- """Load an annotations map from a json encoded file
-
- Args:
- path (str): The path to a json encoded file
-
- Returns:
- AnnotationsMap: The deserialized annotations map
- """
- return AnnotationsMap(Path(path))
diff --git a/python/pip_install/tools/lib/annotations_test.py b/python/pip_install/tools/lib/annotations_test.py
deleted file mode 100644
index f7c360f..0000000
--- a/python/pip_install/tools/lib/annotations_test.py
+++ /dev/null
@@ -1,121 +0,0 @@
-#!/usr/bin/env python3
-# Copyright 2023 The Bazel Authors. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-
-import os
-import textwrap
-import unittest
-from pathlib import Path
-
-from python.pip_install.tools.lib.annotation import Annotation, AnnotationsMap
-from python.runfiles import runfiles
-
-
-class AnnotationsTestCase(unittest.TestCase):
-
- maxDiff = None
-
- def test_annotations_constructor(self) -> None:
- annotations_env = os.environ.get("MOCK_ANNOTATIONS")
- self.assertIsNotNone(annotations_env)
-
- r = runfiles.Create()
-
- annotations_path = Path(r.Rlocation("rules_python/{}".format(annotations_env)))
- self.assertTrue(annotations_path.exists())
-
- annotations_map = AnnotationsMap(annotations_path)
- self.assertListEqual(
- list(annotations_map.annotations.keys()),
- ["pkg_a", "pkg_b", "pkg_c", "pkg_d"],
- )
-
- collection = annotations_map.collect(["pkg_a", "pkg_b", "pkg_c", "pkg_d"])
-
- self.assertEqual(
- collection["pkg_a"],
- Annotation(
- {
- "additive_build_content": None,
- "copy_executables": {},
- "copy_files": {},
- "data": [],
- "data_exclude_glob": [],
- "srcs_exclude_glob": [],
- }
- ),
- )
-
- self.assertEqual(
- collection["pkg_b"],
- Annotation(
- {
- "additive_build_content": None,
- "copy_executables": {},
- "copy_files": {},
- "data": [],
- "data_exclude_glob": ["*.foo", "*.bar"],
- "srcs_exclude_glob": [],
- }
- ),
- )
-
- self.assertEqual(
- collection["pkg_c"],
- Annotation(
- {
- # The `join` and `strip` here accounts for potential
- # differences in new lines between unix and windows
- # hosts.
- "additive_build_content": "\n".join(
- [
- line.strip()
- for line in textwrap.dedent(
- """\
- cc_library(
- name = "my_target",
- hdrs = glob(["**/*.h"]),
- srcs = glob(["**/*.cc"]),
- )
- """
- ).splitlines()
- ]
- ),
- "copy_executables": {},
- "copy_files": {},
- "data": [":my_target"],
- "data_exclude_glob": [],
- "srcs_exclude_glob": [],
- }
- ),
- )
-
- self.assertEqual(
- collection["pkg_d"],
- Annotation(
- {
- "additive_build_content": None,
- "copy_executables": {},
- "copy_files": {},
- "data": [],
- "data_exclude_glob": [],
- "srcs_exclude_glob": ["pkg_d/tests/**"],
- }
- ),
- )
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/python/pip_install/tools/lib/annotations_test_helpers.bzl b/python/pip_install/tools/lib/annotations_test_helpers.bzl
deleted file mode 100644
index 4f56bb7..0000000
--- a/python/pip_install/tools/lib/annotations_test_helpers.bzl
+++ /dev/null
@@ -1,47 +0,0 @@
-# Copyright 2023 The Bazel Authors. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-"""Helper macros and rules for testing the `annotations` module of `tools`"""
-
-load("//python:pip.bzl", _package_annotation = "package_annotation")
-
-package_annotation = _package_annotation
-
-def _package_annotations_file_impl(ctx):
- output = ctx.actions.declare_file(ctx.label.name + ".annotations.json")
-
- annotations = {package: json.decode(data) for (package, data) in ctx.attr.annotations.items()}
- ctx.actions.write(
- output = output,
- content = json.encode_indent(annotations, indent = " " * 4),
- )
-
- return DefaultInfo(
- files = depset([output]),
- runfiles = ctx.runfiles(files = [output]),
- )
-
-package_annotations_file = rule(
- implementation = _package_annotations_file_impl,
- doc = (
- "Consumes `package_annotation` definitions in the same way " +
- "`pip_repository` rules do to produce an annotations file."
- ),
- attrs = {
- "annotations": attr.string_dict(
- doc = "See `@rules_python//python:pip.bzl%package_annotation",
- mandatory = True,
- ),
- },
-)
diff --git a/python/pip_install/tools/lib/bazel.py b/python/pip_install/tools/lib/bazel.py
deleted file mode 100644
index 81119e9..0000000
--- a/python/pip_install/tools/lib/bazel.py
+++ /dev/null
@@ -1,45 +0,0 @@
-# Copyright 2023 The Bazel Authors. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import re
-
-WHEEL_FILE_LABEL = "whl"
-PY_LIBRARY_LABEL = "pkg"
-DATA_LABEL = "data"
-DIST_INFO_LABEL = "dist_info"
-WHEEL_ENTRY_POINT_PREFIX = "rules_python_wheel_entry_point"
-
-
-def sanitise_name(name: str, prefix: str) -> str:
- """Sanitises the name to be compatible with Bazel labels.
-
- See the doc in ../../../private/normalize_name.bzl.
- """
- return prefix + re.sub(r"[-_.]+", "_", name).lower()
-
-
-def _whl_name_to_repo_root(whl_name: str, repo_prefix: str) -> str:
- return "@{}//".format(sanitise_name(whl_name, prefix=repo_prefix))
-
-
-def sanitised_repo_library_label(whl_name: str, repo_prefix: str) -> str:
- return '"{}:{}"'.format(
- _whl_name_to_repo_root(whl_name, repo_prefix), PY_LIBRARY_LABEL
- )
-
-
-def sanitised_repo_file_label(whl_name: str, repo_prefix: str) -> str:
- return '"{}:{}"'.format(
- _whl_name_to_repo_root(whl_name, repo_prefix), WHEEL_FILE_LABEL
- )
diff --git a/python/pip_install/tools/wheel_installer/BUILD.bazel b/python/pip_install/tools/wheel_installer/BUILD.bazel
index 54bbc46..6360ca5 100644
--- a/python/pip_install/tools/wheel_installer/BUILD.bazel
+++ b/python/pip_install/tools/wheel_installer/BUILD.bazel
@@ -4,12 +4,12 @@
py_library(
name = "lib",
srcs = [
+ "arguments.py",
"namespace_pkgs.py",
"wheel.py",
"wheel_installer.py",
],
deps = [
- "//python/pip_install/tools/lib",
requirement("installer"),
requirement("pip"),
requirement("setuptools"),
@@ -25,6 +25,17 @@
)
py_test(
+ name = "arguments_test",
+ size = "small",
+ srcs = [
+ "arguments_test.py",
+ ],
+ deps = [
+ ":lib",
+ ],
+)
+
+py_test(
name = "namespace_pkgs_test",
size = "small",
srcs = [
diff --git a/python/pip_install/tools/lib/arguments.py b/python/pip_install/tools/wheel_installer/arguments.py
similarity index 87%
rename from python/pip_install/tools/lib/arguments.py
rename to python/pip_install/tools/wheel_installer/arguments.py
index 974f03c..aac3c01 100644
--- a/python/pip_install/tools/lib/arguments.py
+++ b/python/pip_install/tools/wheel_installer/arguments.py
@@ -12,16 +12,21 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import argparse
import json
-from argparse import ArgumentParser
+from typing import Any
-def parse_common_args(parser: ArgumentParser) -> ArgumentParser:
+def parser(**kwargs: Any) -> argparse.ArgumentParser:
+ """Create a parser for the wheel_installer tool."""
+ parser = argparse.ArgumentParser(
+ **kwargs,
+ )
parser.add_argument(
- "--repo",
+ "--requirement",
action="store",
required=True,
- help="The external repo name to install dependencies. In the format '@{REPO_NAME}'",
+ help="A single PEP508 requirement specifier string.",
)
parser.add_argument(
"--isolated",
@@ -49,11 +54,6 @@
help="Extra environment variables to set on the pip environment.",
)
parser.add_argument(
- "--repo-prefix",
- required=True,
- help="Prefix to prepend to packages",
- )
- parser.add_argument(
"--download_only",
action="store_true",
help="Use 'pip download' instead of 'pip wheel'. Disables building wheels from source, but allows use of "
diff --git a/python/pip_install/tools/lib/arguments_test.py b/python/pip_install/tools/wheel_installer/arguments_test.py
similarity index 81%
rename from python/pip_install/tools/lib/arguments_test.py
rename to python/pip_install/tools/wheel_installer/arguments_test.py
index dfa96a8..7193f4a 100644
--- a/python/pip_install/tools/lib/arguments_test.py
+++ b/python/pip_install/tools/wheel_installer/arguments_test.py
@@ -16,35 +16,30 @@
import json
import unittest
-from python.pip_install.tools.lib import arguments
+from python.pip_install.tools.wheel_installer import arguments
class ArgumentsTestCase(unittest.TestCase):
def test_arguments(self) -> None:
- parser = argparse.ArgumentParser()
- parser = arguments.parse_common_args(parser)
+ parser = arguments.parser()
repo_name = "foo"
repo_prefix = "pypi_"
index_url = "--index_url=pypi.org/simple"
extra_pip_args = [index_url]
+ requirement = "foo==1.0.0 --hash=sha256:deadbeef"
args_dict = vars(
parser.parse_args(
args=[
- "--repo",
- repo_name,
+ f'--requirement="{requirement}"',
f"--extra_pip_args={json.dumps({'arg': extra_pip_args})}",
- "--repo-prefix",
- repo_prefix,
]
)
)
args_dict = arguments.deserialize_structured_args(args_dict)
- self.assertIn("repo", args_dict)
+ self.assertIn("requirement", args_dict)
self.assertIn("extra_pip_args", args_dict)
self.assertEqual(args_dict["pip_data_exclude"], [])
self.assertEqual(args_dict["enable_implicit_namespace_pkgs"], False)
- self.assertEqual(args_dict["repo"], repo_name)
- self.assertEqual(args_dict["repo_prefix"], repo_prefix)
self.assertEqual(args_dict["extra_pip_args"], extra_pip_args)
def test_deserialize_structured_args(self) -> None:
diff --git a/python/pip_install/tools/wheel_installer/wheel_installer.py b/python/pip_install/tools/wheel_installer/wheel_installer.py
index 9b363c3..c6c2961 100644
--- a/python/pip_install/tools/wheel_installer/wheel_installer.py
+++ b/python/pip_install/tools/wheel_installer/wheel_installer.py
@@ -12,6 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+"""Build and/or fetch a single wheel based on the requirement passed in"""
+
import argparse
import errno
import glob
@@ -28,8 +30,7 @@
from pip._vendor.packaging.utils import canonicalize_name
-from python.pip_install.tools.lib import annotation, arguments, bazel
-from python.pip_install.tools.wheel_installer import namespace_pkgs, wheel
+from python.pip_install.tools.wheel_installer import arguments, namespace_pkgs, wheel
def _configure_reproducible_wheels() -> None:
@@ -103,201 +104,11 @@
namespace_pkgs.add_pkgutil_style_namespace_pkg_init(ns_pkg_dir)
-def _generate_entry_point_contents(
- module: str, attribute: str, shebang: str = "#!/usr/bin/env python3"
-) -> str:
- """Generate the contents of an entry point script.
-
- Args:
- module (str): The name of the module to use.
- attribute (str): The name of the attribute to call.
- shebang (str, optional): The shebang to use for the entry point python
- file.
-
- Returns:
- str: A string of python code.
- """
- return textwrap.dedent(
- """\
- {shebang}
- import sys
- from {module} import {attribute}
- if __name__ == "__main__":
- sys.exit({attribute}())
- """.format(
- shebang=shebang, module=module, attribute=attribute
- )
- )
-
-
-def _generate_entry_point_rule(name: str, script: str, pkg: str) -> str:
- """Generate a Bazel `py_binary` rule for an entry point script.
-
- Note that the script is used to determine the name of the target. The name of
- entry point targets should be uniuqe to avoid conflicts with existing sources or
- directories within a wheel.
-
- Args:
- name (str): The name of the generated py_binary.
- script (str): The path to the entry point's python file.
- pkg (str): The package owning the entry point. This is expected to
- match up with the `py_library` defined for each repository.
-
-
- Returns:
- str: A `py_binary` instantiation.
- """
- return textwrap.dedent(
- """\
- py_binary(
- name = "{name}",
- srcs = ["{src}"],
- # This makes this directory a top-level in the python import
- # search path for anything that depends on this.
- imports = ["."],
- deps = ["{pkg}"],
- )
- """.format(
- name=name, src=str(script).replace("\\", "/"), pkg=pkg
- )
- )
-
-
-def _generate_copy_commands(src, dest, is_executable=False) -> str:
- """Generate a [@bazel_skylib//rules:copy_file.bzl%copy_file][cf] target
-
- [cf]: https://github.com/bazelbuild/bazel-skylib/blob/1.1.1/docs/copy_file_doc.md
-
- Args:
- src (str): The label for the `src` attribute of [copy_file][cf]
- dest (str): The label for the `out` attribute of [copy_file][cf]
- is_executable (bool, optional): Whether or not the file being copied is executable.
- sets `is_executable` for [copy_file][cf]
-
- Returns:
- str: A `copy_file` instantiation.
- """
- return textwrap.dedent(
- """\
- copy_file(
- name = "{dest}.copy",
- src = "{src}",
- out = "{dest}",
- is_executable = {is_executable},
- )
- """.format(
- src=src,
- dest=dest,
- is_executable=is_executable,
- )
- )
-
-
-def _generate_build_file_contents(
- name: str,
- dependencies: List[str],
- whl_file_deps: List[str],
- data_exclude: List[str],
- tags: List[str],
- srcs_exclude: List[str] = [],
- data: List[str] = [],
- additional_content: List[str] = [],
-) -> str:
- """Generate a BUILD file for an unzipped Wheel
-
- Args:
- name: the target name of the py_library
- dependencies: a list of Bazel labels pointing to dependencies of the library
- whl_file_deps: a list of Bazel labels pointing to wheel file dependencies of this wheel.
- data_exclude: more patterns to exclude from the data attribute of generated py_library rules.
- tags: list of tags to apply to generated py_library rules.
- additional_content: A list of additional content to append to the BUILD file.
-
- Returns:
- A complete BUILD file as a string
-
- We allow for empty Python sources as for Wheels containing only compiled C code
- there may be no Python sources whatsoever (e.g. packages written in Cython: like `pymssql`).
- """
-
- data_exclude = list(
- set(
- [
- "**/* *",
- "**/*.py",
- "**/*.pyc",
- "**/*.pyc.*", # During pyc creation, temp files named *.pyc.NNNN are created
- # RECORD is known to contain sha256 checksums of files which might include the checksums
- # of generated files produced when wheels are installed. The file is ignored to avoid
- # Bazel caching issues.
- "**/*.dist-info/RECORD",
- ]
- + data_exclude
- )
- )
-
- return "\n".join(
- [
- textwrap.dedent(
- """\
- load("@rules_python//python:defs.bzl", "py_library", "py_binary")
- load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
-
- package(default_visibility = ["//visibility:public"])
-
- filegroup(
- name = "{dist_info_label}",
- srcs = glob(["site-packages/*.dist-info/**"], allow_empty = True),
- )
-
- filegroup(
- name = "{data_label}",
- srcs = glob(["data/**"], allow_empty = True),
- )
-
- filegroup(
- name = "{whl_file_label}",
- srcs = glob(["*.whl"], allow_empty = True),
- data = [{whl_file_deps}],
- )
-
- py_library(
- name = "{name}",
- srcs = glob(["site-packages/**/*.py"], exclude={srcs_exclude}, allow_empty = True),
- data = {data} + glob(["site-packages/**/*"], exclude={data_exclude}),
- # This makes this directory a top-level in the python import
- # search path for anything that depends on this.
- imports = ["site-packages"],
- deps = [{dependencies}],
- tags = [{tags}],
- )
- """.format(
- name=name,
- dependencies=",".join(sorted(dependencies)),
- data_exclude=json.dumps(sorted(data_exclude)),
- whl_file_label=bazel.WHEEL_FILE_LABEL,
- whl_file_deps=",".join(sorted(whl_file_deps)),
- tags=",".join(sorted(['"%s"' % t for t in tags])),
- data_label=bazel.DATA_LABEL,
- dist_info_label=bazel.DIST_INFO_LABEL,
- entry_point_prefix=bazel.WHEEL_ENTRY_POINT_PREFIX,
- srcs_exclude=json.dumps(sorted(srcs_exclude)),
- data=json.dumps(sorted(data)),
- )
- )
- ]
- + additional_content
- )
-
-
def _extract_wheel(
wheel_file: str,
extras: Dict[str, Set[str]],
- pip_data_exclude: List[str],
enable_implicit_namespace_pkgs: bool,
- repo_prefix: str,
installation_dir: Path = Path("."),
- annotation: Optional[annotation.Annotation] = None,
) -> None:
"""Extracts wheel into given directory and creates py_library and filegroup targets.
@@ -305,9 +116,7 @@
wheel_file: the filepath of the .whl
installation_dir: the destination directory for installation of the wheel.
extras: a list of extras to add as dependencies for the installed wheel
- pip_data_exclude: list of file patterns to exclude from the generated data section of the py_library
enable_implicit_namespace_pkgs: if true, disables conversion of implicit namespace packages and will unzip as-is
- annotation: An optional set of annotations to apply to the BUILD contents of the wheel.
"""
whl = wheel.Wheel(wheel_file)
@@ -322,83 +131,25 @@
self_edge_dep = set([whl.name])
whl_deps = sorted(whl.dependencies(extras_requested) - self_edge_dep)
- sanitised_dependencies = [
- bazel.sanitised_repo_library_label(d, repo_prefix=repo_prefix) for d in whl_deps
- ]
- sanitised_wheel_file_dependencies = [
- bazel.sanitised_repo_file_label(d, repo_prefix=repo_prefix) for d in whl_deps
- ]
-
- entry_points = []
- for name, (module, attribute) in sorted(whl.entry_points().items()):
- # There is an extreme edge-case with entry_points that end with `.py`
- # See: https://github.com/bazelbuild/bazel/blob/09c621e4cf5b968f4c6cdf905ab142d5961f9ddc/src/test/java/com/google/devtools/build/lib/rules/python/PyBinaryConfiguredTargetTest.java#L174
- entry_point_without_py = f"{name[:-3]}_py" if name.endswith(".py") else name
- entry_point_target_name = (
- f"{bazel.WHEEL_ENTRY_POINT_PREFIX}_{entry_point_without_py}"
- )
- entry_point_script_name = f"{entry_point_target_name}.py"
- (installation_dir / entry_point_script_name).write_text(
- _generate_entry_point_contents(module, attribute)
- )
- entry_points.append(
- _generate_entry_point_rule(
- entry_point_target_name,
- entry_point_script_name,
- bazel.PY_LIBRARY_LABEL,
- )
- )
-
- with open(os.path.join(installation_dir, "BUILD.bazel"), "w") as build_file:
- additional_content = entry_points
- data = []
- data_exclude = pip_data_exclude
- srcs_exclude = []
- if annotation:
- for src, dest in annotation.copy_files.items():
- data.append(dest)
- additional_content.append(_generate_copy_commands(src, dest))
- for src, dest in annotation.copy_executables.items():
- data.append(dest)
- additional_content.append(
- _generate_copy_commands(src, dest, is_executable=True)
- )
- data.extend(annotation.data)
- data_exclude.extend(annotation.data_exclude_glob)
- srcs_exclude.extend(annotation.srcs_exclude_glob)
- if annotation.additive_build_content:
- additional_content.append(annotation.additive_build_content)
-
- contents = _generate_build_file_contents(
- name=bazel.PY_LIBRARY_LABEL,
- dependencies=sanitised_dependencies,
- whl_file_deps=sanitised_wheel_file_dependencies,
- data_exclude=data_exclude,
- data=data,
- srcs_exclude=srcs_exclude,
- tags=["pypi_name=" + whl.name, "pypi_version=" + whl.version],
- additional_content=additional_content,
- )
- build_file.write(contents)
+ with open(os.path.join(installation_dir, "metadata.json"), "w") as f:
+ metadata = {
+ "name": whl.name,
+ "version": whl.version,
+ "deps": whl_deps,
+ "entry_points": [
+ {
+ "name": name,
+ "module": module,
+ "attribute": attribute,
+ }
+ for name, (module, attribute) in sorted(whl.entry_points().items())
+ ],
+ }
+ json.dump(metadata, f)
def main() -> None:
- parser = argparse.ArgumentParser(
- description="Build and/or fetch a single wheel based on the requirement passed in"
- )
- parser.add_argument(
- "--requirement",
- action="store",
- required=True,
- help="A single PEP508 requirement specifier string.",
- )
- parser.add_argument(
- "--annotation",
- type=annotation.annotation_from_str_path,
- help="A json encoded file containing annotations for rendered packages.",
- )
- arguments.parse_common_args(parser)
- args = parser.parse_args()
+ args = arguments.parser(description=__doc__).parse_args()
deserialized_args = dict(vars(args))
arguments.deserialize_structured_args(deserialized_args)
@@ -441,10 +192,7 @@
_extract_wheel(
wheel_file=whl,
extras=extras,
- pip_data_exclude=deserialized_args["pip_data_exclude"],
enable_implicit_namespace_pkgs=args.enable_implicit_namespace_pkgs,
- repo_prefix=args.repo_prefix,
- annotation=args.annotation,
)
diff --git a/python/pip_install/tools/wheel_installer/wheel_installer_test.py b/python/pip_install/tools/wheel_installer/wheel_installer_test.py
index 8758b67..b24e500 100644
--- a/python/pip_install/tools/wheel_installer/wheel_installer_test.py
+++ b/python/pip_install/tools/wheel_installer/wheel_installer_test.py
@@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import json
import os
import shutil
import tempfile
@@ -54,28 +55,29 @@
)
-class BazelTestCase(unittest.TestCase):
- def test_generate_entry_point_contents(self):
- got = wheel_installer._generate_entry_point_contents("sphinx.cmd.build", "main")
- want = """#!/usr/bin/env python3
-import sys
-from sphinx.cmd.build import main
-if __name__ == "__main__":
- sys.exit(main())
-"""
- self.assertEqual(got, want)
-
- def test_generate_entry_point_contents_with_shebang(self):
- got = wheel_installer._generate_entry_point_contents(
- "sphinx.cmd.build", "main", shebang="#!/usr/bin/python"
- )
- want = """#!/usr/bin/python
-import sys
-from sphinx.cmd.build import main
-if __name__ == "__main__":
- sys.exit(main())
-"""
- self.assertEqual(got, want)
+# TODO @aignas 2023-07-21: migrate to starlark
+# class BazelTestCase(unittest.TestCase):
+# def test_generate_entry_point_contents(self):
+# got = wheel_installer._generate_entry_point_contents("sphinx.cmd.build", "main")
+# want = """#!/usr/bin/env python3
+# import sys
+# from sphinx.cmd.build import main
+# if __name__ == "__main__":
+# sys.exit(main())
+# """
+# self.assertEqual(got, want)
+#
+# def test_generate_entry_point_contents_with_shebang(self):
+# got = wheel_installer._generate_entry_point_contents(
+# "sphinx.cmd.build", "main", shebang="#!/usr/bin/python"
+# )
+# want = """#!/usr/bin/python
+# import sys
+# from sphinx.cmd.build import main
+# if __name__ == "__main__":
+# sys.exit(main())
+# """
+# self.assertEqual(got, want)
class TestWhlFilegroup(unittest.TestCase):
@@ -93,15 +95,33 @@
self.wheel_path,
installation_dir=Path(self.wheel_dir),
extras={},
- pip_data_exclude=[],
enable_implicit_namespace_pkgs=False,
- repo_prefix="prefix_",
)
- self.assertIn(self.wheel_name, os.listdir(self.wheel_dir))
- with open("{}/BUILD.bazel".format(self.wheel_dir)) as build_file:
- build_file_content = build_file.read()
- self.assertIn("filegroup", build_file_content)
+ want_files = [
+ "metadata.json",
+ "site-packages",
+ self.wheel_name,
+ ]
+ self.assertEqual(
+ sorted(want_files),
+ sorted(
+ [
+ str(p.relative_to(self.wheel_dir))
+ for p in Path(self.wheel_dir).glob("*")
+ ]
+ ),
+ )
+ with open("{}/metadata.json".format(self.wheel_dir)) as metadata_file:
+ metadata_file_content = json.load(metadata_file)
+
+ want = dict(
+ version="0.0.1",
+ name="example-minimal-package",
+ deps=[],
+ entry_points=[],
+ )
+ self.assertEqual(want, metadata_file_content)
if __name__ == "__main__":
diff --git a/tests/pip_install/BUILD.bazel b/tests/pip_install/BUILD.bazel
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/pip_install/BUILD.bazel
diff --git a/tests/pip_install/whl_library/BUILD.bazel b/tests/pip_install/whl_library/BUILD.bazel
new file mode 100644
index 0000000..5a27e11
--- /dev/null
+++ b/tests/pip_install/whl_library/BUILD.bazel
@@ -0,0 +1,3 @@
+load(":generate_build_bazel_tests.bzl", "generate_build_bazel_test_suite")
+
+generate_build_bazel_test_suite(name = "generate_build_bazel_tests")
diff --git a/tests/pip_install/whl_library/generate_build_bazel_tests.bzl b/tests/pip_install/whl_library/generate_build_bazel_tests.bzl
new file mode 100644
index 0000000..365233d
--- /dev/null
+++ b/tests/pip_install/whl_library/generate_build_bazel_tests.bzl
@@ -0,0 +1,225 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+""
+
+load("@rules_testing//lib:test_suite.bzl", "test_suite")
+load("//python/pip_install/private:generate_whl_library_build_bazel.bzl", "generate_whl_library_build_bazel") # buildifier: disable=bzl-visibility
+
+_tests = []
+
+def _test_simple(env):
+ want = """\
+load("@rules_python//python:defs.bzl", "py_library", "py_binary")
+load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
+
+package(default_visibility = ["//visibility:public"])
+
+filegroup(
+ name = "dist_info",
+ srcs = glob(["site-packages/*.dist-info/**"], allow_empty = True),
+)
+
+filegroup(
+ name = "data",
+ srcs = glob(["data/**"], allow_empty = True),
+)
+
+filegroup(
+ name = "whl",
+ srcs = glob(["*.whl"], allow_empty = True),
+ data = ["@pypi_bar_baz//:whl", "@pypi_foo//:whl"],
+)
+
+py_library(
+ name = "pkg",
+ srcs = glob(
+ ["site-packages/**/*.py"],
+ exclude=[],
+ # Empty sources are allowed to support wheels that don't have any
+ # pure-Python code, e.g. pymssql, which is written in Cython.
+ allow_empty = True,
+ ),
+ data = [] + glob(
+ ["site-packages/**/*"],
+ exclude=["**/* *", "**/*.py", "**/*.pyc", "**/*.pyc.*", "**/*.dist-info/RECORD"],
+ ),
+ # This makes this directory a top-level in the python import
+ # search path for anything that depends on this.
+ imports = ["site-packages"],
+ deps = ["@pypi_bar_baz//:pkg", "@pypi_foo//:pkg"],
+ tags = ["tag1", "tag2"],
+)
+"""
+ actual = generate_whl_library_build_bazel(
+ repo_prefix = "pypi_",
+ dependencies = ["foo", "bar-baz"],
+ data_exclude = [],
+ tags = ["tag1", "tag2"],
+ entry_points = {},
+ annotation = None,
+ )
+ env.expect.that_str(actual).equals(want)
+
+_tests.append(_test_simple)
+
+def _test_with_annotation(env):
+ want = """\
+load("@rules_python//python:defs.bzl", "py_library", "py_binary")
+load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
+
+package(default_visibility = ["//visibility:public"])
+
+filegroup(
+ name = "dist_info",
+ srcs = glob(["site-packages/*.dist-info/**"], allow_empty = True),
+)
+
+filegroup(
+ name = "data",
+ srcs = glob(["data/**"], allow_empty = True),
+)
+
+filegroup(
+ name = "whl",
+ srcs = glob(["*.whl"], allow_empty = True),
+ data = ["@pypi_bar_baz//:whl", "@pypi_foo//:whl"],
+)
+
+py_library(
+ name = "pkg",
+ srcs = glob(
+ ["site-packages/**/*.py"],
+ exclude=["srcs_exclude_all"],
+ # Empty sources are allowed to support wheels that don't have any
+ # pure-Python code, e.g. pymssql, which is written in Cython.
+ allow_empty = True,
+ ),
+ data = ["file_dest", "exec_dest"] + glob(
+ ["site-packages/**/*"],
+ exclude=["**/* *", "**/*.py", "**/*.pyc", "**/*.pyc.*", "**/*.dist-info/RECORD", "data_exclude_all"],
+ ),
+ # This makes this directory a top-level in the python import
+ # search path for anything that depends on this.
+ imports = ["site-packages"],
+ deps = ["@pypi_bar_baz//:pkg", "@pypi_foo//:pkg"],
+ tags = ["tag1", "tag2"],
+)
+
+copy_file(
+ name = "file_dest.copy",
+ src = "file_src",
+ out = "file_dest",
+ is_executable = False,
+)
+
+copy_file(
+ name = "exec_dest.copy",
+ src = "exec_src",
+ out = "exec_dest",
+ is_executable = True,
+)
+
+# SOMETHING SPECIAL AT THE END
+"""
+ actual = generate_whl_library_build_bazel(
+ repo_prefix = "pypi_",
+ dependencies = ["foo", "bar-baz"],
+ data_exclude = [],
+ tags = ["tag1", "tag2"],
+ entry_points = {},
+ annotation = struct(
+ copy_files = {"file_src": "file_dest"},
+ copy_executables = {"exec_src": "exec_dest"},
+ data = [],
+ data_exclude_glob = ["data_exclude_all"],
+ srcs_exclude_glob = ["srcs_exclude_all"],
+ additive_build_content = """# SOMETHING SPECIAL AT THE END""",
+ ),
+ )
+ env.expect.that_str(actual).equals(want)
+
+_tests.append(_test_with_annotation)
+
+def _test_with_entry_points(env):
+ want = """\
+load("@rules_python//python:defs.bzl", "py_library", "py_binary")
+load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
+
+package(default_visibility = ["//visibility:public"])
+
+filegroup(
+ name = "dist_info",
+ srcs = glob(["site-packages/*.dist-info/**"], allow_empty = True),
+)
+
+filegroup(
+ name = "data",
+ srcs = glob(["data/**"], allow_empty = True),
+)
+
+filegroup(
+ name = "whl",
+ srcs = glob(["*.whl"], allow_empty = True),
+ data = ["@pypi_bar_baz//:whl", "@pypi_foo//:whl"],
+)
+
+py_library(
+ name = "pkg",
+ srcs = glob(
+ ["site-packages/**/*.py"],
+ exclude=[],
+ # Empty sources are allowed to support wheels that don't have any
+ # pure-Python code, e.g. pymssql, which is written in Cython.
+ allow_empty = True,
+ ),
+ data = [] + glob(
+ ["site-packages/**/*"],
+ exclude=["**/* *", "**/*.py", "**/*.pyc", "**/*.pyc.*", "**/*.dist-info/RECORD"],
+ ),
+ # This makes this directory a top-level in the python import
+ # search path for anything that depends on this.
+ imports = ["site-packages"],
+ deps = ["@pypi_bar_baz//:pkg", "@pypi_foo//:pkg"],
+ tags = ["tag1", "tag2"],
+)
+
+py_binary(
+ name = "rules_python_wheel_entry_point_fizz",
+ srcs = ["buzz.py"],
+ # This makes this directory a top-level in the python import
+ # search path for anything that depends on this.
+ imports = ["."],
+ deps = [":pkg"],
+)
+"""
+ actual = generate_whl_library_build_bazel(
+ repo_prefix = "pypi_",
+ dependencies = ["foo", "bar-baz"],
+ data_exclude = [],
+ tags = ["tag1", "tag2"],
+ entry_points = {"fizz": "buzz.py"},
+ annotation = None,
+ )
+ env.expect.that_str(actual).equals(want)
+
+_tests.append(_test_with_entry_points)
+
+def generate_build_bazel_test_suite(name):
+ """Create the test suite.
+
+ Args:
+ name: the name of the test suite
+ """
+ test_suite(name = name, basic_tests = _tests)