use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use uv_auth::CredentialsCache;
use uv_configuration::NoSources;
use uv_distribution_types::{IndexLocations, Requirement};
use uv_normalize::{GroupName, PackageName};
use uv_workspace::dependency_groups::FlatDependencyGroups;
use uv_workspace::pyproject::{Sources, ToolUvSources};
use uv_workspace::{
DiscoveryOptions, MemberDiscovery, VirtualProject, WorkspaceCache, WorkspaceError,
};
use crate::metadata::{GitWorkspaceMember, LoweredRequirement, MetadataError};
#[derive(Debug, Clone)]
pub struct SourcedDependencyGroups {
pub name: Option<PackageName>,
pub dependency_groups: BTreeMap<GroupName, Box<[Requirement]>>,
}
impl SourcedDependencyGroups {
pub async fn from_virtual_project(
pyproject_path: &Path,
git_member: Option<&GitWorkspaceMember<'_>>,
locations: &IndexLocations,
no_sources: NoSources,
cache: &WorkspaceCache,
credentials_cache: &CredentialsCache,
) -> Result<Self, MetadataError> {
if !pyproject_path.is_file() {
return Err(MetadataError::MissingPyprojectToml(
pyproject_path.to_path_buf(),
));
}
let discovery = DiscoveryOptions {
stop_discovery_at: git_member.map(|git_member| {
git_member
.fetch_root
.parent()
.expect("git checkout has a parent")
.to_path_buf()
}),
members: if no_sources.is_none() {
MemberDiscovery::default()
} else {
MemberDiscovery::None
},
..DiscoveryOptions::default()
};
let empty = PathBuf::new();
let absolute_pyproject_path =
std::path::absolute(pyproject_path).map_err(WorkspaceError::Normalize)?;
let project_dir = absolute_pyproject_path.parent().unwrap_or(&empty);
let project = VirtualProject::discover(project_dir, &discovery, cache).await?;
let dependency_groups =
FlatDependencyGroups::from_pyproject_toml(project.root(), project.pyproject_toml())?;
if matches!(no_sources, NoSources::All) {
return Ok(Self {
name: project.project_name().cloned(),
dependency_groups: dependency_groups
.into_iter()
.map(|(name, group)| {
let requirements = group
.requirements
.into_iter()
.map(Requirement::from)
.collect();
(name, requirements)
})
.collect(),
});
}
let empty = vec![];
let project_indexes = project
.pyproject_toml()
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.index.as_deref())
.unwrap_or(&empty);
let empty = BTreeMap::default();
let project_sources = project
.pyproject_toml()
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.sources.as_ref())
.map(ToolUvSources::inner)
.unwrap_or(&empty);
Self::validate_sources(project_sources, &dependency_groups)?;
let dependency_groups = dependency_groups
.into_iter()
.map(|(name, group)| {
let requirements = group
.requirements
.into_iter()
.flat_map(|requirement| {
if no_sources.for_package(&requirement.name) {
vec![Ok(Requirement::from(requirement))].into_iter()
} else {
let requirement_name = requirement.name.clone();
let group = name.clone();
let extra = None;
LoweredRequirement::from_requirement(
requirement,
project.project_name(),
project.root(),
project_sources,
project_indexes,
extra,
Some(&group),
locations,
project.workspace(),
git_member,
true,
credentials_cache,
)
.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::<Vec<_>>()
.into_iter()
}
})
.collect::<Result<Box<_>, _>>()?;
Ok::<(GroupName, Box<_>), MetadataError>((name, requirements))
})
.collect::<Result<BTreeMap<_, _>, _>>()?;
Ok(Self {
name: project.project_name().cloned(),
dependency_groups,
})
}
fn validate_sources(
sources: &BTreeMap<PackageName, Sources>,
dependency_groups: &FlatDependencyGroups,
) -> Result<(), MetadataError> {
for (name, sources) in sources {
for source in sources.iter() {
if let Some(group) = source.group() {
let Some(flat_group) = dependency_groups.get(group) else {
return Err(MetadataError::MissingSourceGroup(
name.clone(),
group.clone(),
));
};
if !flat_group
.requirements
.iter()
.any(|requirement| requirement.name == *name)
{
return Err(MetadataError::IncompleteSourceGroup(
name.clone(),
group.clone(),
));
}
}
}
}
Ok(())
}
}