Allow transitive URLs via recursive extras (#4155)

## Summary

Closes https://github.com/astral-sh/uv/issues/4152.
This commit is contained in:
Charlie Marsh 2024-06-07 18:10:18 -07:00 committed by GitHub
parent c46fa74e65
commit ac1ddf5e4e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 96 additions and 30 deletions

View file

@ -141,38 +141,11 @@ impl<'a, Context: BuildContext> LookaheadResolver<'a, Context> {
requirement: Requirement,
) -> Result<Option<RequestedRequirements>, LookaheadError> {
trace!("Performing lookahead for {requirement}");
// Determine whether the requirement represents a local distribution and convert to a
// buildable distribution.
let dist = match requirement.source {
RequirementSource::Registry { .. } => return Ok(None),
RequirementSource::Url {
subdirectory,
location,
url,
} => Dist::from_http_url(requirement.name, url, location, subdirectory)?,
RequirementSource::Git {
repository,
reference,
precise,
subdirectory,
url,
} => {
let mut git_url = GitUrl::new(repository, reference);
if let Some(precise) = precise {
git_url = git_url.with_precise(precise);
}
Dist::Source(SourceDist::Git(GitSourceDist {
name: requirement.name,
git: Box::new(git_url),
subdirectory,
url,
}))
}
RequirementSource::Path {
path,
url,
editable,
} => Dist::from_file_url(requirement.name, url, &path, editable)?,
let Some(dist) = required_dist(&requirement)? else {
return Ok(None);
};
// Fetch the metadata for the distribution.
@ -217,6 +190,21 @@ impl<'a, Context: BuildContext> LookaheadResolver<'a, Context> {
}
};
// Respect recursive extras by propagating the source extras to the dependencies.
let requires_dist = requires_dist
.into_iter()
.map(|dependency| {
if dependency.name == requirement.name {
Requirement {
source: requirement.source.clone(),
..dependency
}
} else {
dependency
}
})
.collect();
// Consider the dependencies to be "direct" if the requirement is a local source tree.
let direct = if let Dist::Source(source_dist) = &dist {
source_dist.as_path().is_some_and(std::path::Path::is_dir)
@ -232,3 +220,43 @@ impl<'a, Context: BuildContext> LookaheadResolver<'a, Context> {
)))
}
}
/// Convert a [`Requirement`] into a [`Dist`], if it is a direct URL.
fn required_dist(requirement: &Requirement) -> Result<Option<Dist>, distribution_types::Error> {
Ok(Some(match &requirement.source {
RequirementSource::Registry { .. } => return Ok(None),
RequirementSource::Url {
subdirectory,
location,
url,
} => Dist::from_http_url(
requirement.name.clone(),
url.clone(),
location.clone(),
subdirectory.clone(),
)?,
RequirementSource::Git {
repository,
reference,
precise,
subdirectory,
url,
} => {
let mut git_url = GitUrl::new(repository.clone(), reference.clone());
if let Some(precise) = precise {
git_url = git_url.with_precise(*precise);
}
Dist::Source(SourceDist::Git(GitSourceDist {
name: requirement.name.clone(),
git: Box::new(git_url),
subdirectory: subdirectory.clone(),
url: url.clone(),
}))
}
RequirementSource::Path {
path,
url,
editable,
} => Dist::from_file_url(requirement.name.clone(), url.clone(), path, *editable)?,
}))
}

View file

@ -5211,3 +5211,41 @@ fn tool_uv_sources_is_in_preview() -> Result<()> {
Ok(())
}
/// Allow transitive URLs via recursive extras.
#[test]
fn recursive_extra_transitive_url() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! {r#"
[project]
name = "project"
version = "0.0.0"
dependencies = []
[project.optional-dependencies]
all = [
"project[docs]",
]
docs = [
"iniconfig @ https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl",
]
"#})?;
uv_snapshot!(context.filters(), context.install()
.arg(".[all]"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Downloaded 2 packages in [TIME]
Installed 2 packages in [TIME]
+ iniconfig==2.0.0 (from https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl)
+ project==0.0.0 (from file://[TEMP_DIR]/)
"###);
Ok(())
}