Skip to content

Commit 5244255

Browse files
committed
reimplement the expand_template in a custom rule to better handle selects in the output attr
1 parent 47e7ccd commit 5244255

File tree

3 files changed

+91
-32
lines changed

3 files changed

+91
-32
lines changed

python/uv/private/BUILD.bazel

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,10 @@ bzl_library(
4343
":toolchain_types_bzl",
4444
"//python:py_binary_bzl",
4545
"//python/private:bzlmod_enabled_bzl",
46+
"//python/private:full_version_bzl",
4647
"//python/private:toolchain_types_bzl",
4748
"@bazel_skylib//lib:shell",
48-
"@bazel_skylib//rules:expand_template",
49+
"@pythons_hub//:versions_bzl",
4950
],
5051
)
5152

python/uv/private/lock.bzl

Lines changed: 87 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,17 @@
1616
"""
1717

1818
load("@bazel_skylib//lib:shell.bzl", "shell")
19-
load("@bazel_skylib//rules:expand_template.bzl", "expand_template")
19+
load("@pythons_hub//:versions.bzl", "DEFAULT_PYTHON_VERSION", "MINOR_MAPPING")
2020
load("//python:py_binary.bzl", "py_binary")
2121
load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility
22+
load("//python/private:full_version.bzl", "full_version")
2223
load("//python/private:toolchain_types.bzl", "TARGET_TOOLCHAIN_TYPE") # buildifier: disable=bzl-visibility
2324
load(":toolchain_types.bzl", "UV_TOOLCHAIN_TYPE")
2425

2526
visibility(["//..."])
2627

28+
_PYTHON_VERSION_FLAG = "//python/config_settings:python_version"
29+
2730
_RunLockInfo = provider(
2831
doc = "",
2932
fields = {
@@ -72,7 +75,10 @@ def _args(ctx):
7275

7376
def _lock_impl(ctx):
7477
srcs = ctx.files.srcs
75-
output = ctx.actions.declare_file(ctx.label.name + ".out")
78+
output = ctx.actions.declare_file("{}.{}.out".format(
79+
ctx.label.name,
80+
ctx.attr.python_version.replace(".", "_"),
81+
))
7682

7783
toolchain_info = ctx.toolchains[UV_TOOLCHAIN_TYPE]
7884
uv = toolchain_info.uv_toolchain_info.uv[DefaultInfo].files_to_run.executable
@@ -87,10 +93,9 @@ def _lock_impl(ctx):
8793
"--no-python-downloads",
8894
"--no-cache",
8995
])
90-
args.add("--custom-compile-command", "bazel run //{}:{}".format(
91-
ctx.label.package,
92-
ctx.attr.update_target,
93-
))
96+
pkg = ctx.label.package
97+
update_target = ctx.attr.update_target
98+
args.add("--custom-compile-command", "bazel run //{}:{}".format(pkg, update_target))
9499
if ctx.attr.generate_hashes:
95100
args.add("--generate-hashes")
96101
if not ctx.attr.strip_extras:
@@ -151,6 +156,28 @@ def _lock_impl(ctx):
151156
),
152157
]
153158

159+
def _transition_impl(input_settings, attr):
160+
settings = {
161+
_PYTHON_VERSION_FLAG: input_settings[_PYTHON_VERSION_FLAG],
162+
}
163+
if attr.python_version:
164+
# FIXME @aignas 2025-03-20: using `full_version` is a workaround for a bug in
165+
# how we order toolchains in bazel. If I set the `python_version` flag
166+
# to `3.12`, I would expect the latest version to be selected, i.e. the
167+
# one that is in MINOR_MAPPING, but it seems that 3.12.0 is selected,
168+
# because of how the targets are ordered.
169+
settings[_PYTHON_VERSION_FLAG] = full_version(
170+
version = attr.python_version,
171+
minor_mapping = MINOR_MAPPING,
172+
)
173+
return settings
174+
175+
_python_version_transition = transition(
176+
implementation = _transition_impl,
177+
inputs = [_PYTHON_VERSION_FLAG],
178+
outputs = [_PYTHON_VERSION_FLAG],
179+
)
180+
154181
_lock = rule(
155182
implementation = _lock_impl,
156183
doc = """\
@@ -182,8 +209,16 @@ modifications and the locking is not done from scratch.
182209
doc = "Public, see the docs in the macro.",
183210
default = True,
184211
),
212+
"output": attr.string(
213+
doc = "Public, see the docs in the macro.",
214+
mandatory = True,
215+
),
185216
"python_version": attr.string(
186217
doc = "Public, see the docs in the macro.",
218+
default = full_version(
219+
version = DEFAULT_PYTHON_VERSION,
220+
minor_mapping = MINOR_MAPPING,
221+
),
187222
),
188223
"srcs": attr.label_list(
189224
mandatory = True,
@@ -200,12 +235,16 @@ modifications and the locking is not done from scratch.
200235
The string to input for the 'uv pip compile'.
201236
""",
202237
),
238+
"_allowlist_function_transition": attr.label(
239+
default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
240+
),
203241
},
204242
toolchains = [
205243
UV_TOOLCHAIN_TYPE,
206244
# FIXME @aignas 2025-03-17: should this be instead EXEC_TOOLCHAIN_TYPE?
207245
TARGET_TOOLCHAIN_TYPE,
208246
],
247+
cfg = _python_version_transition,
209248
)
210249

211250
def _lock_run_impl(ctx):
@@ -293,17 +332,45 @@ def _maybe_file(path):
293332

294333
return None
295334

335+
def _expand_template_impl(ctx):
336+
pkg = ctx.label.package
337+
update_src = ctx.actions.declare_file(ctx.attr.update_target + ".py")
338+
ctx.actions.expand_template(
339+
template = ctx.files._template[0],
340+
substitutions = {
341+
"{{dst}}": "{}/{}".format(pkg, ctx.attr.output),
342+
"{{src}}": "{}".format(ctx.files.src[0].short_path),
343+
"{{update_target}}": "//{}:{}".format(pkg, ctx.attr.update_target),
344+
},
345+
output = update_src,
346+
)
347+
return DefaultInfo(files = depset([update_src]))
348+
349+
_expand_template = rule(
350+
implementation = _expand_template_impl,
351+
attrs = {
352+
"output": attr.string(mandatory = True),
353+
"src": attr.label(mandatory = True),
354+
"update_target": attr.string(mandatory = True),
355+
"_template": attr.label(
356+
default = "//python/uv/private:lock_copier.py",
357+
allow_single_file = True,
358+
),
359+
},
360+
doc = "Expand the template for the update script allowing us to use `select` statements in the {attr}`output` attribute.",
361+
)
362+
296363
def lock(
297364
*,
298365
name,
366+
srcs,
367+
out,
299368
args = [],
300369
build_constraints = [],
301370
constraints = [],
302371
env = None,
303372
generate_hashes = True,
304-
out,
305373
python_version = None,
306-
srcs,
307374
strip_extras = False,
308375
**kwargs):
309376
"""Pin the requirements based on the src files.
@@ -344,7 +411,6 @@ def lock(
344411
"""
345412
update_target = "{}.update".format(name)
346413
locker_target = "{}.run".format(name)
347-
template_target = "_{}.update_template".format(name)
348414

349415
# Check if the output file already exists, if yes, first copy it to the
350416
# output file location in order to make `uv` not change the requirements if
@@ -372,6 +438,7 @@ def lock(
372438
strip_extras = strip_extras,
373439
target_compatible_with = target_compatible_with,
374440
update_target = update_target,
441+
output = out,
375442
tags = [
376443
"local",
377444
"manual",
@@ -393,28 +460,19 @@ def lock(
393460
tags = ["manual"],
394461
)
395462

396-
# Write a script that can be used for updating the in-tree version of the
397-
# requirements file
398-
pkg = native.package_name()
399-
expand_template(
400-
name = template_target,
401-
out = update_target + ".py",
402-
template = "//python/uv/private:lock_copier.py",
403-
substitutions = {
404-
"{{dst}}": "{}/{}".format(pkg, out),
405-
"{{src}}": "{}/{}.out".format(pkg, name),
406-
"{{update_target}}": "//{}:{}".format(pkg, update_target),
407-
},
408-
tags = ["manual"],
463+
# FIXME @aignas 2025-03-20: is it possible to extend `py_binary` so that the
464+
# srcs are generated before `py_binary` is run? I found that
465+
# `ctx.files.srcs` usage in the base implementation is making it difficult.
466+
_expand_template(
467+
name = name + "_cp",
468+
src = name,
469+
output = out,
470+
update_target = update_target,
409471
)
410472

411-
# TODO @aignas 2025-03-17: use a custom `py_binary` rule and
412-
# `ctx.actions.expand_template` instead of the bazel skylib rule. This
413-
# would make it possible to avoid the extra target.
414473
py_binary(
415474
name = update_target,
416-
srcs = [update_target + ".py"],
417-
data = [name] + ([] if not maybe_out else [maybe_out]),
418-
python_version = python_version,
419-
**kwargs
475+
srcs = [name + "_cp"],
476+
data = [name] + ([maybe_out] if maybe_out else []),
477+
tags = ["manual"],
420478
)

tests/uv/lock/lock_run_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def test_requirements_updating_for_the_first_time(self):
4242
# Then
4343
self.assertEqual(0, output.returncode, output.stderr)
4444
self.assertIn(
45-
"cp <bazel-sandbox>/tests/uv/lock/requirements_new_file.out",
45+
"cp <bazel-sandbox>/tests/uv/lock/requirements_new_file",
4646
output.stdout.decode("utf-8"),
4747
)
4848
self.assertTrue(want_path.exists(), "The path should exist after the test")
@@ -81,7 +81,7 @@ def test_requirements_updating(self):
8181
# Then
8282
self.assertEqual(0, output.returncode)
8383
self.assertIn(
84-
"cp <bazel-sandbox>/tests/uv/lock/requirements.out",
84+
"cp <bazel-sandbox>/tests/uv/lock/requirements",
8585
output.stdout.decode("utf-8"),
8686
)
8787
self.assertEqual(want_path.read_text(), want_text)

0 commit comments

Comments
 (0)