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::{GitDirectorySourceUrl, 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
56impl uv_errors::Hint for MetadataError {
57    fn hints(&self) -> uv_errors::Hints<'_> {
58        match self {
59            Self::LoweringError(_, err) | Self::GroupLoweringError(_, _, err) => err.hints(),
60            _ => uv_errors::Hints::none(),
61        }
62    }
63}
64
65#[derive(Debug, Clone)]
66pub struct Metadata {
67    // Mandatory fields
68    pub name: PackageName,
69    pub version: Version,
70    // Optional fields
71    pub requires_dist: Box<[Requirement]>,
72    pub requires_python: Option<VersionSpecifiers>,
73    pub provides_extra: Box<[ExtraName]>,
74    pub dependency_groups: BTreeMap<GroupName, Box<[Requirement]>>,
75    pub dynamic: bool,
76}
77
78impl Metadata {
79    /// Lower without considering `tool.uv` in `pyproject.toml`, used for index and other archive
80    /// dependencies.
81    pub fn from_metadata23(metadata: ResolutionMetadata) -> Self {
82        Self {
83            name: metadata.name,
84            version: metadata.version,
85            requires_dist: Box::into_iter(metadata.requires_dist)
86                .map(Requirement::from)
87                .collect(),
88            requires_python: metadata.requires_python,
89            provides_extra: metadata.provides_extra,
90            dependency_groups: BTreeMap::default(),
91            dynamic: metadata.dynamic,
92        }
93    }
94
95    /// Lower by considering `tool.uv` in `pyproject.toml` if present, used for Git and directory
96    /// dependencies.
97    pub async fn from_workspace(
98        metadata: ResolutionMetadata,
99        install_path: &Path,
100        git_source: Option<&GitWorkspaceMember<'_>>,
101        locations: &IndexLocations,
102        sources: NoSources,
103        editable: bool,
104        cache: &WorkspaceCache,
105        credentials_cache: &CredentialsCache,
106    ) -> Result<Self, MetadataError> {
107        // Lower the requirements.
108        let requires_dist = uv_pypi_types::RequiresDist {
109            name: metadata.name,
110            requires_dist: metadata.requires_dist,
111            provides_extra: metadata.provides_extra,
112            dynamic: metadata.dynamic,
113        };
114        let RequiresDist {
115            name,
116            requires_dist,
117            provides_extra,
118            dependency_groups,
119            dynamic,
120        } = RequiresDist::from_project_maybe_workspace(
121            requires_dist,
122            install_path,
123            git_source,
124            locations,
125            sources,
126            editable,
127            cache,
128            credentials_cache,
129        )
130        .await?;
131
132        // Combine with the remaining metadata.
133        Ok(Self {
134            name,
135            version: metadata.version,
136            requires_dist,
137            requires_python: metadata.requires_python,
138            provides_extra,
139            dependency_groups,
140            dynamic,
141        })
142    }
143}
144
145/// The metadata associated with an archive.
146#[derive(Debug, Clone)]
147pub struct ArchiveMetadata {
148    /// The [`Metadata`] for the underlying distribution.
149    pub metadata: Metadata,
150    /// The hashes of the source or built archive.
151    pub hashes: HashDigests,
152}
153
154impl ArchiveMetadata {
155    /// Lower without considering `tool.uv` in `pyproject.toml`, used for index and other archive
156    /// dependencies.
157    pub fn from_metadata23(metadata: ResolutionMetadata) -> Self {
158        Self {
159            metadata: Metadata::from_metadata23(metadata),
160            hashes: HashDigests::empty(),
161        }
162    }
163}
164
165impl From<Metadata> for ArchiveMetadata {
166    fn from(metadata: Metadata) -> Self {
167        Self {
168            metadata,
169            hashes: HashDigests::empty(),
170        }
171    }
172}
173
174/// A workspace member from a checked-out Git repo.
175#[derive(Debug, Clone)]
176pub struct GitWorkspaceMember<'a> {
177    /// The root of the checkout, which may be the root of the workspace or may be above the
178    /// workspace root.
179    pub fetch_root: &'a Path,
180    pub git_source: &'a GitDirectorySourceUrl<'a>,
181}