uv_distribution/metadata/
mod.rs

1use std::collections::BTreeMap;
2use std::path::{Path, PathBuf};
3
4use thiserror::Error;
5use uv_auth::CredentialsCache;
6use uv_configuration::SourceStrategy;
7use uv_distribution_types::{GitSourceUrl, IndexLocations, Requirement};
8use uv_normalize::{ExtraName, GroupName, PackageName};
9use uv_pep440::{Version, VersionSpecifiers};
10use uv_pypi_types::{HashDigests, ResolutionMetadata};
11use uv_workspace::dependency_groups::DependencyGroupError;
12use uv_workspace::{WorkspaceCache, WorkspaceError};
13
14pub use crate::metadata::build_requires::{BuildRequires, LoweredExtraBuildDependencies};
15pub use crate::metadata::dependency_groups::SourcedDependencyGroups;
16pub use crate::metadata::lowering::LoweredRequirement;
17pub use crate::metadata::lowering::LoweringError;
18pub use crate::metadata::requires_dist::{FlatRequiresDist, RequiresDist};
19
20mod build_requires;
21mod dependency_groups;
22mod lowering;
23mod requires_dist;
24
25#[derive(Debug, Error)]
26pub enum MetadataError {
27    #[error(transparent)]
28    Workspace(#[from] WorkspaceError),
29    #[error(transparent)]
30    DependencyGroup(#[from] DependencyGroupError),
31    #[error("No pyproject.toml found at: {0}")]
32    MissingPyprojectToml(PathBuf),
33    #[error("Failed to parse entry: `{0}`")]
34    LoweringError(PackageName, #[source] Box<LoweringError>),
35    #[error("Failed to parse entry in group `{0}`: `{1}`")]
36    GroupLoweringError(GroupName, PackageName, #[source] Box<LoweringError>),
37    #[error(
38        "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}\"] }}`)."
39    )]
40    MissingSourceExtra(PackageName, ExtraName),
41    #[error(
42        "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}\"] }}`)."
43    )]
44    IncompleteSourceExtra(PackageName, ExtraName),
45    #[error(
46        "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}\"] }}`)."
47    )]
48    MissingSourceGroup(PackageName, GroupName),
49    #[error(
50        "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}\"] }}`)."
51    )]
52    IncompleteSourceGroup(PackageName, GroupName),
53}
54
55#[derive(Debug, Clone)]
56pub struct Metadata {
57    // Mandatory fields
58    pub name: PackageName,
59    pub version: Version,
60    // Optional fields
61    pub requires_dist: Box<[Requirement]>,
62    pub requires_python: Option<VersionSpecifiers>,
63    pub provides_extra: Box<[ExtraName]>,
64    pub dependency_groups: BTreeMap<GroupName, Box<[Requirement]>>,
65    pub dynamic: bool,
66}
67
68impl Metadata {
69    /// Lower without considering `tool.uv` in `pyproject.toml`, used for index and other archive
70    /// dependencies.
71    pub fn from_metadata23(metadata: ResolutionMetadata) -> Self {
72        Self {
73            name: metadata.name,
74            version: metadata.version,
75            requires_dist: Box::into_iter(metadata.requires_dist)
76                .map(Requirement::from)
77                .collect(),
78            requires_python: metadata.requires_python,
79            provides_extra: metadata.provides_extra,
80            dependency_groups: BTreeMap::default(),
81            dynamic: metadata.dynamic,
82        }
83    }
84
85    /// Lower by considering `tool.uv` in `pyproject.toml` if present, used for Git and directory
86    /// dependencies.
87    pub async fn from_workspace(
88        metadata: ResolutionMetadata,
89        install_path: &Path,
90        git_source: Option<&GitWorkspaceMember<'_>>,
91        locations: &IndexLocations,
92        sources: SourceStrategy,
93        cache: &WorkspaceCache,
94        credentials_cache: &CredentialsCache,
95    ) -> Result<Self, MetadataError> {
96        // Lower the requirements.
97        let requires_dist = uv_pypi_types::RequiresDist {
98            name: metadata.name,
99            requires_dist: metadata.requires_dist,
100            provides_extra: metadata.provides_extra,
101            dynamic: metadata.dynamic,
102        };
103        let RequiresDist {
104            name,
105            requires_dist,
106            provides_extra,
107            dependency_groups,
108            dynamic,
109        } = RequiresDist::from_project_maybe_workspace(
110            requires_dist,
111            install_path,
112            git_source,
113            locations,
114            sources,
115            cache,
116            credentials_cache,
117        )
118        .await?;
119
120        // Combine with the remaining metadata.
121        Ok(Self {
122            name,
123            version: metadata.version,
124            requires_dist,
125            requires_python: metadata.requires_python,
126            provides_extra,
127            dependency_groups,
128            dynamic,
129        })
130    }
131}
132
133/// The metadata associated with an archive.
134#[derive(Debug, Clone)]
135pub struct ArchiveMetadata {
136    /// The [`Metadata`] for the underlying distribution.
137    pub metadata: Metadata,
138    /// The hashes of the source or built archive.
139    pub hashes: HashDigests,
140}
141
142impl ArchiveMetadata {
143    /// Lower without considering `tool.uv` in `pyproject.toml`, used for index and other archive
144    /// dependencies.
145    pub fn from_metadata23(metadata: ResolutionMetadata) -> Self {
146        Self {
147            metadata: Metadata::from_metadata23(metadata),
148            hashes: HashDigests::empty(),
149        }
150    }
151}
152
153impl From<Metadata> for ArchiveMetadata {
154    fn from(metadata: Metadata) -> Self {
155        Self {
156            metadata,
157            hashes: HashDigests::empty(),
158        }
159    }
160}
161
162/// A workspace member from a checked-out Git repo.
163#[derive(Debug, Clone)]
164pub struct GitWorkspaceMember<'a> {
165    /// The root of the checkout, which may be the root of the workspace or may be above the
166    /// workspace root.
167    pub fetch_root: &'a Path,
168    pub git_source: &'a GitSourceUrl<'a>,
169}