Skip to content

Commit

Permalink
Error messages
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Nov 17, 2024
1 parent b748081 commit d7a9215
Show file tree
Hide file tree
Showing 4 changed files with 217 additions and 236 deletions.
8 changes: 8 additions & 0 deletions crates/uv-distribution/src/metadata/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ pub enum MetadataError {
LoweringError(PackageName, #[source] Box<LoweringError>),
#[error("Failed to parse entry in group `{0}`: `{1}`")]
GroupLoweringError(GroupName, PackageName, #[source] Box<LoweringError>),
#[error("Source entry for `{0}` only applies to extra `{1}`, but the `{1}` extra does not exist. When an extra is present on a source (e.g., `extra = \"{1}\"`), the relevant package must be included in the `project.optional-dependencies` section for that extra (e.g., `project.optional-dependencies = {{ \"{1}\" = [\"{0}\"] }}`).")]
MissingSourceExtra(PackageName, ExtraName),
#[error("Source entry for `{0}` only applies to extra `{1}`, but `{0}` was not found under the `project.optional-dependencies` section for that extra. When an extra is present on a source (e.g., `extra = \"{1}\"`), the relevant package must be included in the `project.optional-dependencies` section for that extra (e.g., `project.optional-dependencies = {{ \"{1}\" = [\"{0}\"] }}`).")]
IncompleteSourceExtra(PackageName, ExtraName),
#[error("Source entry for `{0}` only applies to dependency group `{1}`, but the `{1}` group does not exist. When a group is present on a source (e.g., `group = \"{1}\"`), the relevant package must be included in the `dependency-groups` section for that extra (e.g., `dependency-groups = {{ \"{1}\" = [\"{0}\"] }}`).")]
MissingSourceGroup(PackageName, GroupName),
#[error("Source entry for `{0}` only applies to dependency group `{1}`, but `{0}` was not found under the `dependency-groups` section for that group. When a group is present on a source (e.g., `group = \"{1}\"`), the relevant package must be included in the `dependency-groups` section for that extra (e.g., `dependency-groups = {{ \"{1}\" = [\"{0}\"] }}`).")]
IncompleteSourceGroup(PackageName, GroupName),
}

#[derive(Debug, Clone)]
Expand Down
193 changes: 125 additions & 68 deletions crates/uv-distribution/src/metadata/requires_dist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use uv_configuration::{LowerBound, SourceStrategy};
use uv_distribution_types::IndexLocations;
use uv_normalize::{ExtraName, GroupName, PackageName, DEV_DEPENDENCIES};
use uv_workspace::dependency_groups::FlatDependencyGroups;
use uv_workspace::pyproject::ToolUvSources;
use uv_workspace::pyproject::{Sources, ToolUvSources};
use uv_workspace::{DiscoveryOptions, ProjectWorkspace};

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -111,6 +111,7 @@ impl RequiresDist {
SourceStrategy::Disabled => &empty,
};

// Collect the dependency groups.
let dependency_groups = {
// First, collect `tool.uv.dev_dependencies`
let dev_dependencies = project_workspace
Expand All @@ -130,76 +131,74 @@ impl RequiresDist {
.flatten()
.collect::<BTreeMap<_, _>>();

// Resolve any `include-group` entries in `dependency-groups`.
let dependency_groups =
FlatDependencyGroups::from_dependency_groups(&dependency_groups)?
.into_iter()
.chain(
// Only add the `dev` group if `dev-dependencies` is defined.
dev_dependencies
.into_iter()
.map(|requirements| (DEV_DEPENDENCIES.clone(), requirements.clone())),
)
.map(|(name, requirements)| {
let requirements = match source_strategy {
SourceStrategy::Enabled => requirements
.into_iter()
.flat_map(|requirement| {
let requirement_name = requirement.name.clone();
let group = name.clone();
let extra = None;
LoweredRequirement::from_requirement(
requirement,
&metadata.name,
project_workspace.project_root(),
project_sources,
project_indexes,
extra,
Some(group.clone()),
locations,
project_workspace.workspace(),
lower_bound,
git_member,
)
.map(move |requirement| {
match requirement {
Ok(requirement) => Ok(requirement.into_inner()),
Err(err) => Err(MetadataError::GroupLoweringError(
group.clone(),
requirement_name.clone(),
Box::new(err),
)),
}
})
})
.collect::<Result<Vec<_>, _>>(),
SourceStrategy::Disabled => Ok(requirements
.into_iter()
.map(uv_pypi_types::Requirement::from)
.collect()),
}?;
Ok::<(GroupName, Vec<uv_pypi_types::Requirement>), MetadataError>((
name,
requirements,
))
})
.collect::<Result<Vec<_>, _>>()?;

// Merge any overlapping groups.
let mut map = BTreeMap::new();
for (name, dependencies) in dependency_groups {
match map.entry(name) {
std::collections::btree_map::Entry::Vacant(entry) => {
entry.insert(dependencies);
}
std::collections::btree_map::Entry::Occupied(mut entry) => {
entry.get_mut().extend(dependencies);
}
}
// Flatten the dependency groups.
let mut dependency_groups =
FlatDependencyGroups::from_dependency_groups(&dependency_groups)?;

// Add the `dev` group, if `dev-dependencies` is defined.
if let Some(dev_dependencies) = dev_dependencies {
dependency_groups
.entry(DEV_DEPENDENCIES.clone())
.or_insert_with(Vec::new)
.extend(dev_dependencies.clone());
}
map

dependency_groups
};

// Now that we've resolved the dependency groups, we can validate that each source references
// a valid extra or group, if present.
Self::validate_sources(project_sources, &metadata, &dependency_groups)?;

// Lower the dependency groups.
let dependency_groups = dependency_groups
.into_iter()
.map(|(name, requirements)| {
let requirements = match source_strategy {
SourceStrategy::Enabled => requirements
.into_iter()
.flat_map(|requirement| {
let requirement_name = requirement.name.clone();
let group = name.clone();
let extra = None;
LoweredRequirement::from_requirement(
requirement,
&metadata.name,
project_workspace.project_root(),
project_sources,
project_indexes,
extra,
Some(group.clone()),
locations,
project_workspace.workspace(),
lower_bound,
git_member,
)
.map(
move |requirement| match requirement {
Ok(requirement) => Ok(requirement.into_inner()),
Err(err) => Err(MetadataError::GroupLoweringError(
group.clone(),
requirement_name.clone(),
Box::new(err),
)),
},
)
})
.collect::<Result<Vec<_>, _>>(),
SourceStrategy::Disabled => Ok(requirements
.into_iter()
.map(uv_pypi_types::Requirement::from)
.collect()),
}?;
Ok::<(GroupName, Vec<uv_pypi_types::Requirement>), MetadataError>((
name,
requirements,
))
})
.collect::<Result<BTreeMap<_, _>, _>>()?;

// Lower the requirements.
let requires_dist = metadata.requires_dist.into_iter();
let requires_dist = match source_strategy {
SourceStrategy::Enabled => requires_dist
Expand Down Expand Up @@ -242,6 +241,64 @@ impl RequiresDist {
provides_extras: metadata.provides_extras,
})
}

/// Validate the sources for a given [`uv_pypi_types::RequiresDist`].
///
/// If a source is requested with an `extra` or `group`, ensure that the relevant dependency is
/// present in the relevant `project.optional-dependencies` or `dependency-groups` section.
fn validate_sources(
sources: &BTreeMap<PackageName, Sources>,
metadata: &uv_pypi_types::RequiresDist,
dependency_groups: &FlatDependencyGroups,
) -> Result<(), MetadataError> {
for (name, sources) in sources {
for source in sources.iter() {
if let Some(extra) = source.extra() {
// If the extra doesn't exist at all, error.
if !metadata.provides_extras.contains(extra) {
return Err(MetadataError::MissingSourceExtra(
name.clone(),
extra.clone(),
));
}

// If there is no such requirement with the extra, error.
if !metadata.requires_dist.iter().any(|requirement| {
requirement.name == *name
&& requirement.marker.top_level_extra_name().as_ref() == Some(extra)
}) {
return Err(MetadataError::IncompleteSourceExtra(
name.clone(),
extra.clone(),
));
}
}

if let Some(group) = source.group() {
// If the group doesn't exist at all, error.
let Some(dependencies) = dependency_groups.get(group) else {
return Err(MetadataError::MissingSourceGroup(
name.clone(),
group.clone(),
));
};

// If there is no such requirement with the group, error.
if !dependencies
.iter()
.any(|requirement| requirement.name == *name)
{
return Err(MetadataError::IncompleteSourceGroup(
name.clone(),
group.clone(),
));
}
}
}
}

Ok(())
}
}

impl From<Metadata> for RequiresDist {
Expand Down
8 changes: 8 additions & 0 deletions crates/uv-workspace/src/dependency_groups.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::collections::btree_map::Entry;
use std::collections::BTreeMap;
use std::str::FromStr;

Expand Down Expand Up @@ -102,6 +103,13 @@ impl FlatDependencyGroups {
) -> Option<&Vec<uv_pep508::Requirement<VerbatimParsedUrl>>> {
self.0.get(group)
}

pub fn entry(
&mut self,
group: GroupName,
) -> Entry<GroupName, Vec<uv_pep508::Requirement<VerbatimParsedUrl>>> {
self.0.entry(group)
}
}

impl IntoIterator for FlatDependencyGroups {
Expand Down
Loading

0 comments on commit d7a9215

Please sign in to comment.