feat(gazelle): support generic manifest lockfiles (#4117)

Adds a format-agnostic lockfiles argument to gazelle_python_manifest and
deprecates requirements.

This lets callers use uv.lock or any dependency lockfile as the manifest
integrity input. The existing build-file-generation example now
exercises lockfiles.

Fixes #4111.

The macro rejects specifying both arguments so existing requirements
users can migrate without ambiguity.
diff --git a/examples/build_file_generation/BUILD.bazel b/examples/build_file_generation/BUILD.bazel
index 27f0145..bfff6f3 100644
--- a/examples/build_file_generation/BUILD.bazel
+++ b/examples/build_file_generation/BUILD.bazel
@@ -40,11 +40,11 @@
 #   the manifest doesn't need to be updated
 gazelle_python_manifest(
     name = "gazelle_python_manifest",
-    modules_mapping = ":modules_map",
-    pip_repository_name = "pip",
     # NOTE: We can pass a list just like in `bzlmod_build_file_generation` example
     # but we keep a single target here for regression testing.
-    requirements = "//:requirements_lock.txt",
+    lockfiles = "//:requirements_lock.txt",
+    modules_mapping = ":modules_map",
+    pip_repository_name = "pip",
 )
 
 # Our gazelle target points to the python gazelle binary.
diff --git a/gazelle/docs/installation_and_usage.md b/gazelle/docs/installation_and_usage.md
index c3a0043..16bccde 100644
--- a/gazelle/docs/installation_and_usage.md
+++ b/gazelle/docs/installation_and_usage.md
@@ -113,16 +113,21 @@
     # python libraries are loaded in BUILD files.
     pip_repository_name = "pip",
 
-    # This should point to wherever we declare our python dependencies
-    # (the same as what we passed to the modules_mapping rule in WORKSPACE)
+    # This should point to the lockfile for our Python dependencies.
     # This argument is optional. If provided, the `.test` target is very
     # fast because it just has to check an integrity field. If not provided,
     # the integrity field is not added to the manifest which can help avoid
     # merge conflicts in large repos.
-    requirements = "//:requirements_lock.txt",
+    # `lockfiles` accepts one file or a list, including a `uv.lock` file.
+    lockfiles = "//:uv.lock",
 )
 ```
 
+:::{versionchanged} VERSION_NEXT_FEATURE
+The `requirements` argument is deprecated in favor of `lockfiles`, which can
+refer to any dependency lockfile, including `uv.lock`.
+:::
+
 Finally, you create a target that you'll invoke to run the Gazelle tool
 with the `rules_python` extension included. This typically goes in your root
 `/BUILD.bazel` file:
diff --git a/gazelle/manifest/defs.bzl b/gazelle/manifest/defs.bzl
index 71a4d57..c61618f 100644
--- a/gazelle/manifest/defs.bzl
+++ b/gazelle/manifest/defs.bzl
@@ -23,21 +23,28 @@
 def gazelle_python_manifest(
         name,
         modules_mapping,
-        requirements = [],
+        requirements = None,
+        lockfiles = [],
         pip_repository_name = "",
         pip_deps_repository_name = "",
         manifest = ":gazelle_python.yaml",
         **kwargs):
     """A macro for defining the updating and testing targets for the Gazelle manifest file.
 
+    :::{versionchanged} VERSION_NEXT_FEATURE
+    The `requirements` argument is deprecated in favor of `lockfiles`, which
+    can refer to any dependency lockfile, including `uv.lock`.
+    :::
+
     Args:
         name: the name used as a base for the targets.
         modules_mapping: the target for the generated modules_mapping.json file.
-        requirements: the target for the requirements.txt file or a list of
-            requirements files that will be concatenated before passing on to
-            the manifest generator. If unset, no integrity field is added to the
-            manifest, meaning testing it is just as expensive as generating it,
-            but modifying it is much less likely to result in a merge conflict.
+        requirements: deprecated. Use lockfiles instead.
+        lockfiles: the target for a lockfile or a list of lockfiles that will be
+            concatenated before passing on to the manifest generator. If unset,
+            no integrity field is added to the manifest, meaning testing it is
+            just as expensive as generating it, but modifying it is much less
+            likely to result in a merge conflict.
         pip_repository_name: the name of the pip_install or pip_repository target.
         pip_deps_repository_name: deprecated - the old {bzl:obj}`pip_parse` target name.
         manifest: the Gazelle manifest file.
@@ -57,6 +64,20 @@
         # This is a temporary check while pip_deps_repository_name exists as deprecated.
         fail("pip_repository_name must be set in //{}:{}".format(native.package_name(), name))
 
+    if requirements != None:
+        if lockfiles:
+            fail("only one of requirements or lockfiles may be set in //{}:{}".format(
+                native.package_name(),
+                name,
+            ))
+
+        # buildifier: disable=print
+        print("DEPRECATED requirements in //{}:{}. Please use lockfiles instead.".format(
+            native.package_name(),
+            name,
+        ))
+        lockfiles = requirements
+
     test_target = "{}.test".format(name)
     update_target = "{}.update".format(name)
     update_target_label = "//{}:{}".format(native.package_name(), update_target)
@@ -66,20 +87,19 @@
     manifest_generator = Label("//manifest/generate:generate")
     manifest_generator_hash = Label("//manifest/generate:generate_lib_sources_hash")
 
-    if requirements and type(requirements) == "list":
-        # This runs if requirements is a list or is unset (default value is empty list)
+    if lockfiles and type(lockfiles) == "list":
         native.genrule(
-            name = name + "_requirements_gen",
-            srcs = sorted(requirements),
-            outs = [name + "_requirements.txt"],
+            name = name + "_lockfiles_gen",
+            srcs = sorted(lockfiles),
+            outs = [name + "_lockfiles.txt"],
             cmd_bash = "cat $(SRCS) > $@",
             cmd_bat = "type $(SRCS) > $@",
         )
-        requirements = name + "_requirements_gen"
+        lockfiles = name + "_lockfiles_gen"
 
     update_args = [
         "--manifest-generator-hash=$(execpath {})".format(manifest_generator_hash),
-        "--requirements=$(execpath {})".format(requirements) if requirements else "--requirements=",
+        "--requirements=$(execpath {})".format(lockfiles) if lockfiles else "--requirements=",
         "--pip-repository-name={}".format(pip_repository_name),
         "--modules-mapping=$(execpath {})".format(modules_mapping),
         "--output=$(execpath {})".format(generated_manifest),
@@ -94,7 +114,7 @@
         srcs = [
             modules_mapping,
             manifest_generator_hash,
-        ] + ([requirements] if requirements else []),
+        ] + ([lockfiles] if lockfiles else []),
         tags = ["manual"],
     )
 
@@ -114,12 +134,12 @@
         **{k: v for k, v in kwargs.items() if k != "tags"}
     )
 
-    if requirements:
+    if lockfiles:
         attrs = {
             "env": {
                 "_TEST_MANIFEST": "$(rlocationpath {})".format(manifest),
                 "_TEST_MANIFEST_GENERATOR_HASH": "$(rlocationpath {})".format(manifest_generator_hash),
-                "_TEST_REQUIREMENTS": "$(rlocationpath {})".format(requirements),
+                "_TEST_REQUIREMENTS": "$(rlocationpath {})".format(lockfiles),
             },
             "size": "small",
         }
@@ -128,7 +148,7 @@
             srcs = [Label("//manifest/test:test.go")],
             data = [
                 manifest,
-                requirements,
+                lockfiles,
                 manifest_generator_hash,
             ],
             rundir = ".",
diff --git a/news/4111.changed.md b/news/4111.changed.md
new file mode 100644
index 0000000..bde84bc
--- /dev/null
+++ b/news/4111.changed.md
@@ -0,0 +1,2 @@
+(gazelle) Deprecate the `requirements` argument of `gazelle_python_manifest`
+in favor of the format-agnostic `lockfiles` argument, which supports `uv.lock`.