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