Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions news/4179.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
The `lock()` macro's `.run` target uses the `uv` cache and honors
`UV_CACHE_DIR` and `UV_NO_CACHE`. Pass `--refresh` to refresh cached data or
`--no-cache` to disable caching. Lock build actions, including those used by
`.update`, disable the `uv` cache.
([#4179](https://github.com/bazel-contrib/rules_python/issues/4179))
14 changes: 12 additions & 2 deletions python/uv/private/lock.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,9 @@ def _common_lock(ctx, locker):
srcs, output_filename, mnemonic, progress_message = locker(args, output)

args.add("--no-python-downloads")
args.add("--no-cache")

# Build actions must not depend on the host cache but `.run` may reuse it.
args.add_run_shell("--no-cache")

project = ctx.attr.project
if not project:
Expand Down Expand Up @@ -607,7 +609,11 @@ def lock(
to the same command that would be run in the `name` action. This will
update the source copy of the requirements file. You can customize the
args via the command line, but it requires being able to run `uv` (and
possibly `python`) directly on your host.
possibly `python`) directly on your host. This target uses the `uv`
cache and inherits its cache settings from the environment, including
`UV_CACHE_DIR` and `UV_NO_CACHE`. Pass `--refresh` to refresh cached
data or `--no-cache` to disable caching. Build actions, including those
used by `name.update`, disable the `uv` cache.
- `name.update`: a target that can be run to update the source-tree version
of the requirements lock file. The output can be fed to the
{obj}`pip.parse` bzlmod extension tag class. Note, you can use
Expand All @@ -622,6 +628,10 @@ def lock(
All of the targets have `manual` tags as locking results cannot be cached.
:::

:::{versionchanged} VERSION_NEXT_PATCH
The `name.run` target uses the `uv` cache by default.
:::

Args:
name: {type}`str` The prefix of all targets created by this macro.
srcs: {type}`list[Label]` The sources that will be used. Add all of the
Expand Down
7 changes: 7 additions & 0 deletions tests/integration/uv_lock/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,10 @@ diff_test(
file1 = ":requirements",
file2 = ":requirements.txt",
)

lock(
name = "project_lock",
srcs = ["pyproject.toml"],
out = "uv.lock",
tags = ["no-remote-exec"],
)
44 changes: 44 additions & 0 deletions tests/integration/uv_lock_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,50 @@ def _assert_lock_file(self, result):
self.assertIn("my-local-pkg", contents)
self.assertIn("--hash=sha256:", contents)

def _run_cached_lock(self, target, *args, check=True):
return self.run_bazel(
"run",
target,
"--",
"--default-index=" + self.server_url + "/simple/",
*args,
check=check,
)

def test_run_reuses_cache_offline(self):
for target, filename in (
("//:requirements.run", "requirements.txt"),
(
"//:project_lock.run",
"bazel-bin/project_lock.run.runfiles/_main/uv.lock",
),
):
with self.subTest(target=target):
self.bazel_env["UV_CACHE_DIR"] = str(
self.test_tmp_dir / (Path(filename).name + ".cache")
)
output = self.repo_root / filename
output.unlink(missing_ok=True)
cold = self._run_cached_lock(target, "--offline", check=False)
self.assertNotEqual(cold.exit_code, 0, cold.describe())
self._run_cached_lock(target)
expected = output.read_text()
self.assertIn("my-local-pkg", expected)
output.unlink()
self._run_cached_lock(target, "--offline")
self.assertEqual(expected, output.read_text())
output.unlink()
bypass = self._run_cached_lock(
target, "--offline", "--no-cache", check=False
)
self.assertNotEqual(bypass.exit_code, 0, bypass.describe())
self.bazel_env["UV_NO_CACHE"] = "true"
try:
bypass_env = self._run_cached_lock(target, "--offline", check=False)
self.assertNotEqual(bypass_env.exit_code, 0, bypass_env.describe())
finally:
del self.bazel_env["UV_NO_CACHE"]

def test_lock_update_with_custom_index(self):
self._assert_server_requires_auth()
self._assert_simple_api_sha256()
Expand Down
3 changes: 3 additions & 0 deletions tests/uv/lock/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
load(":cache_tests.bzl", "cache_test_suite")
load(":lock_tests.bzl", "lock_test_suite")
load(":uv_lock_to_requirements_tests.bzl", "uv_lock_to_requirements_test_suite")

Expand All @@ -13,3 +14,5 @@ lock_test_suite(
uv_lock_to_requirements_test_suite(
name = "uv_lock_to_requirements_tests",
)

cache_test_suite(name = "cache_tests")
84 changes: 84 additions & 0 deletions tests/uv/lock/cache_tests.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Cache policy tests for build actions and runnable lock targets."""

load("@rules_testing//lib:analysis_test.bzl", "analysis_test")
load("@rules_testing//lib:test_suite.bzl", "test_suite")
load("//python/uv:lock.bzl", "lock")
load("//tests/support/platforms:platforms.bzl", "platform_targets")

def _cache_tests(name, is_windows, extension):
config_settings = {
"//command_line_option:platforms": [
platform_targets.WINDOWS_X86_64 if is_windows else platform_targets.LINUX_X86_64,
],
}
for no_cache in [False, True]:
subject = name + ("_no_cache" if no_cache else "_default")
lock(
name = subject,
srcs = ["testdata/pyproject.toml"],
args = ["--no-cache"] if no_cache else [],
out = subject + extension,
)
analysis_test(
name = subject + "_build_test",
impl = _test_windows_build_impl if is_windows else _test_build_impl,
target = subject,
config_settings = config_settings,
)
analysis_test(
name = subject + "_run_test",
impl = _test_no_cache_run_impl if no_cache else _test_run_impl,
target = subject + ".run",
config_settings = config_settings,
)
native.test_suite(
name = name,
tests = [
name + mode + kind
for mode in ["_default", "_no_cache"]
for kind in ["_build_test", "_run_test"]
],
)

def _test_build_impl(env, target):
output = target[DefaultInfo].files.to_list()[0]
env.expect.that_target(target).action_generating(output.short_path).argv().contains("--no-cache")

def _test_windows_build_impl(env, target):
env.expect.that_target(target).action_generating(
"{package}/{name}_lock.bat",
).content().contains("--no-cache")

def _run_script(env, target):
executable = target[DefaultInfo].files_to_run.executable
return env.expect.that_target(target).action_generating(executable.short_path).content()

def _test_run_impl(env, target):
_run_script(env, target).split("--no-cache").has_size(1)

def _test_no_cache_run_impl(env, target):
_run_script(env, target).contains("--no-cache")

def _test_requirements_cache(name):
_cache_tests(name, is_windows = False, extension = ".txt")

def _test_uv_lock_cache(name):
_cache_tests(name, is_windows = False, extension = ".lock")

def _test_windows_requirements_cache(name):
_cache_tests(name, is_windows = True, extension = ".txt")

def _test_windows_uv_lock_cache(name):
_cache_tests(name, is_windows = True, extension = ".lock")

def cache_test_suite(name):
"""Check cache defaults and explicit `--no-cache` for both lock formats."""
test_suite(
name = name,
tests = [
_test_requirements_cache,
_test_uv_lock_cache,
_test_windows_requirements_cache,
_test_windows_uv_lock_cache,
],
)
3 changes: 2 additions & 1 deletion tests/uv/lock/lock_run_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class LockTests(unittest.TestCase):
def _subprocess_env(self, workspace_dir: Path) -> dict[str, str]:
env = {
"BUILD_WORKSPACE_DIRECTORY": str(workspace_dir),
"UV_CACHE_DIR": str(workspace_dir / "uv-cache"),
}
# Inherit specific env vars needed for finding runfiles on Windows
for key in (
Expand Down Expand Up @@ -249,7 +250,7 @@ def test_requirements_run_script_has_expected_args(self):
self.assertIn("--generate-hashes", content)
self.assertIn("--no-strip-extras", content)
self.assertIn("--no-python-downloads", content)
self.assertIn("--no-cache", content)
self.assertNotIn("--no-cache", content)
self.assertIn("--no-progress", content)
self.assertIn("--quiet", content)
self.assertIn("--output-file", content)
Expand Down
Loading