uv_distribution/metadata/
mod.rs1use 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::Hinted 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 pub name: PackageName,
70 pub version: Version,
71 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 pub(crate) fn from_metadata23(metadata: ResolutionMetadata) -> Self {
83 Self::from_resolution_metadata(metadata).with_force_relative(true)
86 }
87
88 pub(crate) fn from_dependency_metadata(metadata: ResolutionMetadata) -> Self {
90 Self::from_resolution_metadata(metadata)
92 }
93
94 fn from_resolution_metadata(metadata: ResolutionMetadata) -> Self {
96 Self {
97 name: metadata.name,
98 version: metadata.version,
99 requires_dist: Box::into_iter(metadata.requires_dist)
100 .map(Requirement::from)
101 .collect(),
102 requires_python: metadata.requires_python,
103 provides_extra: metadata.provides_extra,
104 dependency_groups: BTreeMap::default(),
105 dynamic: metadata.dynamic,
106 }
107 }
108
109 pub async fn from_workspace(
112 metadata: ResolutionMetadata,
113 install_path: &Path,
114 git_source: Option<&GitWorkspaceMember<'_>>,
115 locations: &IndexLocations,
116 sources: NoSources,
117 editable: bool,
118 cache: &Cache,
119 workspace_cache: &WorkspaceCache,
120 credentials_cache: &CredentialsCache,
121 ) -> Result<Self, MetadataError> {
122 let requires_dist = uv_pypi_types::RequiresDist {
124 name: metadata.name,
125 requires_dist: metadata.requires_dist,
126 provides_extra: metadata.provides_extra,
127 dynamic: metadata.dynamic,
128 };
129 let RequiresDist {
130 name,
131 requires_dist,
132 provides_extra,
133 dependency_groups,
134 dynamic,
135 } = RequiresDist::from_project_maybe_workspace(
136 requires_dist,
137 install_path,
138 git_source,
139 locations,
140 sources,
141 editable,
142 cache,
143 workspace_cache,
144 credentials_cache,
145 )
146 .await?;
147
148 Ok(Self {
150 name,
151 version: metadata.version,
152 requires_dist,
153 requires_python: metadata.requires_python,
154 provides_extra,
155 dependency_groups,
156 dynamic,
157 })
158 }
159
160 #[must_use]
164 pub fn with_force_relative(mut self, force_relative: bool) -> Self {
165 for requirement in self.requires_dist.iter_mut().chain(
166 self.dependency_groups
167 .values_mut()
168 .flat_map(|requirements| requirements.iter_mut()),
169 ) {
170 requirement.set_force_relative(force_relative);
171 }
172
173 self
174 }
175}
176
177#[derive(Debug, Clone)]
179pub struct ArchiveMetadata {
180 pub metadata: Metadata,
182 pub hashes: HashDigests,
184}
185
186impl ArchiveMetadata {
187 pub fn from_metadata23(metadata: ResolutionMetadata) -> Self {
190 Self {
191 metadata: Metadata::from_metadata23(metadata),
192 hashes: HashDigests::empty(),
193 }
194 }
195}
196
197impl From<Metadata> for ArchiveMetadata {
198 fn from(metadata: Metadata) -> Self {
199 Self {
200 metadata,
201 hashes: HashDigests::empty(),
202 }
203 }
204}
205
206#[derive(Debug, Clone)]
208pub struct GitWorkspaceMember<'a> {
209 pub fetch_root: &'a Path,
212 pub git_source: &'a GitDirectorySourceUrl<'a>,
213}