Respect named --index and --default-index values in tool.uv.sources (#7910)

## Summary

If you pass a named index via the CLI, you can now reference it as a
named source. This required some surprisingly large refactors, since we
now need to be able to track whether a given index was provided on the
CLI vs. elsewhere (since, e.g., we don't want users to be able to
reference named indexes defined in global configuration).

Closes https://github.com/astral-sh/uv/issues/7899.
This commit is contained in:
Charlie Marsh 2024-10-15 16:56:24 -07:00 committed by GitHub
parent a034a8b83b
commit 2153c6ac0d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 370 additions and 133 deletions

View file

@ -6,7 +6,7 @@ use thiserror::Error;
use url::Url;
use uv_distribution_filename::DistExtension;
use uv_distribution_types::Index;
use uv_distribution_types::{Index, IndexLocations, Origin};
use uv_git::GitReference;
use uv_normalize::PackageName;
use uv_pep440::VersionSpecifiers;
@ -20,7 +20,7 @@ use uv_workspace::Workspace;
pub struct LoweredRequirement(Requirement);
#[derive(Debug, Clone, Copy)]
enum Origin {
enum RequirementOrigin {
/// The `tool.uv.sources` were read from the project.
Project,
/// The `tool.uv.sources` were read from the workspace root.
@ -35,15 +35,16 @@ impl LoweredRequirement {
project_dir: &'data Path,
project_sources: &'data BTreeMap<PackageName, Sources>,
project_indexes: &'data [Index],
locations: &'data IndexLocations,
workspace: &'data Workspace,
lower_bound: LowerBound,
) -> impl Iterator<Item = Result<LoweredRequirement, LoweringError>> + 'data {
) -> impl Iterator<Item = Result<Self, LoweringError>> + 'data {
let (source, origin) = if let Some(source) = project_sources.get(&requirement.name) {
(Some(source), Origin::Project)
(Some(source), RequirementOrigin::Project)
} else if let Some(source) = workspace.sources().get(&requirement.name) {
(Some(source), Origin::Workspace)
(Some(source), RequirementOrigin::Workspace)
} else {
(None, Origin::Project)
(None, RequirementOrigin::Project)
};
let source = source.cloned();
@ -155,16 +156,14 @@ impl LoweredRequirement {
Source::Registry { index, marker } => {
// Identify the named index from either the project indexes or the workspace indexes,
// in that order.
let Some(index) = project_indexes
.iter()
let Some(index) = locations
.indexes()
.filter(|index| matches!(index.origin, Some(Origin::Cli)))
.chain(project_indexes.iter())
.chain(workspace.indexes().iter())
.find(|Index { name, .. }| {
name.as_ref().is_some_and(|name| *name == index)
})
.or_else(|| {
workspace.indexes().iter().find(|Index { name, .. }| {
name.as_ref().is_some_and(|name| *name == index)
})
})
.map(|Index { url: index, .. }| index.clone())
else {
return Err(LoweringError::MissingIndex(
@ -260,7 +259,8 @@ impl LoweredRequirement {
dir: &'data Path,
sources: &'data BTreeMap<PackageName, Sources>,
indexes: &'data [Index],
) -> impl Iterator<Item = Result<LoweredRequirement, LoweringError>> + 'data {
locations: &'data IndexLocations,
) -> impl Iterator<Item = Result<Self, LoweringError>> + 'data {
let source = sources.get(&requirement.name).cloned();
let Some(source) = source else {
@ -332,7 +332,7 @@ impl LoweredRequirement {
}
let source = path_source(
PathBuf::from(path),
Origin::Project,
RequirementOrigin::Project,
dir,
dir,
editable.unwrap_or(false),
@ -340,8 +340,10 @@ impl LoweredRequirement {
(source, marker)
}
Source::Registry { index, marker } => {
let Some(index) = indexes
.iter()
let Some(index) = locations
.indexes()
.filter(|index| matches!(index.origin, Some(Origin::Cli)))
.chain(indexes.iter())
.find(|Index { name, .. }| {
name.as_ref().is_some_and(|name| *name == index)
})
@ -508,15 +510,15 @@ fn registry_source(
/// Convert a path string to a file or directory source.
fn path_source(
path: impl AsRef<Path>,
origin: Origin,
origin: RequirementOrigin,
project_dir: &Path,
workspace_root: &Path,
editable: bool,
) -> Result<RequirementSource, LoweringError> {
let path = path.as_ref();
let base = match origin {
Origin::Project => project_dir,
Origin::Workspace => workspace_root,
RequirementOrigin::Project => project_dir,
RequirementOrigin::Workspace => workspace_root,
};
let url = VerbatimUrl::from_path(path, base)?.with_given(path.to_string_lossy());
let install_path = url.to_file_path().map_err(|()| {

View file

@ -4,6 +4,7 @@ use std::path::Path;
use thiserror::Error;
use uv_configuration::SourceStrategy;
use uv_distribution_types::IndexLocations;
use uv_normalize::{ExtraName, GroupName, PackageName};
use uv_pep440::{Version, VersionSpecifiers};
use uv_pypi_types::{HashDigest, ResolutionMetadata};
@ -61,6 +62,7 @@ impl Metadata {
pub async fn from_workspace(
metadata: ResolutionMetadata,
install_path: &Path,
locations: &IndexLocations,
sources: SourceStrategy,
) -> Result<Self, MetadataError> {
// Lower the requirements.
@ -76,6 +78,7 @@ impl Metadata {
provides_extras: metadata.provides_extras,
},
install_path,
locations,
sources,
LowerBound::Warn,
)

View file

@ -1,10 +1,11 @@
use crate::metadata::lowering::LowerBound;
use crate::metadata::{LoweredRequirement, MetadataError};
use crate::Metadata;
use crate::metadata::lowering::LowerBound;
use std::collections::BTreeMap;
use std::path::Path;
use uv_configuration::SourceStrategy;
use uv_distribution_types::IndexLocations;
use uv_normalize::{ExtraName, GroupName, PackageName, DEV_DEPENDENCIES};
use uv_workspace::pyproject::ToolUvSources;
use uv_workspace::{DiscoveryOptions, ProjectWorkspace};
@ -38,6 +39,7 @@ impl RequiresDist {
pub async fn from_project_maybe_workspace(
metadata: uv_pypi_types::RequiresDist,
install_path: &Path,
locations: &IndexLocations,
sources: SourceStrategy,
lower_bound: LowerBound,
) -> Result<Self, MetadataError> {
@ -50,18 +52,25 @@ impl RequiresDist {
return Ok(Self::from_metadata23(metadata));
};
Self::from_project_workspace(metadata, &project_workspace, sources, lower_bound)
Self::from_project_workspace(
metadata,
&project_workspace,
locations,
sources,
lower_bound,
)
}
fn from_project_workspace(
metadata: uv_pypi_types::RequiresDist,
project_workspace: &ProjectWorkspace,
locations: &IndexLocations,
source_strategy: SourceStrategy,
lower_bound: LowerBound,
) -> Result<Self, MetadataError> {
// Collect any `tool.uv.index` entries.
let empty = vec![];
let indexes = match source_strategy {
let project_indexes = match source_strategy {
SourceStrategy::Enabled => project_workspace
.current_project()
.pyproject_toml()
@ -75,7 +84,7 @@ impl RequiresDist {
// Collect any `tool.uv.sources` and `tool.uv.dev_dependencies` from `pyproject.toml`.
let empty = BTreeMap::default();
let sources = match source_strategy {
let project_sources = match source_strategy {
SourceStrategy::Enabled => project_workspace
.current_project()
.pyproject_toml()
@ -107,8 +116,9 @@ impl RequiresDist {
requirement,
&metadata.name,
project_workspace.project_root(),
sources,
indexes,
project_sources,
project_indexes,
locations,
project_workspace.workspace(),
lower_bound,
)
@ -141,8 +151,9 @@ impl RequiresDist {
requirement,
&metadata.name,
project_workspace.project_root(),
sources,
indexes,
project_sources,
project_indexes,
locations,
project_workspace.workspace(),
lower_bound,
)
@ -188,6 +199,7 @@ mod test {
use indoc::indoc;
use insta::assert_snapshot;
use uv_configuration::SourceStrategy;
use uv_distribution_types::IndexLocations;
use uv_workspace::pyproject::PyProjectToml;
use uv_workspace::{DiscoveryOptions, ProjectWorkspace};
@ -214,7 +226,8 @@ mod test {
Ok(RequiresDist::from_project_workspace(
requires_dist,
&project_workspace,
SourceStrategy::Enabled,
&IndexLocations::default(),
SourceStrategy::default(),
LowerBound::Warn,
)?)
}

View file

@ -388,6 +388,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
let requires_dist = RequiresDist::from_project_maybe_workspace(
requires_dist,
project_root,
self.build_context.locations(),
self.build_context.sources(),
LowerBound::Warn,
)
@ -1086,6 +1087,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
Metadata::from_workspace(
metadata,
resource.install_path.as_ref(),
self.build_context.locations(),
self.build_context.sources(),
)
.await?,
@ -1120,6 +1122,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
Metadata::from_workspace(
metadata,
resource.install_path.as_ref(),
self.build_context.locations(),
self.build_context.sources(),
)
.await?,
@ -1149,6 +1152,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
Metadata::from_workspace(
metadata,
resource.install_path.as_ref(),
self.build_context.locations(),
self.build_context.sources(),
)
.await?,
@ -1194,6 +1198,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
Metadata::from_workspace(
metadata,
resource.install_path.as_ref(),
self.build_context.locations(),
self.build_context.sources(),
)
.await?,
@ -1374,7 +1379,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
Self::read_static_metadata(source, fetch.path(), resource.subdirectory).await?
{
return Ok(ArchiveMetadata::from(
Metadata::from_workspace(metadata, &path, self.build_context.sources()).await?,
Metadata::from_workspace(
metadata,
&path,
self.build_context.locations(),
self.build_context.sources(),
)
.await?,
));
}
@ -1395,7 +1406,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
debug!("Using cached metadata for: {source}");
return Ok(ArchiveMetadata::from(
Metadata::from_workspace(metadata, &path, self.build_context.sources()).await?,
Metadata::from_workspace(
metadata,
&path,
self.build_context.locations(),
self.build_context.sources(),
)
.await?,
));
}
}
@ -1420,7 +1437,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
.map_err(Error::CacheWrite)?;
return Ok(ArchiveMetadata::from(
Metadata::from_workspace(metadata, &path, self.build_context.sources()).await?,
Metadata::from_workspace(
metadata,
&path,
self.build_context.locations(),
self.build_context.sources(),
)
.await?,
));
}
@ -1460,7 +1483,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
.map_err(Error::CacheWrite)?;
Ok(ArchiveMetadata::from(
Metadata::from_workspace(metadata, fetch.path(), self.build_context.sources()).await?,
Metadata::from_workspace(
metadata,
fetch.path(),
self.build_context.locations(),
self.build_context.sources(),
)
.await?,
))
}