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 <lpulley@ocient.com>
Co-authored-by: Ignas Anikevicius <240938+aignas@users.noreply.github.com>
Co-authored-by: Richard Levasseur <rlevasseur@google.com>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8d52bac..23d910c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -35,6 +35,9 @@
* `3.12 -> 3.12.4`
### Fixed
+* (rules) `compile_pip_requirements` now sets the `USERPROFILE` env variable on
+ Windows to work around an issue where `setuptools` fails to locate the user's
+ home directory.
* (rules) correctly handle absolute URLs in parse_simpleapi_html.bzl.
* (rules) Fixes build targets linking against `@rules_python//python/cc:current_py_cc_libs`
in host platform builds on macOS, by editing the `LC_ID_DYLIB` field of the hermetic interpreter's
@@ -73,6 +76,7 @@
Fixes [#1631](https://github.com/bazelbuild/rules_python/issues/1631).
### Added
+* (rules) `compile_pip_requirements` supports multiple requirements input files as `srcs`.
* (rules) `PYTHONSAFEPATH` is inherited from the calling environment to allow
disabling it (Requires {obj}`--bootstrap_impl=script`)
([#2060](https://github.com/bazelbuild/rules_python/issues/2060)).
diff --git a/python/private/pypi/dependency_resolver/dependency_resolver.py b/python/private/pypi/dependency_resolver/dependency_resolver.py
index afe5076..0ff9b2f 100644
--- a/python/private/pypi/dependency_resolver/dependency_resolver.py
+++ b/python/private/pypi/dependency_resolver/dependency_resolver.py
@@ -80,7 +80,7 @@
@click.command(context_settings={"ignore_unknown_options": True})
-@click.argument("requirements_in")
+@click.option("--src", "srcs", multiple=True, required=True)
@click.argument("requirements_txt")
@click.argument("update_target_label")
@click.option("--requirements-linux")
@@ -88,7 +88,7 @@
@click.option("--requirements-windows")
@click.argument("extra_args", nargs=-1, type=click.UNPROCESSED)
def main(
- requirements_in: str,
+ srcs: Tuple[str, ...],
requirements_txt: str,
update_target_label: str,
requirements_linux: Optional[str],
@@ -105,7 +105,7 @@
requirements_windows=requirements_windows,
)
- resolved_requirements_in = _locate(bazel_runfiles, requirements_in)
+ resolved_srcs = [_locate(bazel_runfiles, src) for src in srcs]
resolved_requirements_file = _locate(bazel_runfiles, requirements_file)
# Files in the runfiles directory has the following naming schema:
@@ -118,11 +118,11 @@
: -(len(requirements_file) - len(repository_prefix))
]
- # As requirements_in might contain references to generated files we want to
+ # As srcs might contain references to generated files we want to
# use the runfiles file first. Thus, we need to compute the relative path
# from the execution root.
# Note: Windows cannot reference generated files without runfiles support enabled.
- requirements_in_relative = requirements_in[len(repository_prefix) :]
+ srcs_relative = [src[len(repository_prefix) :] for src in srcs]
requirements_file_relative = requirements_file[len(repository_prefix) :]
# Before loading click, set the locale for its parser.
@@ -162,10 +162,9 @@
argv.append(
f"--output-file={requirements_file_relative if UPDATE else requirements_out}"
)
- argv.append(
- requirements_in_relative
- if Path(requirements_in_relative).exists()
- else resolved_requirements_in
+ argv.extend(
+ (src_relative if Path(src_relative).exists() else resolved_src)
+ for src_relative, resolved_src in zip(srcs_relative, resolved_srcs)
)
argv.extend(extra_args)
@@ -200,7 +199,7 @@
print(
"pip-compile exited with code 2. This means that pip-compile found "
"incompatible requirements or could not find a version that matches "
- f"the install requirement in {requirements_in_relative}.",
+ f"the install requirement in one of {srcs_relative}.",
file=sys.stderr,
)
sys.exit(1)
diff --git a/python/private/pypi/pip_compile.bzl b/python/private/pypi/pip_compile.bzl
index f284a00..a6cabf7 100644
--- a/python/private/pypi/pip_compile.bzl
+++ b/python/private/pypi/pip_compile.bzl
@@ -23,6 +23,7 @@
def pip_compile(
name,
+ srcs = None,
src = None,
extra_args = [],
extra_deps = [],
@@ -53,6 +54,11 @@
Args:
name: base name for generated targets, typically "requirements".
+ srcs: a list of files containing inputs to dependency resolution. If not specified,
+ defaults to `["pyproject.toml"]`. Supported formats are:
+ * a requirements text file, usually named `requirements.in`
+ * A `.toml` file, where the `project.dependencies` list is used as per
+ [PEP621](https://peps.python.org/pep-0621/).
src: file containing inputs to dependency resolution. If not specified,
defaults to `pyproject.toml`. Supported formats are:
* a requirements text file, usually named `requirements.in`
@@ -63,7 +69,7 @@
generate_hashes: whether to put hashes in the requirements_txt file.
py_binary: the py_binary rule to be used.
py_test: the py_test rule to be used.
- requirements_in: file expressing desired dependencies. Deprecated, use src instead.
+ requirements_in: file expressing desired dependencies. Deprecated, use src or srcs instead.
requirements_txt: result of "compiling" the requirements.in file.
requirements_linux: File of linux specific resolve output to check validate if requirement.in has changes.
requirements_darwin: File of darwin specific resolve output to check validate if requirement.in has changes.
@@ -72,10 +78,15 @@
visibility: passed to both the _test and .update rules.
**kwargs: other bazel attributes passed to the "_test" rule.
"""
- if requirements_in and src:
- fail("Only one of 'src' and 'requirements_in' attributes can be used")
+ if len([x for x in [srcs, src, requirements_in] if x != None]) > 1:
+ fail("At most one of 'srcs', 'src', and 'requirements_in' attributes may be provided")
+
+ if requirements_in:
+ srcs = [requirements_in]
+ elif src:
+ srcs = [src]
else:
- src = requirements_in or src or "pyproject.toml"
+ srcs = srcs or ["pyproject.toml"]
requirements_txt = name + ".txt" if requirements_txt == None else requirements_txt
@@ -88,7 +99,7 @@
visibility = visibility,
)
- data = [name, requirements_txt, src] + [f for f in (requirements_linux, requirements_darwin, requirements_windows) if f != None]
+ data = [name, requirements_txt] + srcs + [f for f in (requirements_linux, requirements_darwin, requirements_windows) if f != None]
# Use the Label constructor so this is expanded in the context of the file
# where it appears, which is to say, in @rules_python
@@ -96,8 +107,7 @@
loc = "$(rlocationpath {})"
- args = [
- loc.format(src),
+ args = ["--src=%s" % loc.format(src) for src in srcs] + [
loc.format(requirements_txt),
"//%s:%s.update" % (native.package_name(), name),
"--resolver=backtracking",
@@ -144,12 +154,15 @@
"visibility": visibility,
}
- # cheap way to detect the bazel version
- _bazel_version_4_or_greater = "propeller_optimize" in dir(native)
-
- # Bazel 4.0 added the "env" attribute to py_test/py_binary
- if _bazel_version_4_or_greater:
- attrs["env"] = kwargs.pop("env", {})
+ # setuptools (the default python build tool) attempts to find user
+ # configuration in the user's home direcotory. This seems to work fine on
+ # linux and macOS, but fails on Windows, so we conditionally provide a fake
+ # USERPROFILE env variable to allow setuptools to proceed without finding
+ # user-provided configuration.
+ kwargs["env"] = select({
+ "@@platforms//os:windows": {"USERPROFILE": "Z:\\FakeSetuptoolsHomeDirectoryHack"},
+ "//conditions:default": {},
+ }) | kwargs.get("env", {})
py_binary(
name = name + ".update",
diff --git a/tests/multiple_inputs/BUILD.bazel b/tests/multiple_inputs/BUILD.bazel
new file mode 100644
index 0000000..3e3cab8
--- /dev/null
+++ b/tests/multiple_inputs/BUILD.bazel
@@ -0,0 +1,30 @@
+load("@rules_python//python:pip.bzl", "compile_pip_requirements")
+
+compile_pip_requirements(
+ name = "multiple_requirements_in",
+ srcs = [
+ "requirements_1.in",
+ "requirements_2.in",
+ ],
+ requirements_txt = "multiple_requirements_in.txt",
+)
+
+compile_pip_requirements(
+ name = "multiple_pyproject_toml",
+ srcs = [
+ "a/pyproject.toml",
+ "b/pyproject.toml",
+ ],
+ requirements_txt = "multiple_pyproject_toml.txt",
+)
+
+compile_pip_requirements(
+ name = "multiple_inputs",
+ srcs = [
+ "a/pyproject.toml",
+ "b/pyproject.toml",
+ "requirements_1.in",
+ "requirements_2.in",
+ ],
+ requirements_txt = "multiple_inputs.txt",
+)
diff --git a/tests/multiple_inputs/README.md b/tests/multiple_inputs/README.md
new file mode 100644
index 0000000..7b6bade
--- /dev/null
+++ b/tests/multiple_inputs/README.md
@@ -0,0 +1,3 @@
+# multiple_inputs
+
+Test that `compile_pip_requirements` works as intended when using more than one input file.
diff --git a/tests/multiple_inputs/a/pyproject.toml b/tests/multiple_inputs/a/pyproject.toml
new file mode 100644
index 0000000..91efec3
--- /dev/null
+++ b/tests/multiple_inputs/a/pyproject.toml
@@ -0,0 +1,5 @@
+[project]
+name = "multiple_inputs_1"
+version = "0.0.0"
+
+dependencies = ["urllib3"]
diff --git a/tests/multiple_inputs/b/pyproject.toml b/tests/multiple_inputs/b/pyproject.toml
new file mode 100644
index 0000000..a461f4e
--- /dev/null
+++ b/tests/multiple_inputs/b/pyproject.toml
@@ -0,0 +1,5 @@
+[project]
+name = "multiple_inputs_2"
+version = "0.0.0"
+
+dependencies = ["attrs"]
diff --git a/tests/multiple_inputs/multiple_inputs.txt b/tests/multiple_inputs/multiple_inputs.txt
new file mode 100644
index 0000000..a036c3f
--- /dev/null
+++ b/tests/multiple_inputs/multiple_inputs.txt
@@ -0,0 +1,18 @@
+#
+# This file is autogenerated by pip-compile with Python 3.11
+# by the following command:
+#
+# bazel run //tests/multiple_inputs:multiple_inputs.update
+#
+attrs==23.1.0 \
+ --hash=sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04 \
+ --hash=sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015
+ # via
+ # -r tests/multiple_inputs/requirements_2.in
+ # multiple_inputs_2 (tests/multiple_inputs/b/pyproject.toml)
+urllib3==2.0.7 \
+ --hash=sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84 \
+ --hash=sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e
+ # via
+ # -r tests/multiple_inputs/requirements_1.in
+ # multiple_inputs_1 (tests/multiple_inputs/a/pyproject.toml)
diff --git a/tests/multiple_inputs/multiple_pyproject_toml.txt b/tests/multiple_inputs/multiple_pyproject_toml.txt
new file mode 100644
index 0000000..b8af28a
--- /dev/null
+++ b/tests/multiple_inputs/multiple_pyproject_toml.txt
@@ -0,0 +1,14 @@
+#
+# This file is autogenerated by pip-compile with Python 3.11
+# by the following command:
+#
+# bazel run //tests/multiple_inputs:multiple_pyproject_toml.update
+#
+attrs==23.1.0 \
+ --hash=sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04 \
+ --hash=sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015
+ # via multiple_inputs_2 (tests/multiple_inputs/b/pyproject.toml)
+urllib3==2.0.7 \
+ --hash=sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84 \
+ --hash=sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e
+ # via multiple_inputs_1 (tests/multiple_inputs/a/pyproject.toml)
diff --git a/tests/multiple_inputs/multiple_requirements_in.txt b/tests/multiple_inputs/multiple_requirements_in.txt
new file mode 100644
index 0000000..63edfe9
--- /dev/null
+++ b/tests/multiple_inputs/multiple_requirements_in.txt
@@ -0,0 +1,14 @@
+#
+# This file is autogenerated by pip-compile with Python 3.11
+# by the following command:
+#
+# bazel run //tests/multiple_inputs:multiple_requirements_in.update
+#
+attrs==23.1.0 \
+ --hash=sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04 \
+ --hash=sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015
+ # via -r tests/multiple_inputs/requirements_2.in
+urllib3==2.0.7 \
+ --hash=sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84 \
+ --hash=sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e
+ # via -r tests/multiple_inputs/requirements_1.in
diff --git a/tests/multiple_inputs/requirements_1.in b/tests/multiple_inputs/requirements_1.in
new file mode 100644
index 0000000..a42590b
--- /dev/null
+++ b/tests/multiple_inputs/requirements_1.in
@@ -0,0 +1 @@
+urllib3
diff --git a/tests/multiple_inputs/requirements_2.in b/tests/multiple_inputs/requirements_2.in
new file mode 100644
index 0000000..04cb102
--- /dev/null
+++ b/tests/multiple_inputs/requirements_2.in
@@ -0,0 +1 @@
+attrs