From bef2244d295ea5bf979261513b31bf38b941ac28 Mon Sep 17 00:00:00 2001
From: UebelAndre
-
|"pip_parsed_deps"
|
-| kwargs | -
| none | +| requirements_lock | A fully resolved 'requirements.txt' pip requirement file containing the transitive set of your dependencies. If this file is passed instead of 'requirements' no resolve will take place and pip_repository will create individual repositories for each of your dependencies so that wheels are fetched/built only for the targets specified by 'build/run/test'. | none | +| name | The name of the generated repository. |"pip_parsed_deps"
|
+| kwargs | Additional keyword arguments for the underlying pip_repository
rule. | none |
diff --git a/examples/pip_install/BUILD b/examples/pip_install/BUILD
index c8fbc0bbee..c57ffbdcb5 100644
--- a/examples/pip_install/BUILD
+++ b/examples/pip_install/BUILD
@@ -1,4 +1,8 @@
-load("@pip//:requirements.bzl", "requirement")
+load(
+ "@pip//:requirements.bzl",
+ "entry_point",
+ "requirement",
+)
load("@rules_python//python:defs.bzl", "py_binary", "py_test")
load("@rules_python//python/pip_install:requirements.bzl", "compile_pip_requirements")
@@ -42,7 +46,21 @@ py_test(
deps = [":main"],
)
+# For pip dependencies which have entry points, the `entry_point` macro can be
+# used from the generated `pip_install` repository to access a runnable binary.
+alias(
+ name = "yamllint",
+ actual = entry_point("yamllint"),
+)
+
+py_test(
+ name = "entry_point_test",
+ srcs = ["entry_point_test.py"],
+ data = [":yamllint"],
+)
+
# Check that our compiled requirements are up-to-date
compile_pip_requirements(
name = "requirements",
+ extra_args = ["--allow-unsafe"],
)
diff --git a/examples/pip_install/entry_point_test.py b/examples/pip_install/entry_point_test.py
new file mode 100644
index 0000000000..b6b589a7b6
--- /dev/null
+++ b/examples/pip_install/entry_point_test.py
@@ -0,0 +1,20 @@
+#!/usr/bin/env python3
+
+from pathlib import Path
+import subprocess
+import unittest
+
+
+class PipParseEntryPointTest(unittest.TestCase):
+ def test_output(self):
+ self.maxDiff = None
+
+ entry_point = Path("external/pip/pypi__yamllint/rules_python_wheel_entry_point_yamllint")
+ self.assertTrue(entry_point.exists())
+
+ proc = subprocess.run([entry_point, "--version"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ self.assertEqual(proc.stdout.decode("utf-8").strip(), "yamllint 1.26.3")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/examples/pip_install/requirements.in b/examples/pip_install/requirements.in
index cbc5542582..6ecac0dc9a 100644
--- a/examples/pip_install/requirements.in
+++ b/examples/pip_install/requirements.in
@@ -1 +1,2 @@
boto3==1.14.51
+yamllint==1.26.3
diff --git a/examples/pip_install/requirements.txt b/examples/pip_install/requirements.txt
index 78b024682b..267c37002a 100644
--- a/examples/pip_install/requirements.txt
+++ b/examples/pip_install/requirements.txt
@@ -25,19 +25,63 @@ jmespath==0.10.0 \
# via
# boto3
# botocore
-python-dateutil==2.8.1 \
- --hash=sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c \
- --hash=sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a
+pathspec==0.9.0 \
+ --hash=sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a \
+ --hash=sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1
+ # via yamllint
+python-dateutil==2.8.2 \
+ --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \
+ --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9
# via botocore
-s3transfer==0.3.3 \
- --hash=sha256:2482b4259524933a022d59da830f51bd746db62f047d6eb213f2f8855dcb8a13 \
- --hash=sha256:921a37e2aefc64145e7b73d50c71bb4f26f46e4c9f414dc648c6245ff92cf7db
+pyyaml==5.4.1 \
+ --hash=sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf \
+ --hash=sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696 \
+ --hash=sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393 \
+ --hash=sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77 \
+ --hash=sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922 \
+ --hash=sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5 \
+ --hash=sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8 \
+ --hash=sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10 \
+ --hash=sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc \
+ --hash=sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018 \
+ --hash=sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e \
+ --hash=sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253 \
+ --hash=sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347 \
+ --hash=sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183 \
+ --hash=sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541 \
+ --hash=sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb \
+ --hash=sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185 \
+ --hash=sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc \
+ --hash=sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db \
+ --hash=sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa \
+ --hash=sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46 \
+ --hash=sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122 \
+ --hash=sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b \
+ --hash=sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63 \
+ --hash=sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df \
+ --hash=sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc \
+ --hash=sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247 \
+ --hash=sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6 \
+ --hash=sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0
+ # via yamllint
+s3transfer==0.3.7 \
+ --hash=sha256:35627b86af8ff97e7ac27975fe0a98a312814b46c6333d8a6b889627bcd80994 \
+ --hash=sha256:efa5bd92a897b6a8d5c1383828dca3d52d0790e0756d49740563a3fb6ed03246
# via boto3
-six==1.15.0 \
- --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \
- --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced
+six==1.16.0 \
+ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
+ --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
# via python-dateutil
urllib3==1.25.11 \
--hash=sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2 \
--hash=sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e
# via botocore
+yamllint==1.26.3 \
+ --hash=sha256:3934dcde484374596d6b52d8db412929a169f6d9e52e20f9ade5bf3523d9b96e
+ # via -r requirements.in
+
+# The following packages are considered to be unsafe in a requirements file:
+setuptools==57.5.0 \
+ --hash=sha256:60d78588f15b048f86e35cdab73003d8b21dd45108ee61a6693881a427f22073 \
+ --hash=sha256:d9d3266d50f59c6967b9312844470babbdb26304fe740833a5f8d89829ba3a24
+ # via yamllint
diff --git a/examples/pip_parse/BUILD b/examples/pip_parse/BUILD
index ca56af9c0d..2bc713b7ac 100644
--- a/examples/pip_parse/BUILD
+++ b/examples/pip_parse/BUILD
@@ -1,5 +1,6 @@
-load("@pip_parsed_deps//:requirements.bzl", "requirement")
+load("@pip_parsed_deps//:requirements.bzl", "entry_point", "requirement")
load("@rules_python//python:defs.bzl", "py_binary", "py_test")
+load("@rules_python//python/pip_install:requirements.bzl", "compile_pip_requirements")
# Toolchain setup, this is optional.
# Demonstrate that we can use the same python interpreter for the toolchain and executing pip in pip install (see WORKSPACE).
@@ -40,3 +41,29 @@ py_test(
srcs = ["test.py"],
deps = [":main"],
)
+
+# For pip dependencies which have entry points, the `entry_point` macro can be
+# used from the generated `pip_parse` repository to access a runnable binary.
+alias(
+ name = "yamllint",
+ # If `pkg` and `script` are the same, passing a single string to
+ # `entry_point` would work as well: `entry_point("yamllint")`
+ actual = entry_point(
+ pkg = "yamllint",
+ script = "yamllint",
+ ),
+)
+
+py_test(
+ name = "entry_point_test",
+ srcs = ["entry_point_test.py"],
+ data = [":yamllint"],
+)
+
+# This rule adds a convenient way to update the requiremenst file.
+compile_pip_requirements(
+ name = "requirements",
+ extra_args = ["--allow-unsafe"],
+ requirements_in = "requirements.txt",
+ requirements_txt = "requirements_lock.txt",
+)
diff --git a/examples/pip_parse/entry_point_test.py b/examples/pip_parse/entry_point_test.py
new file mode 100644
index 0000000000..7d2234377f
--- /dev/null
+++ b/examples/pip_parse/entry_point_test.py
@@ -0,0 +1,20 @@
+#!/usr/bin/env python3
+
+from pathlib import Path
+import subprocess
+import unittest
+
+
+class PipParseEntryPointTest(unittest.TestCase):
+ def test_output(self):
+ self.maxDiff = None
+
+ entry_point = Path("external/pip_parsed_deps_pypi__yamllint/rules_python_wheel_entry_point_yamllint")
+ self.assertTrue(entry_point.exists())
+
+ proc = subprocess.run([entry_point, "--version"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ self.assertEqual(proc.stdout.decode("utf-8").strip(), "yamllint 1.26.3")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/examples/pip_parse/requirements.txt b/examples/pip_parse/requirements.txt
index 9d84d35885..019562ae60 100644
--- a/examples/pip_parse/requirements.txt
+++ b/examples/pip_parse/requirements.txt
@@ -1 +1,2 @@
requests==2.25.1
+yamllint==1.26.3
diff --git a/examples/pip_parse/requirements_lock.txt b/examples/pip_parse/requirements_lock.txt
index 7573a6f591..dd29e95011 100644
--- a/examples/pip_parse/requirements_lock.txt
+++ b/examples/pip_parse/requirements_lock.txt
@@ -2,7 +2,7 @@
# This file is autogenerated by pip-compile
# To update, run:
#
-# pip-compile --generate-hashes --output-file=requirements_lock.txt requirements.txt
+# bazel run //:requirements.update
#
certifi==2020.12.5 \
--hash=sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c \
@@ -16,6 +16,41 @@ idna==2.10 \
--hash=sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6 \
--hash=sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0
# via requests
+pathspec==0.9.0 \
+ --hash=sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a \
+ --hash=sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1
+ # via yamllint
+pyyaml==5.4.1 \
+ --hash=sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf \
+ --hash=sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696 \
+ --hash=sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393 \
+ --hash=sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77 \
+ --hash=sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922 \
+ --hash=sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5 \
+ --hash=sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8 \
+ --hash=sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10 \
+ --hash=sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc \
+ --hash=sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018 \
+ --hash=sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e \
+ --hash=sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253 \
+ --hash=sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347 \
+ --hash=sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183 \
+ --hash=sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541 \
+ --hash=sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb \
+ --hash=sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185 \
+ --hash=sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc \
+ --hash=sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db \
+ --hash=sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa \
+ --hash=sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46 \
+ --hash=sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122 \
+ --hash=sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b \
+ --hash=sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63 \
+ --hash=sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df \
+ --hash=sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc \
+ --hash=sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247 \
+ --hash=sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6 \
+ --hash=sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0
+ # via yamllint
requests==2.25.1 \
--hash=sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804 \
--hash=sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e
@@ -24,3 +59,12 @@ urllib3==1.26.5 \
--hash=sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c \
--hash=sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098
# via requests
+yamllint==1.26.3 \
+ --hash=sha256:3934dcde484374596d6b52d8db412929a169f6d9e52e20f9ade5bf3523d9b96e
+ # via -r requirements.txt
+
+# The following packages are considered to be unsafe in a requirements file:
+setuptools==57.5.0 \
+ --hash=sha256:60d78588f15b048f86e35cdab73003d8b21dd45108ee61a6693881a427f22073 \
+ --hash=sha256:d9d3266d50f59c6967b9312844470babbdb26304fe740833a5f8d89829ba3a24
+ # via yamllint
diff --git a/python/pip.bzl b/python/pip.bzl
index 5027666806..785156f4b7 100644
--- a/python/pip.bzl
+++ b/python/pip.bzl
@@ -42,10 +42,40 @@ def pip_install(requirements, name = "pip", **kwargs):
)
```
+ In addition to the `requirement` macro, which is used to access the generated `py_library`
+ target generated from a package's wheel, The generated `requirements.bzl` file contains
+ functionality for exposing [entry points][whl_ep] as `py_binary` targets as well.
+
+ [whl_ep]: https://packaging.python.org/specifications/entry-points/
+
+ ```python
+ load("@pip_deps//:requirements.bzl", "entry_point")
+
+ alias(
+ name = "pip-compile",
+ actual = entry_point(
+ pkg = "pip-tools",
+ script = "pip-compile",
+ ),
+ )
+ ```
+
+ Note that for packages who's name and script are the same, only the name of the package
+ is needed when calling the `entry_point` macro.
+
+ ```python
+ load("@pip_deps//:requirements.bzl", "entry_point")
+
+ alias(
+ name = "flake8",
+ actual = entry_point("flake8"),
+ )
+ ```
+
Args:
- requirements: A 'requirements.txt' pip requirements file.
- name: A unique name for the created external repository (default 'pip').
- **kwargs: Keyword arguments passed directly to the `pip_repository` repository rule.
+ requirements: A 'requirements.txt' pip requirements file.
+ name: A unique name for the created external repository (default 'pip').
+ **kwargs: Keyword arguments passed directly to the `pip_repository` repository rule.
"""
# Just in case our dependencies weren't already fetched
@@ -58,6 +88,80 @@ def pip_install(requirements, name = "pip", **kwargs):
)
def pip_parse(requirements_lock, name = "pip_parsed_deps", **kwargs):
+ """Imports a locked/compiled requirements file and generates a new `requirements.bzl` file.
+
+ This is used via the `WORKSPACE` pattern:
+
+ ```python
+ load("@rules_python//python:pip.bzl", "pip_parse")
+
+ pip_parse(
+ name = "pip_deps",
+ requirements_lock = ":requirements.txt",
+ )
+
+ load("@pip_deps//:requirements.bzl", "install_deps")
+
+ install_deps()
+ ```
+
+ You can then reference imported dependencies from your `BUILD` file with:
+
+ ```python
+ load("@pip_deps//:requirements.bzl", "requirement")
+
+ py_library(
+ name = "bar",
+ ...
+ deps = [
+ "//my/other:dep",
+ requirement("requests"),
+ requirement("numpy"),
+ ],
+ )
+ ```
+
+ In addition to the `requirement` macro, which is used to access the generated `py_library`
+ target generated from a package's wheel, The generated `requirements.bzl` file contains
+ functionality for exposing [entry points][whl_ep] as `py_binary` targets as well.
+
+ [whl_ep]: https://packaging.python.org/specifications/entry-points/
+
+ ```python
+ load("@pip_deps//:requirements.bzl", "entry_point")
+
+ alias(
+ name = "pip-compile",
+ actual = entry_point(
+ pkg = "pip-tools",
+ script = "pip-compile",
+ ),
+ )
+ ```
+
+ Note that for packages who's name and script are the same, only the name of the package
+ is needed when calling the `entry_point` macro.
+
+ ```python
+ load("@pip_deps//:requirements.bzl", "entry_point")
+
+ alias(
+ name = "flake8",
+ actual = entry_point("flake8"),
+ )
+ ```
+
+ Args:
+ requirements_lock (Label): A fully resolved 'requirements.txt' pip requirement file
+ containing the transitive set of your dependencies. If this file is passed instead
+ of 'requirements' no resolve will take place and pip_repository will create
+ individual repositories for each of your dependencies so that wheels are
+ fetched/built only for the targets specified by 'build/run/test'.
+ name (str, optional): The name of the generated repository.
+ **kwargs (dict): Additional keyword arguments for the underlying
+ `pip_repository` rule.
+ """
+
# Just in case our dependencies weren't already fetched
pip_install_dependencies()
diff --git a/python/pip_install/extract_wheels/lib/bazel.py b/python/pip_install/extract_wheels/lib/bazel.py
index 0dbc560d86..a51a41ad5e 100644
--- a/python/pip_install/extract_wheels/lib/bazel.py
+++ b/python/pip_install/extract_wheels/lib/bazel.py
@@ -4,6 +4,7 @@
import json
from typing import Iterable, List, Dict, Set, Optional
import shutil
+from pathlib import Path
from python.pip_install.extract_wheels.lib import namespace_pkgs, wheel, purelib
@@ -12,10 +13,73 @@
PY_LIBRARY_LABEL = "pkg"
DATA_LABEL = "data"
DIST_INFO_LABEL = "dist_info"
+WHEEL_ENTRY_POINT_PREFIX = "rules_python_wheel_entry_point"
+
+
+def generate_entry_point_contents(entry_point: str, shebang: str = "#!/usr/bin/env python3") -> str:
+ """Generate the contents of an entry point script.
+
+ Args:
+ entry_point (str): The name of the entry point as show in the
+ `console_scripts` section of `entry_point.txt`.
+ shebang (str, optional): The shebang to use for the entry point python
+ file.
+
+ Returns:
+ str: A string of python code.
+ """
+ module, method = entry_point.split(":", 1)
+ return textwrap.dedent("""\
+ {shebang}
+ if __name__ == "__main__":
+ from {module} import {method}
+ {method}()
+ """.format(
+ shebang=shebang,
+ module=module,
+ method=method
+ ))
+
+
+def generate_entry_point_rule(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:
+ 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.
+ """
+ name = os.path.splitext(script)[0]
+ 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_build_file_contents(
- name: str, dependencies: List[str], whl_file_deps: List[str], pip_data_exclude: List[str],
+ name: str,
+ dependencies: List[str],
+ whl_file_deps: List[str],
+ pip_data_exclude: List[str],
+ additional_targets: List[str] = [],
) -> str:
"""Generate a BUILD file for an unzipped Wheel
@@ -23,6 +87,7 @@ def generate_build_file_contents(
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.
+ additional_targets: A list of additional targets to append to the BUILD file contents.
Returns:
A complete BUILD file as a string
@@ -31,12 +96,19 @@ def generate_build_file_contents(
there may be no Python sources whatsoever (e.g. packages written in Cython: like `pymssql`).
"""
- data_exclude = ["*.whl", "**/*.py", "**/* *", "BUILD.bazel", "WORKSPACE"] + pip_data_exclude
+ data_exclude = [
+ "*.whl",
+ "**/*.py",
+ f"{WHEEL_ENTRY_POINT_PREFIX}*.py",
+ "**/* *",
+ "BUILD.bazel",
+ "WORKSPACE",
+ ] + pip_data_exclude
- return textwrap.dedent(
+ return "\n".join([textwrap.dedent(
"""\
- load("@rules_python//python:defs.bzl", "py_library")
-
+ load("@rules_python//python:defs.bzl", "py_library", "py_binary")
+
package(default_visibility = ["//visibility:public"])
filegroup(
@@ -57,7 +129,7 @@ def generate_build_file_contents(
py_library(
name = "{name}",
- srcs = glob(["**/*.py"], allow_empty = True),
+ srcs = glob(["**/*.py"], exclude=["{entry_point_prefix}*.py"], allow_empty = True),
data = glob(["**/*"], exclude={data_exclude}),
# This makes this directory a top-level in the python import
# search path for anything that depends on this.
@@ -72,7 +144,8 @@ def generate_build_file_contents(
whl_file_deps=",".join(whl_file_deps),
data_label=DATA_LABEL,
dist_info_label=DIST_INFO_LABEL,
- )
+ entry_point_prefix=WHEEL_ENTRY_POINT_PREFIX,
+ ))] + additional_targets
)
@@ -114,6 +187,11 @@ def data_requirement(name):
def dist_info_requirement(name):
return requirement(name) + ":{dist_info_label}"
+ def entry_point(pkg, script = None):
+ if not script:
+ script = pkg
+ return requirement(pkg) + ":{entry_point_prefix}_" + script
+
def install_deps():
fail("install_deps() only works if you are creating an incremental repo. Did you mean to use pip_parse()?")
""".format(
@@ -123,6 +201,7 @@ def install_deps():
whl_file_label=WHEEL_FILE_LABEL,
data_label=DATA_LABEL,
dist_info_label=DIST_INFO_LABEL,
+ entry_point_prefix=WHEEL_ENTRY_POINT_PREFIX,
)
)
@@ -262,12 +341,25 @@ def extract_wheel(
sanitised_file_label(d) for d in whl_deps
]
+ library_name = PY_LIBRARY_LABEL if incremental else sanitise_name(whl.name)
+
+ directory_path = Path(directory)
+ entry_points = []
+ for name, entry_point in sorted(whl.entry_points().items()):
+ entry_point_script = f"{WHEEL_ENTRY_POINT_PREFIX}_{name}.py"
+ (directory_path / entry_point_script).write_text(generate_entry_point_contents(entry_point))
+ entry_points.append(generate_entry_point_rule(
+ entry_point_script,
+ library_name,
+ ))
+
with open(os.path.join(directory, "BUILD.bazel"), "w") as build_file:
contents = generate_build_file_contents(
- PY_LIBRARY_LABEL if incremental else sanitise_name(whl.name),
+ library_name,
sanitised_dependencies,
sanitised_wheel_file_dependencies,
- pip_data_exclude
+ pip_data_exclude,
+ entry_points,
)
build_file.write(contents)
diff --git a/python/pip_install/extract_wheels/lib/wheel.py b/python/pip_install/extract_wheels/lib/wheel.py
index c13f4e8621..aa5b0ca2d3 100644
--- a/python/pip_install/extract_wheels/lib/wheel.py
+++ b/python/pip_install/extract_wheels/lib/wheel.py
@@ -1,4 +1,5 @@
"""Utility class to inspect an extracted wheel directory"""
+import configparser
import glob
import os
import stat
@@ -42,6 +43,32 @@ def name(self) -> str:
def metadata(self) -> pkginfo.Wheel:
return pkginfo.get_metadata(self.path)
+ def entry_points(self) -> Dict[str, str]:
+ """Returns the entrypoints defined in the current wheel
+
+ See https://packaging.python.org/specifications/entry-points/ for more info
+
+ Returns:
+ Dict[str, str]: A mappying of the entry point's name to it's method
+ """
+ with zipfile.ZipFile(self.path, "r") as whl:
+ # Calculate the location of the entry_points.txt file
+ metadata = self.metadata
+ name = "{}-{}".format(metadata.name.replace("-", "_"), metadata.version)
+ entry_points_path = os.path.join("{}.dist-info".format(name), "entry_points.txt")
+
+ # If this file does not exist in the wheel, there are no entry points
+ if entry_points_path not in whl.namelist():
+ return dict()
+
+ # Parse the avaialble entry points
+ config = configparser.ConfigParser()
+ config.read_string(whl.read(entry_points_path).decode("utf-8"))
+ if "console_scripts" in config.sections():
+ return dict(config["console_scripts"])
+
+ return dict()
+
def dependencies(self, extras_requested: Optional[Set[str]] = None) -> Set[str]:
dependency_set = set()
diff --git a/python/pip_install/parse_requirements_to_bzl/__init__.py b/python/pip_install/parse_requirements_to_bzl/__init__.py
index a77dd858f2..aa0f1b6a21 100644
--- a/python/pip_install/parse_requirements_to_bzl/__init__.py
+++ b/python/pip_install/parse_requirements_to_bzl/__init__.py
@@ -90,10 +90,15 @@ def whl_requirement(name):
return "@{repo_prefix}" + _clean_name(name) + "//:{wheel_file_label}"
def data_requirement(name):
- return requirement(name) + ":{data_label}"
+ return "@{repo_prefix}" + _clean_name(name) + "//:{data_label}"
def dist_info_requirement(name):
- return requirement(name) + ":{dist_info_label}"
+ return "@{repo_prefix}" + _clean_name(name) + "//:{dist_info_label}"
+
+ def entry_point(pkg, script = None):
+ if not script:
+ script = pkg
+ return "@{repo_prefix}" + _clean_name(pkg) + "//:{entry_point_prefix}_" + script
def install_deps():
for name, requirement in _packages:
@@ -112,6 +117,7 @@ def install_deps():
wheel_file_label=bazel.WHEEL_FILE_LABEL,
data_label=bazel.DATA_LABEL,
dist_info_label=bazel.DIST_INFO_LABEL,
+ entry_point_prefix=bazel.WHEEL_ENTRY_POINT_PREFIX,
)
)