use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use thiserror::Error;
use uv_auth::CredentialsCache;
use uv_configuration::NoSources;
use uv_distribution_types::{GitSourceUrl, IndexLocations, Requirement};
use uv_normalize::{ExtraName, GroupName, PackageName};
use uv_pep440::{Version, VersionSpecifiers};
use uv_pypi_types::{HashDigests, ResolutionMetadata};
use uv_workspace::dependency_groups::DependencyGroupError;
use uv_workspace::{WorkspaceCache, WorkspaceError};
pub use crate::metadata::build_requires::{BuildRequires, LoweredExtraBuildDependencies};
pub use crate::metadata::dependency_groups::SourcedDependencyGroups;
pub use crate::metadata::lowering::LoweredRequirement;
pub use crate::metadata::lowering::LoweringError;
pub use crate::metadata::requires_dist::{FlatRequiresDist, RequiresDist};
mod build_requires;
mod dependency_groups;
mod lowering;
mod requires_dist;
#[derive(Debug, Error)]
pub enum MetadataError {
#[error(transparent)]
Workspace(#[from] WorkspaceError),
#[error(transparent)]
DependencyGroup(#[from] DependencyGroupError),
#[error("No pyproject.toml found at: {0}")]
MissingPyprojectToml(PathBuf),
#[error("Failed to parse entry: `{0}`")]
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)]
pub struct Metadata {
pub name: PackageName,
pub version: Version,
pub requires_dist: Box<[Requirement]>,
pub requires_python: Option<VersionSpecifiers>,
pub provides_extra: Box<[ExtraName]>,
pub dependency_groups: BTreeMap<GroupName, Box<[Requirement]>>,
pub dynamic: bool,
}
impl Metadata {
pub fn from_metadata23(metadata: ResolutionMetadata) -> Self {
Self {
name: metadata.name,
version: metadata.version,
requires_dist: Box::into_iter(metadata.requires_dist)
.map(Requirement::from)
.collect(),
requires_python: metadata.requires_python,
provides_extra: metadata.provides_extra,
dependency_groups: BTreeMap::default(),
dynamic: metadata.dynamic,
}
}
pub async fn from_workspace(
metadata: ResolutionMetadata,
install_path: &Path,
git_source: Option<&GitWorkspaceMember<'_>>,
locations: &IndexLocations,
sources: NoSources,
editable: bool,
cache: &WorkspaceCache,
credentials_cache: &CredentialsCache,
) -> Result<Self, MetadataError> {
let requires_dist = uv_pypi_types::RequiresDist {
name: metadata.name,
requires_dist: metadata.requires_dist,
provides_extra: metadata.provides_extra,
dynamic: metadata.dynamic,
};
let RequiresDist {
name,
requires_dist,
provides_extra,
dependency_groups,
dynamic,
} = RequiresDist::from_project_maybe_workspace(
requires_dist,
install_path,
git_source,
locations,
sources,
editable,
cache,
credentials_cache,
)
.await?;
Ok(Self {
name,
version: metadata.version,
requires_dist,
requires_python: metadata.requires_python,
provides_extra,
dependency_groups,
dynamic,
})
}
}
#[derive(Debug, Clone)]
pub struct ArchiveMetadata {
pub metadata: Metadata,
pub hashes: HashDigests,
}
impl ArchiveMetadata {
pub fn from_metadata23(metadata: ResolutionMetadata) -> Self {
Self {
metadata: Metadata::from_metadata23(metadata),
hashes: HashDigests::empty(),
}
}
}
impl From<Metadata> for ArchiveMetadata {
fn from(metadata: Metadata) -> Self {
Self {
metadata,
hashes: HashDigests::empty(),
}
}
}
#[derive(Debug, Clone)]
pub struct GitWorkspaceMember<'a> {
pub fetch_root: &'a Path,
pub git_source: &'a GitSourceUrl<'a>,
}