Skip to content

Commit 12662b6

Browse files
authored
change approach for vendoring parsed requirements (#679)
Instead of printing something different in the pip_parse tooling, just suggest that users continue to run the repository rule, but check the output into their repo, then load from that instead of from the generated repository. This is simpler than trying to work out what correct arguments to pass to the tool when running it outside of the pip_parse starlark context.
1 parent 4c7e63f commit 12662b6

File tree

15 files changed

+226
-43
lines changed

15 files changed

+226
-43
lines changed

.bazelci/presubmit.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,5 @@ platforms:
3636
- "-//gazelle/..."
3737
# The dependencies needed for this test are not cross-platform: https://github.com/bazelbuild/rules_python/issues/260
3838
- "-//tests:pip_repository_entry_points_example"
39+
test_flags:
40+
- "--test_tag_filters=-fix-windows"

.bazelrc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
# This lets us glob() up all the files inside the examples to make them inputs to tests
44
# (Note, we cannot use `common --deleted_packages` because the bazel version command doesn't support it)
55
# To update these lines, run tools/bazel_integration_test/update_deleted_packages.sh
6-
build --deleted_packages=examples/build_file_generation,examples/pip_install,examples/pip_parse,examples/pip_repository_annotations,examples/py_import,examples/relative_requirements,tests/pip_repository_entry_points
7-
query --deleted_packages=examples/build_file_generation,examples/pip_install,examples/pip_parse,examples/pip_repository_annotations,examples/py_import,examples/relative_requirements,tests/pip_repository_entry_points
6+
build --deleted_packages=examples/build_file_generation,examples/pip_install,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_import,examples/relative_requirements,tests/pip_repository_entry_points
7+
query --deleted_packages=examples/build_file_generation,examples/pip_install,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_import,examples/relative_requirements,tests/pip_repository_entry_points
88

99
test --test_output=errors
1010

docs/pip.md

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -230,19 +230,11 @@ See https://github.com/bazelbuild/rules_python/issues/608
230230
This is the same workflow as Gazelle, which creates `go_repository` rules with
231231
[`update-repos`](https://github.com/bazelbuild/bazel-gazelle#update-repos)
232232

233-
Simply run the same tool that the `pip_parse` repository rule calls.
234-
You can find the arguments in the generated BUILD file in the pip_parse repo,
235-
for example in `$(bazel info output_base)/external/pypi/BUILD.bazel` for a repo
236-
named `pypi`.
237-
238-
The command will look like this:
239-
```shell
240-
bazel run -- @rules_python//python/pip_install/parse_requirements_to_bzl \
241-
--requirements_lock ./requirements_lock.txt \
242-
--quiet False --timeout 120 --repo pypi --repo-prefix pypi_ > requirements.bzl
243-
```
244-
245-
Then load the requirements.bzl file directly, without using `pip_parse` in the WORKSPACE.
233+
To do this, use the "write to source file" pattern documented in
234+
https://blog.aspect.dev/bazel-can-write-to-the-source-folder
235+
to put a copy of the generated requirements.bzl into your project.
236+
Then load the requirements.bzl file directly rather than from the generated repository.
237+
See the example in rules_python/examples/pip_parse_vendored.
246238

247239

248240
**PARAMETERS**

examples/BUILD

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ bazel_integration_test(
2727
timeout = "long",
2828
)
2929

30+
bazel_integration_test(
31+
name = "pip_parse_vendored_example",
32+
timeout = "long",
33+
tags = ["fix-windows"],
34+
)
35+
3036
bazel_integration_test(
3137
name = "pip_repository_annotations_example",
3238
timeout = "long",

examples/pip_parse_vendored/BUILD

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
load("@bazel_skylib//rules:diff_test.bzl", "diff_test")
2+
load("@bazel_skylib//rules:write_file.bzl", "write_file")
3+
load("@rules_python//python:pip.bzl", "compile_pip_requirements")
4+
5+
# This rule adds a convenient way to update the requirements.txt
6+
# lockfile based on the requirements.in.
7+
compile_pip_requirements(name = "requirements")
8+
9+
# The requirements.bzl file is generated with a reference to the interpreter for the host platform.
10+
# In order to check in a platform-agnostic file, we have to replace that reference with the symbol
11+
# loaded from our python toolchain.
12+
genrule(
13+
name = "make_platform_agnostic",
14+
srcs = ["@pip//:requirements.bzl"],
15+
outs = ["requirements.clean.bzl"],
16+
cmd = " | ".join([
17+
"cat $<",
18+
# Insert our load statement after the existing one so we don't produce a file with buildifier warnings
19+
"""sed -e '/^load.*/i\\'$$'\\n''load("@python39//:defs.bzl", "interpreter")'""",
20+
"""tr "'" '"' """,
21+
"""sed 's#"@python39_.*//:bin/python3"#interpreter#' >$@""",
22+
]),
23+
)
24+
25+
write_file(
26+
name = "gen_update",
27+
out = "update.sh",
28+
content = [
29+
# This depends on bash, would need tweaks for Windows
30+
"#!/usr/bin/env bash",
31+
# Bazel gives us a way to access the source folder!
32+
"cd $BUILD_WORKSPACE_DIRECTORY",
33+
"cp -fv bazel-bin/requirements.clean.bzl requirements.bzl",
34+
],
35+
)
36+
37+
sh_binary(
38+
name = "vendor_requirements",
39+
srcs = ["update.sh"],
40+
data = [":make_platform_agnostic"],
41+
)
42+
43+
# Similarly ensures that the requirements.bzl file is updated
44+
# based on the requirements.txt lockfile.
45+
diff_test(
46+
name = "test_vendored",
47+
failure_message = "Please run: bazel run //:vendor_requirements",
48+
file1 = "requirements.bzl",
49+
file2 = ":make_platform_agnostic",
50+
)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# pip_parse vendored
2+
3+
This example is like pip_parse, however we avoid loading from the generated file.
4+
See https://github.com/bazelbuild/rules_python/issues/608
5+
and https://blog.aspect.dev/avoid-eager-fetches.
6+
7+
The requirements now form a triple:
8+
9+
- requirements.in - human editable, expresses only direct dependencies and load-bearing version constraints
10+
- requirements.txt - lockfile produced by pip-compile or other means
11+
- requirements.bzl - the "parsed" version of the lockfile readable by Bazel downloader
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
workspace(name = "pip_repository_annotations_example")
2+
3+
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
4+
5+
local_repository(
6+
name = "rules_python",
7+
path = "../..",
8+
)
9+
10+
http_archive(
11+
name = "bazel_skylib",
12+
sha256 = "c6966ec828da198c5d9adbaa94c05e3a1c7f21bd012a0b29ba8ddbccb2c93b0d",
13+
urls = [
14+
"https://github.com/bazelbuild/bazel-skylib/releases/download/1.1.1/bazel-skylib-1.1.1.tar.gz",
15+
"https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.1.1/bazel-skylib-1.1.1.tar.gz",
16+
],
17+
)
18+
19+
load("@rules_python//python:repositories.bzl", "python_register_toolchains")
20+
21+
python_register_toolchains(
22+
name = "python39",
23+
python_version = "3.9",
24+
)
25+
26+
load("@python39//:defs.bzl", "interpreter")
27+
load("@rules_python//python:pip.bzl", "pip_parse")
28+
29+
# This repository isn't referenced, except by our test that asserts the requirements.bzl is updated.
30+
# It also wouldn't be needed by users of this ruleset.
31+
pip_parse(
32+
name = "pip",
33+
python_interpreter_target = interpreter,
34+
requirements_lock = "//:requirements.txt",
35+
)
36+
37+
# This example vendors the file produced by `pip_parse` above into the repo.
38+
# This way our Bazel doesn't eagerly fetch and install the pip_parse'd
39+
# repository for builds that don't need it.
40+
# See discussion of the trade-offs in the pip_parse documentation
41+
# and the "vendor_requirements" target in the BUILD file.
42+
load("//:requirements.bzl", "install_deps")
43+
44+
install_deps()
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Starlark representation of locked requirements.
2+
3+
@generated by rules_python pip_parse repository rule
4+
from //:requirements.txt
5+
"""
6+
7+
load("@python39//:defs.bzl", "interpreter")
8+
load("@rules_python//python/pip_install:pip_repository.bzl", "whl_library")
9+
10+
all_requirements = ["@pip_certifi//:pkg", "@pip_charset_normalizer//:pkg", "@pip_idna//:pkg", "@pip_requests//:pkg", "@pip_urllib3//:pkg"]
11+
12+
all_whl_requirements = ["@pip_certifi//:whl", "@pip_charset_normalizer//:whl", "@pip_idna//:whl", "@pip_requests//:whl", "@pip_urllib3//:whl"]
13+
14+
_packages = [("pip_certifi", "certifi==2021.10.8 --hash=sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872 --hash=sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"), ("pip_charset_normalizer", "charset-normalizer==2.0.12 --hash=sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597 --hash=sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"), ("pip_idna", "idna==3.3 --hash=sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff --hash=sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"), ("pip_requests", "requests==2.27.1 --hash=sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61 --hash=sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"), ("pip_urllib3", "urllib3==1.26.9 --hash=sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14 --hash=sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e")]
15+
_config = {"enable_implicit_namespace_pkgs": False, "environment": {}, "extra_pip_args": [], "isolated": True, "pip_data_exclude": [], "python_interpreter": "python3", "python_interpreter_target": interpreter, "quiet": True, "repo": "pip", "repo_prefix": "pip_", "timeout": 600}
16+
_annotations = {}
17+
18+
def _clean_name(name):
19+
return name.replace("-", "_").replace(".", "_").lower()
20+
21+
def requirement(name):
22+
return "@pip_" + _clean_name(name) + "//:pkg"
23+
24+
def whl_requirement(name):
25+
return "@pip_" + _clean_name(name) + "//:whl"
26+
27+
def data_requirement(name):
28+
return "@pip_" + _clean_name(name) + "//:data"
29+
30+
def dist_info_requirement(name):
31+
return "@pip_" + _clean_name(name) + "//:dist_info"
32+
33+
def entry_point(pkg, script = None):
34+
if not script:
35+
script = pkg
36+
return "@pip_" + _clean_name(pkg) + "//:rules_python_wheel_entry_point_" + script
37+
38+
def _get_annotation(requirement):
39+
# This expects to parse `setuptools==58.2.0 --hash=sha256:2551203ae6955b9876741a26ab3e767bb3242dafe86a32a749ea0d78b6792f11`
40+
# down wo `setuptools`.
41+
name = requirement.split(" ")[0].split("=")[0]
42+
return _annotations.get(name)
43+
44+
def install_deps():
45+
for name, requirement in _packages:
46+
whl_library(
47+
name = name,
48+
requirement = requirement,
49+
annotation = _get_annotation(requirement),
50+
**_config
51+
)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
requests
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#
2+
# This file is autogenerated by pip-compile
3+
# To update, run:
4+
#
5+
# bazel run //:requirements.update
6+
#
7+
certifi==2021.10.8 \
8+
--hash=sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872 \
9+
--hash=sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569
10+
# via requests
11+
charset-normalizer==2.0.12 \
12+
--hash=sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597 \
13+
--hash=sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df
14+
# via requests
15+
idna==3.3 \
16+
--hash=sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff \
17+
--hash=sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d
18+
# via requests
19+
requests==2.27.1 \
20+
--hash=sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61 \
21+
--hash=sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d
22+
# via -r requirements.in
23+
urllib3==1.26.9 \
24+
--hash=sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14 \
25+
--hash=sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e
26+
# via requests

0 commit comments

Comments
 (0)