Skip to content

Commit

Permalink
Allow transitive URLs via recursive extras (#4155)
Browse files Browse the repository at this point in the history
## Summary

Closes #4152.
  • Loading branch information
charliermarsh authored Jun 8, 2024
1 parent c46fa74 commit ac1ddf5
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 30 deletions.
88 changes: 58 additions & 30 deletions crates/uv-requirements/src/lookahead.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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)?,
}))
}
38 changes: 38 additions & 0 deletions crates/uv/tests/pip_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}

0 comments on commit ac1ddf5

Please sign in to comment.