1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
use uv_distribution_filename::DistExtension;
use uv_normalize::{ExtraName, GroupName, PackageName};
use uv_pypi_types::{HashDigest, HashDigests};
use crate::{
BuiltDist, Diagnostic, Dist, IndexMetadata, Name, RequirementSource, ResolvedDist, SourceDist,
};
/// A set of packages pinned at specific versions.
///
/// This is similar to [`ResolverOutput`], but represents a resolution for a subset of all
/// marker environments. For example, the resolution is guaranteed to contain at most one version
/// for a given package.
#[derive(Debug, Default, Clone)]
pub struct Resolution {
graph: petgraph::graph::DiGraph<Node, Edge>,
diagnostics: Vec<ResolutionDiagnostic>,
}
impl Resolution {
/// Create a [`Resolution`] from the given pinned packages.
pub fn new(graph: petgraph::graph::DiGraph<Node, Edge>) -> Self {
Self {
graph,
diagnostics: Vec::new(),
}
}
/// Return the underlying graph of the resolution.
pub fn graph(&self) -> &petgraph::graph::DiGraph<Node, Edge> {
&self.graph
}
/// Add [`Diagnostics`] to the resolution.
#[must_use]
pub fn with_diagnostics(mut self, diagnostics: Vec<ResolutionDiagnostic>) -> Self {
self.diagnostics.extend(diagnostics);
self
}
/// Return the hashes for the given package name, if they exist.
pub fn hashes(&self) -> impl Iterator<Item = (&ResolvedDist, &[HashDigest])> {
self.graph
.node_indices()
.filter_map(move |node| match &self.graph[node] {
Node::Dist {
dist,
hashes,
install,
..
} if *install => Some((dist, hashes.as_slice())),
_ => None,
})
}
/// Iterate over the [`ResolvedDist`] entities in this resolution.
pub fn distributions(&self) -> impl Iterator<Item = &ResolvedDist> {
self.graph
.raw_nodes()
.iter()
.filter_map(|node| match &node.weight {
Node::Dist { dist, install, .. } if *install => Some(dist),
_ => None,
})
}
/// Return the number of distributions in this resolution.
pub fn len(&self) -> usize {
self.distributions().count()
}
/// Return `true` if there are no pinned packages in this resolution.
pub fn is_empty(&self) -> bool {
self.distributions().next().is_none()
}
/// Return the [`ResolutionDiagnostic`]s that were produced during resolution.
pub fn diagnostics(&self) -> &[ResolutionDiagnostic] {
&self.diagnostics
}
/// Filter the resolution to only include packages that match the given predicate.
#[must_use]
pub fn filter(mut self, predicate: impl Fn(&ResolvedDist) -> bool) -> Self {
for node in self.graph.node_weights_mut() {
if let Node::Dist { dist, install, .. } = node {
if !predicate(dist) {
*install = false;
}
}
}
self
}
/// Map over the resolved distributions in this resolution.
///
/// For efficiency, the map function should return `None` if the resolved distribution is
/// unchanged.
#[must_use]
pub fn map(mut self, predicate: impl Fn(&ResolvedDist) -> Option<ResolvedDist>) -> Self {
for node in self.graph.node_weights_mut() {
if let Node::Dist { dist, .. } = node {
if let Some(transformed) = predicate(dist) {
*dist = transformed;
}
}
}
self
}
}
#[derive(Debug, Clone, Hash)]
pub enum ResolutionDiagnostic {
MissingExtra {
/// The distribution that was requested with a non-existent extra. For example,
/// `black==23.10.0`.
dist: ResolvedDist,
/// The extra that was requested. For example, `colorama` in `black[colorama]`.
extra: ExtraName,
},
MissingGroup {
/// The distribution that was requested with a non-existent development dependency group.
dist: ResolvedDist,
/// The development dependency group that was requested.
group: GroupName,
},
YankedVersion {
/// The package that was requested with a yanked version. For example, `black==23.10.0`.
dist: ResolvedDist,
/// The reason that the version was yanked, if any.
reason: Option<String>,
},
MissingLowerBound {
/// The name of the package that had no lower bound from any other package in the
/// resolution. For example, `black`.
package_name: PackageName,
},
}
impl Diagnostic for ResolutionDiagnostic {
/// Convert the diagnostic into a user-facing message.
fn message(&self) -> String {
match self {
Self::MissingExtra { dist, extra } => {
format!("The package `{dist}` does not have an extra named `{extra}`")
}
Self::MissingGroup { dist, group } => {
format!(
"The package `{dist}` does not have a development dependency group named `{group}`"
)
}
Self::YankedVersion { dist, reason } => {
if let Some(reason) = reason {
format!("`{dist}` is yanked (reason: \"{reason}\")")
} else {
format!("`{dist}` is yanked")
}
}
Self::MissingLowerBound { package_name: name } => {
format!(
"The transitive dependency `{name}` is unpinned. \
Consider setting a lower bound with a constraint when using \
`--resolution lowest` to avoid using outdated versions."
)
}
}
}
/// Returns `true` if the [`PackageName`] is involved in this diagnostic.
fn includes(&self, name: &PackageName) -> bool {
match self {
Self::MissingExtra { dist, .. } => name == dist.name(),
Self::MissingGroup { dist, .. } => name == dist.name(),
Self::YankedVersion { dist, .. } => name == dist.name(),
Self::MissingLowerBound { package_name } => name == package_name,
}
}
}
/// A node in the resolution, along with whether its been filtered out.
///
/// We retain filtered nodes as we still need to be able to trace dependencies through the graph
/// (e.g., to determine why a package was included in the resolution).
#[derive(Debug, Clone)]
pub enum Node {
Root,
Dist {
dist: ResolvedDist,
hashes: HashDigests,
install: bool,
},
}
impl Node {
/// Returns `true` if the node should be installed.
pub fn install(&self) -> bool {
match self {
Self::Root => false,
Self::Dist { install, .. } => *install,
}
}
}
/// An edge in the resolution graph.
#[derive(Debug, Clone)]
pub enum Edge {
Prod,
Optional(ExtraName),
Dev(GroupName),
}
impl From<&ResolvedDist> for RequirementSource {
fn from(resolved_dist: &ResolvedDist) -> Self {
match resolved_dist {
ResolvedDist::Installable { dist, .. } => match dist.as_ref() {
Dist::Built(BuiltDist::Registry(wheels)) => {
let wheel = wheels.best_wheel();
Self::Registry {
specifier: uv_pep440::VersionSpecifiers::from(
uv_pep440::VersionSpecifier::equals_version(
wheel.filename.version.clone(),
),
),
index: Some(IndexMetadata::from(wheel.index.clone())),
conflict: None,
}
}
Dist::Built(BuiltDist::DirectUrl(wheel)) => {
let mut location = wheel.url.to_url();
location.set_fragment(None);
Self::Url {
url: wheel.url.clone(),
location,
subdirectory: None,
ext: DistExtension::Wheel,
}
}
Dist::Built(BuiltDist::Path(wheel)) => Self::Path {
install_path: wheel.install_path.clone(),
url: wheel.url.clone(),
ext: DistExtension::Wheel,
},
Dist::Source(SourceDist::Registry(sdist)) => Self::Registry {
specifier: uv_pep440::VersionSpecifiers::from(
uv_pep440::VersionSpecifier::equals_version(sdist.version.clone()),
),
index: Some(IndexMetadata::from(sdist.index.clone())),
conflict: None,
},
Dist::Source(SourceDist::DirectUrl(sdist)) => {
let mut location = sdist.url.to_url();
location.set_fragment(None);
Self::Url {
url: sdist.url.clone(),
location,
subdirectory: sdist.subdirectory.clone(),
ext: DistExtension::Source(sdist.ext),
}
}
Dist::Source(SourceDist::Git(sdist)) => Self::Git {
git: (*sdist.git).clone(),
url: sdist.url.clone(),
subdirectory: sdist.subdirectory.clone(),
},
Dist::Source(SourceDist::Path(sdist)) => Self::Path {
install_path: sdist.install_path.clone(),
url: sdist.url.clone(),
ext: DistExtension::Source(sdist.ext),
},
Dist::Source(SourceDist::Directory(sdist)) => Self::Directory {
install_path: sdist.install_path.clone(),
url: sdist.url.clone(),
editable: sdist.editable,
r#virtual: sdist.r#virtual,
},
},
ResolvedDist::Installed { dist } => Self::Registry {
specifier: uv_pep440::VersionSpecifiers::from(
uv_pep440::VersionSpecifier::equals_version(dist.version().clone()),
),
index: None,
conflict: None,
},
}
}
}