Skip to content

Commit

Permalink
Add support for passing non-default arguments to uv pip compile
Browse files Browse the repository at this point in the history
  • Loading branch information
mark-thm committed May 30, 2024
1 parent fdf8b3c commit a319984
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 49 deletions.
7 changes: 7 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ Run the compilation step with `bazel run //:generate_requirements_txt`.

This will automatically register a diff test with name `[name]_diff_test`.

Additionally, you can specify the following optional args:

- `python_platform`: the `uv pip compile` compatible `--python-platform` value to pass to uv
- `args`: override the default arguments passed to uv (`--generate-hashes`, `--emit-index-url` and `--no-strip-extras`)
- `tags`: tags to apply to the test target
- `target_compatible_with`: restrict targets to running on the specified Bazel platform

### create_venv

Create a virtual environment creation target:
Expand Down
90 changes: 73 additions & 17 deletions uv/private/pip.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,51 @@

_PY_TOOLCHAIN = "@bazel_tools//tools/python:toolchain_type"

_common_attrs = {
_COMMON_ATTRS = {
"requirements_in": attr.label(mandatory = True, allow_single_file = True),
"requirements_txt": attr.label(mandatory = True, allow_single_file = True),
"python_platform": attr.string(default = ""),
"python_platform": attr.string(),
"_uv": attr.label(default = "@multitool//tools/uv", executable = True, cfg = "exec"),
}

_DEFAULT_ARGS = [
"--generate-hashes",
"--emit-index-url",
"--no-strip-extras",
]

def _python_version(py_toolchain):
return "{major}.{minor}".format(
major = py_toolchain.py3_runtime.interpreter_version_info.major,
minor = py_toolchain.py3_runtime.interpreter_version_info.minor,
)

def _python_platform(maybe_python_platform):
if maybe_python_platform == "":
return ""
return "--python-platform {python_platform}".format(python_platform = maybe_python_platform)

def _uv_pip_compile(ctx, template, executable, generator_label):
def _uv_pip_compile(
ctx,
template,
executable,
generator_label,
args):
py_toolchain = ctx.toolchains[_PY_TOOLCHAIN]
compile_command = "bazel run {label}".format(label = str(generator_label))

cmd_args = []
cmd_args += args
cmd_args.append("--custom-compile-command='{compile_command}'".format(compile_command = compile_command))
cmd_args.append("--python-version={version}".format(version = _python_version(py_toolchain)))
if ctx.attr.python_platform:
cmd_args.append("--python-platform={platform}".format(platform = ctx.attr.python_platform))

ctx.actions.expand_template(
template = template,
output = executable,
substitutions = {
"{{uv}}": ctx.executable._uv.short_path,
"{{args}}": " \\\n ".join(cmd_args),
"{{requirements_in}}": ctx.file.requirements_in.short_path,
"{{requirements_txt}}": ctx.file.requirements_txt.short_path,
"{{compile_command}}": compile_command,
"{{resolved_python}}": py_toolchain.py3_runtime.interpreter.short_path,
"{{python_version}}": _python_version(py_toolchain),
"{{python_platform}}": _python_platform(ctx.attr.python_platform),
"{{label}}": str(generator_label),
},
)

Expand All @@ -47,14 +61,20 @@ def _runfiles(ctx):

def _pip_compile_impl(ctx):
executable = ctx.actions.declare_file(ctx.attr.name)
_uv_pip_compile(ctx, ctx.file._template, executable, ctx.label)
_uv_pip_compile(
ctx = ctx,
template = ctx.file._template,
executable = executable,
generator_label = ctx.label,
args = ctx.attr.args,
)
return DefaultInfo(
executable = executable,
runfiles = _runfiles(ctx),
)

_pip_compile = rule(
attrs = _common_attrs | {
attrs = _COMMON_ATTRS | {
"_template": attr.label(default = "//uv/private:pip_compile.sh", allow_single_file = True),
},
toolchains = [_PY_TOOLCHAIN],
Expand All @@ -64,14 +84,20 @@ _pip_compile = rule(

def _pip_compile_test_impl(ctx):
executable = ctx.actions.declare_file(ctx.attr.name)
_uv_pip_compile(ctx, ctx.file._template, executable, ctx.attr.generator_label.label)
_uv_pip_compile(
ctx = ctx,
template = ctx.file._template,
executable = executable,
generator_label = ctx.attr.generator_label.label,
args = ctx.attr.args,
)
return DefaultInfo(
executable = executable,
runfiles = _runfiles(ctx),
)

_pip_compile_test = rule(
attrs = _common_attrs | {
attrs = _COMMON_ATTRS | {
"generator_label": attr.label(mandatory = True),
"_template": attr.label(default = "//uv/private:pip_compile_test.sh", allow_single_file = True),
},
Expand All @@ -80,15 +106,44 @@ _pip_compile_test = rule(
test = True,
)

def pip_compile(name, requirements_in = None, requirements_txt = None, target_compatible_with = None, python_platform = None, tags = None):
def pip_compile(
name,
requirements_in = None,
requirements_txt = None,
target_compatible_with = None,
python_platform = None,
args = None,
tags = None):
"""
Produce targets to compile a requirements.in or pyproject.toml file into a requirements.txt file.
Args:
name: name of the primary compilation target.
requirements_in: (optional, default "//:requirements.in") a label for the requirements.in file.
requirements_txt: (optional, default "//:requirements.txt") a label for the requirements.txt file.
python_platform: (optional) a uv pip compile compatible value for --python-platform.
target_compatible_with: (optional) specify that a particular target is compatible only with certain
Bazel platforms.
args: (optional) override the default arguments passed to uv pip compile, default arguments are:
--generate-hashes (Include distribution hashes in the output file)
--emit-index-url (Include `--index-url` and `--extra-index-url` entries in the generated output file)
--no-strip-extras (Include extras in the output file)
tags: (optional) tags to apply to the generated test target
Targets produced by this macro are:
[name]: a runnable target that will use requirements_in to generate and overwrite requirements_txt
[name]_diff_test: a testable target that will check that requirements_txt is up to date with requirements_in
"""
tags = tags or []
args = args or _DEFAULT_ARGS

_pip_compile(
name = name,
requirements_in = requirements_in or "//:requirements.in",
requirements_txt = requirements_txt or "//:requirements.txt",
python_platform = python_platform or "",
python_platform = python_platform,
target_compatible_with = target_compatible_with,
args = args,
)

_pip_compile_test(
Expand All @@ -98,5 +153,6 @@ def pip_compile(name, requirements_in = None, requirements_txt = None, target_co
requirements_txt = requirements_txt or "//:requirements.txt",
python_platform = python_platform or "",
target_compatible_with = target_compatible_with,
args = args,
tags = ["requires-network"] + tags,
)
20 changes: 5 additions & 15 deletions uv/private/pip_compile.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,16 @@

set -euo pipefail

UV="{{uv}}"
PYTHON_PLATFORM="{{python_platform}}"
RESOLVED_PYTHON="{{resolved_python}}"
PYTHON_VERSION="{{python_version}}"
# inputs from Bazel
REQUIREMENTS_IN="{{requirements_in}}"
REQUIREMENTS_TXT="{{requirements_txt}}"
LABEL="{{label}}"

RESOLVED_PYTHON_BIN="$(dirname "$RESOLVED_PYTHON")"

# set resolved python to front of the path
RESOLVED_PYTHON_BIN="$(dirname "{{resolved_python}}")"
export PATH="$RESOLVED_PYTHON_BIN:$PATH"

$UV pip compile \
--generate-hashes \
--emit-index-url \
--no-strip-extras \
--custom-compile-command "bazel run $LABEL" \
--python-version="$PYTHON_VERSION" \
$(echo $PYTHON_PLATFORM) \
-o "$REQUIREMENTS_TXT" \
{{uv}} pip compile \
{{args}} \
--output-file="$REQUIREMENTS_TXT" \
"$REQUIREMENTS_IN" \
"$@"
25 changes: 8 additions & 17 deletions uv/private/pip_compile_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,30 @@

set -euo pipefail

UV="{{uv}}"
PYTHON_PLATFORM="{{python_platform}}"
RESOLVED_PYTHON="{{resolved_python}}"
PYTHON_VERSION="{{python_version}}"
# inputs from Bazel
REQUIREMENTS_IN="{{requirements_in}}"
REQUIREMENTS_TXT="{{requirements_txt}}"
LABEL="{{label}}"

RESOLVED_PYTHON_BIN="$(dirname "$RESOLVED_PYTHON")"
COMPILE_COMMAND="{{compile_command}}"

# set resolved python to front of the path
RESOLVED_PYTHON_BIN="$(dirname "{{resolved_python}}")"
export PATH="$RESOLVED_PYTHON_BIN:$PATH"

# make a writable copy of incoming requirements
cp "$REQUIREMENTS_TXT" __updated__

$UV pip compile \
{{uv}} pip compile \
--quiet \
--no-cache \
--generate-hashes \
--emit-index-url \
--no-strip-extras \
--custom-compile-command "bazel run ${LABEL}" \
--python-version="$PYTHON_VERSION" \
$(echo $PYTHON_PLATFORM) \
-o __updated__ \
{{args}} \
--output-file="__updated__" \
"$REQUIREMENTS_IN"

# check files match
DIFF="$(diff "$REQUIREMENTS_TXT" "__updated__" || true)"
if [ "$DIFF" != "" ]
then
echo >&2 "FAIL: $REQUIREMENTS_TXT is out-of-date. Run 'bazel run $LABEL' to update."
echo >&2 "FAIL: $REQUIREMENTS_TXT is out-of-date. Run '$compile_command' to update."
echo >&2 "$DIFF"
exit 1
fi

0 comments on commit a319984

Please sign in to comment.