fix(pypi): handle revisions in uv.lock Git sources (#4086)
Git sources in `uv.lock` can include a revision in the URL query and
fragment. `pip.parse` currently includes those components in the derived
filename, which can produce an invalid Bazel repository name.
Derive the filename from only the URL path while preserving the complete
revision-bearing Git URL. Regression coverage verifies the filename is
clean
and the fetch URL remains unchanged.
Fixes #4084
diff --git a/news/4084.fixed.md b/news/4084.fixed.md
new file mode 100644
index 0000000..6e19ca3
--- /dev/null
+++ b/news/4084.fixed.md
@@ -0,0 +1,3 @@
+(pypi) Fixed {obj}`pip.parse` repository names for Git sources in `uv.lock`
+files by excluding URL query and fragment components
+([#4084](https://github.com/bazel-contrib/rules_python/issues/4084)).
diff --git a/python/private/pypi/parse_requirements.bzl b/python/private/pypi/parse_requirements.bzl
index 4874c7a..580989e 100644
--- a/python/private/pypi/parse_requirements.bzl
+++ b/python/private/pypi/parse_requirements.bzl
@@ -227,7 +227,11 @@
git_struct = None
if pkg.get("source", {}).get("git"):
url = pkg["source"]["git"]
- _, _, filename = url.rpartition("/")
+
+ # Keep the revision in the URL, but exclude it from the repository filename.
+ url_path, _, _ = url.partition("?")
+ url_path, _, _ = url_path.partition("#")
+ _, _, filename = url_path.rpartition("/")
git_struct = struct(
filename = filename,
url = url,
diff --git a/tests/pypi/parse_requirements/parse_requirements_tests.bzl b/tests/pypi/parse_requirements/parse_requirements_tests.bzl
index 54f9069..f6b9d7e 100644
--- a/tests/pypi/parse_requirements/parse_requirements_tests.bzl
+++ b/tests/pypi/parse_requirements/parse_requirements_tests.bzl
@@ -114,7 +114,7 @@
"uv_lock_foo_sha512": """{"package":[{"name":"foo","source":{"registry":"https://pypi.org/simple"},"version":"0.0.1","wheels":[{"hash":"sha512:deadbeef","url":"https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl"}]}]}""",
"uv_lock_foo_virtual": """{"package":[{"name":"foo","source":{"registry":"https://pypi.org/simple"},"version":"0.0.1","wheels":[{"hash":"sha256:deadbeef","url":"https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl"}]},{"name":"virtual-pkg","source":{"virtual":true},"version":"0.0.0"}]}""",
"uv_lock_foo_with_extras": """{"package":[{"name":"foo","provides-extras":["extra"],"source":{"registry":"https://pypi.org/simple"},"version":"0.0.1","wheels":[{"hash":"sha256:deadbeef","url":"https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl"}]}]}""",
- "uv_lock_git_vcs": """{"package":[{"name":"foo","source":{"git":"https://github.com/org/foo.git"},"version":"0.1.0"}]}""",
+ "uv_lock_git_vcs": """{"package":[{"name":"foo","source":{"git":"https://github.com/org/foo?rev=deadbeef#deadbeef"},"version":"0.1.0"}]}""",
"uv_lock_rules_python_pkg": """{"package":[{"name":"rules_python","source":{"registry":"https://pypi.org/simple"},"version":"0.0.1","wheels":[{"hash":"sha256:deadbeef","url":"https://files.pythonhosted.org/packages/rules_python-0.0.1-py3-none-any.whl"}]}]}""",
}
@@ -1321,7 +1321,7 @@
_tests.append(_test_uv_lock_cross_consistent)
def _test_uv_lock_vcs_entry(env):
- """Test that VCS entries in uv.lock are handled without crashing."""
+ """Test that VCS entry filenames exclude URL query and fragment components."""
got = parse_requirements(
uv_lock = "uv_lock_git_vcs",
)
@@ -1337,9 +1337,9 @@
extra_pip_args = [],
requirement_line = "foo==0.1.0",
target_platforms = ["linux_x86_64"],
- filename = "foo.git",
+ filename = "foo",
digest = "",
- url = "https://github.com/org/foo.git",
+ url = "https://github.com/org/foo?rev=deadbeef#deadbeef",
yanked = None,
),
],