uv_distribution/metadata/
mod.rs1use 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 pub name: PackageName,
69 pub version: Version,
70 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 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 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 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 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#[derive(Debug, Clone)]
147pub struct ArchiveMetadata {
148 pub metadata: Metadata,
150 pub hashes: HashDigests,
152}
153
154impl ArchiveMetadata {
155 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#[derive(Debug, Clone)]
176pub struct GitWorkspaceMember<'a> {
177 pub fetch_root: &'a Path,
180 pub git_source: &'a GitDirectorySourceUrl<'a>,
181}