diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 5af4377b9..0b72014a8 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -2371,7 +2371,13 @@ impl Package { let sdist = match &self.id.source { Source::Path(path) => { // A direct path source can also be a wheel, so validate the extension. - let DistExtension::Source(ext) = DistExtension::from_path(path)? else { + let DistExtension::Source(ext) = DistExtension::from_path(path).map_err(|err| { + LockErrorKind::MissingExtension { + id: self.id.clone(), + err, + } + })? + else { return Ok(None); }; let install_path = absolute_path(workspace_root, path)?; @@ -2444,7 +2450,14 @@ impl Package { } Source::Direct(url, direct) => { // A direct URL source can also be a wheel, so validate the extension. - let DistExtension::Source(ext) = DistExtension::from_path(url.as_ref())? else { + let DistExtension::Source(ext) = + DistExtension::from_path(url.base_str()).map_err(|err| { + LockErrorKind::MissingExtension { + id: self.id.clone(), + err, + } + })? + else { return Ok(None); }; let location = url.to_url().map_err(LockErrorKind::InvalidUrl)?; @@ -2483,7 +2496,12 @@ impl Package { .ok_or_else(|| LockErrorKind::MissingFilename { id: self.id.clone(), })?; - let ext = SourceDistExtension::from_path(filename.as_ref())?; + let ext = SourceDistExtension::from_path(filename.as_ref()).map_err(|err| { + LockErrorKind::MissingExtension { + id: self.id.clone(), + err, + } + })?; let file = Box::new(uv_distribution_types::File { dist_info_metadata: false, filename: SmallString::from(filename), @@ -2535,7 +2553,12 @@ impl Package { .ok_or_else(|| LockErrorKind::MissingFilename { id: self.id.clone(), })?; - let ext = SourceDistExtension::from_path(filename.as_ref())?; + let ext = SourceDistExtension::from_path(filename.as_ref()).map_err(|err| { + LockErrorKind::MissingExtension { + id: self.id.clone(), + err, + } + })?; let file = Box::new(uv_distribution_types::File { dist_info_metadata: false, filename: SmallString::from(filename), @@ -5222,8 +5245,13 @@ enum LockErrorKind { ), /// An error that occurs when the extension can't be determined /// for a given wheel or source distribution. - #[error("Failed to parse file extension; expected one of: {0}")] - MissingExtension(#[from] ExtensionError), + #[error("Failed to parse file extension for `{id}`; expected one of: {err}", id = id.cyan())] + MissingExtension { + /// The filename that was expected to have an extension. + id: PackageId, + /// The list of valid extensions that were expected. + err: ExtensionError, + }, /// Failed to parse a Git source URL. #[error("Failed to parse Git URL")] InvalidGitSourceUrl( diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index e6fe831d3..9da2fb62a 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -9943,3 +9943,32 @@ fn sync_required_environment_hint() -> Result<()> { Ok(()) } + +#[test] +fn sync_url_with_query_parameters() -> Result<()> { + let context = TestContext::new("3.13").with_exclude_newer("2025-03-24T19:00:00Z"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(r#" + [project] + name = "example" + version = "0.1.0" + requires-python = ">=3.13.2" + dependencies = ["source-distribution @ https://files.pythonhosted.org/packages/1f/e5/5b016c945d745f8b108e759d428341488a6aee8f51f07c6c4e33498bb91f/source_distribution-0.0.3.tar.gz?foo=bar"] + "# + )?; + + uv_snapshot!(context.filters(), context.sync(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + source-distribution==0.0.3 (from https://files.pythonhosted.org/packages/1f/e5/5b016c945d745f8b108e759d428341488a6aee8f51f07c6c4e33498bb91f/source_distribution-0.0.3.tar.gz?foo=bar) + "); + + Ok(()) +}