Skip to content

Commit e923f9e

Browse files
cj81499lpulleyaignasrickeylev
authored
feat: update compile_pip_requirements to support multiple input files (#1067)
`pip-compile` can compile multiple input files into a single output file, but `rules_python`'s `compile_pip_requirements` doesn't currently support this. With this change, the `requirements_in` argument to `compile_pip_requirements` can now accept a list of strings (in addition to the previously accepted argument types). In order to support a variable number of input files, my coworker (@lpulley) and I updated `dependency_resolver.py` to use the `click` CLI library. We felt this was acceptable since `pip-compile` already requires `click` to run, so we're not adding a new dependency. We also made changes to the script to avoid mutating `sys.argv`, instead opting to build a new list (`argv`) from scratch that'll be passed to the `pip-compile` CLI. While subjective, I feel this improves readability, since it's not immediately obvious what's in `sys.argv`, but it's clear that `argv` begins empty, and is added to over the course of the program's execution. --------- Co-authored-by: Logan Pulley <[email protected]> Co-authored-by: Ignas Anikevicius <[email protected]> Co-authored-by: Richard Levasseur <[email protected]>
1 parent 0d6d8f3 commit e923f9e

12 files changed

+130
-23
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ A brief description of the categories of changes:
3535
* `3.12 -> 3.12.4`
3636

3737
### Fixed
38+
* (rules) `compile_pip_requirements` now sets the `USERPROFILE` env variable on
39+
Windows to work around an issue where `setuptools` fails to locate the user's
40+
home directory.
3841
* (rules) correctly handle absolute URLs in parse_simpleapi_html.bzl.
3942
* (rules) Fixes build targets linking against `@rules_python//python/cc:current_py_cc_libs`
4043
in host platform builds on macOS, by editing the `LC_ID_DYLIB` field of the hermetic interpreter's
@@ -73,6 +76,7 @@ A brief description of the categories of changes:
7376
Fixes [#1631](https://github.com/bazelbuild/rules_python/issues/1631).
7477

7578
### Added
79+
* (rules) `compile_pip_requirements` supports multiple requirements input files as `srcs`.
7680
* (rules) `PYTHONSAFEPATH` is inherited from the calling environment to allow
7781
disabling it (Requires {obj}`--bootstrap_impl=script`)
7882
([#2060](https://github.com/bazelbuild/rules_python/issues/2060)).

python/private/pypi/dependency_resolver/dependency_resolver.py

+9-10
Original file line numberDiff line numberDiff line change
@@ -80,15 +80,15 @@ def _locate(bazel_runfiles, file):
8080

8181

8282
@click.command(context_settings={"ignore_unknown_options": True})
83-
@click.argument("requirements_in")
83+
@click.option("--src", "srcs", multiple=True, required=True)
8484
@click.argument("requirements_txt")
8585
@click.argument("update_target_label")
8686
@click.option("--requirements-linux")
8787
@click.option("--requirements-darwin")
8888
@click.option("--requirements-windows")
8989
@click.argument("extra_args", nargs=-1, type=click.UNPROCESSED)
9090
def main(
91-
requirements_in: str,
91+
srcs: Tuple[str, ...],
9292
requirements_txt: str,
9393
update_target_label: str,
9494
requirements_linux: Optional[str],
@@ -105,7 +105,7 @@ def main(
105105
requirements_windows=requirements_windows,
106106
)
107107

108-
resolved_requirements_in = _locate(bazel_runfiles, requirements_in)
108+
resolved_srcs = [_locate(bazel_runfiles, src) for src in srcs]
109109
resolved_requirements_file = _locate(bazel_runfiles, requirements_file)
110110

111111
# Files in the runfiles directory has the following naming schema:
@@ -118,11 +118,11 @@ def main(
118118
: -(len(requirements_file) - len(repository_prefix))
119119
]
120120

121-
# As requirements_in might contain references to generated files we want to
121+
# As srcs might contain references to generated files we want to
122122
# use the runfiles file first. Thus, we need to compute the relative path
123123
# from the execution root.
124124
# Note: Windows cannot reference generated files without runfiles support enabled.
125-
requirements_in_relative = requirements_in[len(repository_prefix) :]
125+
srcs_relative = [src[len(repository_prefix) :] for src in srcs]
126126
requirements_file_relative = requirements_file[len(repository_prefix) :]
127127

128128
# Before loading click, set the locale for its parser.
@@ -162,10 +162,9 @@ def main(
162162
argv.append(
163163
f"--output-file={requirements_file_relative if UPDATE else requirements_out}"
164164
)
165-
argv.append(
166-
requirements_in_relative
167-
if Path(requirements_in_relative).exists()
168-
else resolved_requirements_in
165+
argv.extend(
166+
(src_relative if Path(src_relative).exists() else resolved_src)
167+
for src_relative, resolved_src in zip(srcs_relative, resolved_srcs)
169168
)
170169
argv.extend(extra_args)
171170

@@ -200,7 +199,7 @@ def main(
200199
print(
201200
"pip-compile exited with code 2. This means that pip-compile found "
202201
"incompatible requirements or could not find a version that matches "
203-
f"the install requirement in {requirements_in_relative}.",
202+
f"the install requirement in one of {srcs_relative}.",
204203
file=sys.stderr,
205204
)
206205
sys.exit(1)

python/private/pypi/pip_compile.bzl

+26-13
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ load("//python:defs.bzl", _py_binary = "py_binary", _py_test = "py_test")
2323

2424
def pip_compile(
2525
name,
26+
srcs = None,
2627
src = None,
2728
extra_args = [],
2829
extra_deps = [],
@@ -53,6 +54,11 @@ def pip_compile(
5354
5455
Args:
5556
name: base name for generated targets, typically "requirements".
57+
srcs: a list of files containing inputs to dependency resolution. If not specified,
58+
defaults to `["pyproject.toml"]`. Supported formats are:
59+
* a requirements text file, usually named `requirements.in`
60+
* A `.toml` file, where the `project.dependencies` list is used as per
61+
[PEP621](https://peps.python.org/pep-0621/).
5662
src: file containing inputs to dependency resolution. If not specified,
5763
defaults to `pyproject.toml`. Supported formats are:
5864
* a requirements text file, usually named `requirements.in`
@@ -63,7 +69,7 @@ def pip_compile(
6369
generate_hashes: whether to put hashes in the requirements_txt file.
6470
py_binary: the py_binary rule to be used.
6571
py_test: the py_test rule to be used.
66-
requirements_in: file expressing desired dependencies. Deprecated, use src instead.
72+
requirements_in: file expressing desired dependencies. Deprecated, use src or srcs instead.
6773
requirements_txt: result of "compiling" the requirements.in file.
6874
requirements_linux: File of linux specific resolve output to check validate if requirement.in has changes.
6975
requirements_darwin: File of darwin specific resolve output to check validate if requirement.in has changes.
@@ -72,10 +78,15 @@ def pip_compile(
7278
visibility: passed to both the _test and .update rules.
7379
**kwargs: other bazel attributes passed to the "_test" rule.
7480
"""
75-
if requirements_in and src:
76-
fail("Only one of 'src' and 'requirements_in' attributes can be used")
81+
if len([x for x in [srcs, src, requirements_in] if x != None]) > 1:
82+
fail("At most one of 'srcs', 'src', and 'requirements_in' attributes may be provided")
83+
84+
if requirements_in:
85+
srcs = [requirements_in]
86+
elif src:
87+
srcs = [src]
7788
else:
78-
src = requirements_in or src or "pyproject.toml"
89+
srcs = srcs or ["pyproject.toml"]
7990

8091
requirements_txt = name + ".txt" if requirements_txt == None else requirements_txt
8192

@@ -88,16 +99,15 @@ def pip_compile(
8899
visibility = visibility,
89100
)
90101

91-
data = [name, requirements_txt, src] + [f for f in (requirements_linux, requirements_darwin, requirements_windows) if f != None]
102+
data = [name, requirements_txt] + srcs + [f for f in (requirements_linux, requirements_darwin, requirements_windows) if f != None]
92103

93104
# Use the Label constructor so this is expanded in the context of the file
94105
# where it appears, which is to say, in @rules_python
95106
pip_compile = Label("//python/private/pypi/dependency_resolver:dependency_resolver.py")
96107

97108
loc = "$(rlocationpath {})"
98109

99-
args = [
100-
loc.format(src),
110+
args = ["--src=%s" % loc.format(src) for src in srcs] + [
101111
loc.format(requirements_txt),
102112
"//%s:%s.update" % (native.package_name(), name),
103113
"--resolver=backtracking",
@@ -144,12 +154,15 @@ def pip_compile(
144154
"visibility": visibility,
145155
}
146156

147-
# cheap way to detect the bazel version
148-
_bazel_version_4_or_greater = "propeller_optimize" in dir(native)
149-
150-
# Bazel 4.0 added the "env" attribute to py_test/py_binary
151-
if _bazel_version_4_or_greater:
152-
attrs["env"] = kwargs.pop("env", {})
157+
# setuptools (the default python build tool) attempts to find user
158+
# configuration in the user's home direcotory. This seems to work fine on
159+
# linux and macOS, but fails on Windows, so we conditionally provide a fake
160+
# USERPROFILE env variable to allow setuptools to proceed without finding
161+
# user-provided configuration.
162+
kwargs["env"] = select({
163+
"@@platforms//os:windows": {"USERPROFILE": "Z:\\FakeSetuptoolsHomeDirectoryHack"},
164+
"//conditions:default": {},
165+
}) | kwargs.get("env", {})
153166

154167
py_binary(
155168
name = name + ".update",

tests/multiple_inputs/BUILD.bazel

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
load("@rules_python//python:pip.bzl", "compile_pip_requirements")
2+
3+
compile_pip_requirements(
4+
name = "multiple_requirements_in",
5+
srcs = [
6+
"requirements_1.in",
7+
"requirements_2.in",
8+
],
9+
requirements_txt = "multiple_requirements_in.txt",
10+
)
11+
12+
compile_pip_requirements(
13+
name = "multiple_pyproject_toml",
14+
srcs = [
15+
"a/pyproject.toml",
16+
"b/pyproject.toml",
17+
],
18+
requirements_txt = "multiple_pyproject_toml.txt",
19+
)
20+
21+
compile_pip_requirements(
22+
name = "multiple_inputs",
23+
srcs = [
24+
"a/pyproject.toml",
25+
"b/pyproject.toml",
26+
"requirements_1.in",
27+
"requirements_2.in",
28+
],
29+
requirements_txt = "multiple_inputs.txt",
30+
)

tests/multiple_inputs/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# multiple_inputs
2+
3+
Test that `compile_pip_requirements` works as intended when using more than one input file.
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[project]
2+
name = "multiple_inputs_1"
3+
version = "0.0.0"
4+
5+
dependencies = ["urllib3"]
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[project]
2+
name = "multiple_inputs_2"
3+
version = "0.0.0"
4+
5+
dependencies = ["attrs"]
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#
2+
# This file is autogenerated by pip-compile with Python 3.11
3+
# by the following command:
4+
#
5+
# bazel run //tests/multiple_inputs:multiple_inputs.update
6+
#
7+
attrs==23.1.0 \
8+
--hash=sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04 \
9+
--hash=sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015
10+
# via
11+
# -r tests/multiple_inputs/requirements_2.in
12+
# multiple_inputs_2 (tests/multiple_inputs/b/pyproject.toml)
13+
urllib3==2.0.7 \
14+
--hash=sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84 \
15+
--hash=sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e
16+
# via
17+
# -r tests/multiple_inputs/requirements_1.in
18+
# multiple_inputs_1 (tests/multiple_inputs/a/pyproject.toml)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#
2+
# This file is autogenerated by pip-compile with Python 3.11
3+
# by the following command:
4+
#
5+
# bazel run //tests/multiple_inputs:multiple_pyproject_toml.update
6+
#
7+
attrs==23.1.0 \
8+
--hash=sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04 \
9+
--hash=sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015
10+
# via multiple_inputs_2 (tests/multiple_inputs/b/pyproject.toml)
11+
urllib3==2.0.7 \
12+
--hash=sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84 \
13+
--hash=sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e
14+
# via multiple_inputs_1 (tests/multiple_inputs/a/pyproject.toml)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#
2+
# This file is autogenerated by pip-compile with Python 3.11
3+
# by the following command:
4+
#
5+
# bazel run //tests/multiple_inputs:multiple_requirements_in.update
6+
#
7+
attrs==23.1.0 \
8+
--hash=sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04 \
9+
--hash=sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015
10+
# via -r tests/multiple_inputs/requirements_2.in
11+
urllib3==2.0.7 \
12+
--hash=sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84 \
13+
--hash=sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e
14+
# via -r tests/multiple_inputs/requirements_1.in
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
urllib3
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
attrs

0 commit comments

Comments
 (0)