Skip to main content

uv_distribution/metadata/
mod.rs

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