Skip to content

Commit 6efd07c

Browse files
committed
Refactor Resolution type to retain dependency graph
1 parent b37170d commit 6efd07c

File tree

10 files changed

+323
-141
lines changed

10 files changed

+323
-141
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/uv-distribution-types/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ bitflags = { workspace = true }
3333
fs-err = { workspace = true }
3434
itertools = { workspace = true }
3535
jiff = { workspace = true }
36+
petgraph = { workspace = true }
3637
rkyv = { workspace = true }
3738
rustc-hash = { workspace = true }
3839
schemars = { workspace = true, optional = true }

crates/uv-distribution-types/src/resolution.rs

Lines changed: 95 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,74 @@
1-
use std::collections::BTreeMap;
1+
use petgraph::{Directed, Graph};
2+
23
use uv_distribution_filename::DistExtension;
34
use uv_normalize::{ExtraName, GroupName, PackageName};
5+
use uv_pep508::MarkerTree;
46
use uv_pypi_types::{HashDigest, RequirementSource};
57

68
use crate::{BuiltDist, Diagnostic, Dist, Name, ResolvedDist, SourceDist};
79

810
/// A set of packages pinned at specific versions.
911
#[derive(Debug, Default, Clone)]
1012
pub struct Resolution {
11-
packages: BTreeMap<PackageName, ResolvedDist>,
12-
hashes: BTreeMap<PackageName, Vec<HashDigest>>,
13+
graph: Graph<Node, Edge, Directed>,
1314
diagnostics: Vec<ResolutionDiagnostic>,
1415
}
1516

1617
impl Resolution {
1718
/// Create a new resolution from the given pinned packages.
18-
pub fn new(
19-
packages: BTreeMap<PackageName, ResolvedDist>,
20-
hashes: BTreeMap<PackageName, Vec<HashDigest>>,
21-
diagnostics: Vec<ResolutionDiagnostic>,
22-
) -> Self {
19+
pub fn new(graph: Graph<Node, Edge, Directed>) -> Self {
2320
Self {
24-
packages,
25-
hashes,
26-
diagnostics,
21+
graph,
22+
diagnostics: Vec::new(),
2723
}
2824
}
2925

30-
/// Return the hashes for the given package name, if they exist.
31-
pub fn get_hashes(&self, package_name: &PackageName) -> &[HashDigest] {
32-
self.hashes.get(package_name).map_or(&[], Vec::as_slice)
26+
/// Return the underlying graph of the resolution.
27+
pub fn graph(&self) -> &Graph<Node, Edge, Directed> {
28+
&self.graph
29+
}
30+
31+
/// Add [`Diagnostics`] to the resolution.
32+
#[must_use]
33+
pub fn with_diagnostics(mut self, diagnostics: Vec<ResolutionDiagnostic>) -> Self {
34+
self.diagnostics.extend(diagnostics);
35+
self
3336
}
3437

35-
/// Iterate over the [`PackageName`] entities in this resolution.
36-
pub fn packages(&self) -> impl Iterator<Item = &PackageName> {
37-
self.packages.keys()
38+
/// Return the hashes for the given package name, if they exist.
39+
pub fn hashes(&self) -> impl Iterator<Item = (&ResolvedDist, &[HashDigest])> {
40+
self.graph
41+
.node_indices()
42+
.filter_map(move |node| match &self.graph[node] {
43+
Node::Dist {
44+
dist,
45+
hashes,
46+
install,
47+
..
48+
} if *install => Some((dist, hashes.as_slice())),
49+
_ => None,
50+
})
3851
}
3952

4053
/// Iterate over the [`ResolvedDist`] entities in this resolution.
4154
pub fn distributions(&self) -> impl Iterator<Item = &ResolvedDist> {
42-
self.packages.values()
55+
self.graph
56+
.raw_nodes()
57+
.iter()
58+
.filter_map(|node| match &node.weight {
59+
Node::Dist { dist, install, .. } if *install => Some(dist),
60+
_ => None,
61+
})
4362
}
4463

4564
/// Return the number of distributions in this resolution.
4665
pub fn len(&self) -> usize {
47-
self.packages.len()
66+
self.distributions().count()
4867
}
4968

5069
/// Return `true` if there are no pinned packages in this resolution.
5170
pub fn is_empty(&self) -> bool {
52-
self.packages.is_empty()
71+
self.distributions().next().is_none()
5372
}
5473

5574
/// Return the [`ResolutionDiagnostic`]s that were produced during resolution.
@@ -59,44 +78,31 @@ impl Resolution {
5978

6079
/// Filter the resolution to only include packages that match the given predicate.
6180
#[must_use]
62-
pub fn filter(self, predicate: impl Fn(&ResolvedDist) -> bool) -> Self {
63-
let packages = self
64-
.packages
65-
.into_iter()
66-
.filter(|(_, dist)| predicate(dist))
67-
.collect::<BTreeMap<_, _>>();
68-
let hashes = self
69-
.hashes
70-
.into_iter()
71-
.filter(|(name, _)| packages.contains_key(name))
72-
.collect();
73-
let diagnostics = self.diagnostics.clone();
74-
Self {
75-
packages,
76-
hashes,
77-
diagnostics,
81+
pub fn filter(mut self, predicate: impl Fn(&ResolvedDist) -> bool) -> Self {
82+
for node in self.graph.node_weights_mut() {
83+
if let Node::Dist { dist, install, .. } = node {
84+
if !predicate(dist) {
85+
*install = false;
86+
}
87+
}
7888
}
89+
self
7990
}
8091

8192
/// Map over the resolved distributions in this resolution.
93+
///
94+
/// For efficiency, the map function should return `None` if the resolved distribution is
95+
/// unchanged.
8296
#[must_use]
83-
pub fn map(self, predicate: impl Fn(ResolvedDist) -> ResolvedDist) -> Self {
84-
let packages = self
85-
.packages
86-
.into_iter()
87-
.map(|(name, dist)| (name, predicate(dist)))
88-
.collect::<BTreeMap<_, _>>();
89-
let hashes = self
90-
.hashes
91-
.into_iter()
92-
.filter(|(name, _)| packages.contains_key(name))
93-
.collect();
94-
let diagnostics = self.diagnostics.clone();
95-
Self {
96-
packages,
97-
hashes,
98-
diagnostics,
97+
pub fn map(mut self, predicate: impl Fn(&ResolvedDist) -> Option<ResolvedDist>) -> Self {
98+
for node in self.graph.node_weights_mut() {
99+
if let Node::Dist { dist, .. } = node {
100+
if let Some(transformed) = predicate(dist) {
101+
*dist = transformed;
102+
}
103+
}
99104
}
105+
self
100106
}
101107
}
102108

@@ -166,15 +172,48 @@ impl Diagnostic for ResolutionDiagnostic {
166172
}
167173
}
168174

175+
/// A node in the resolution, along with whether its been filtered out.
176+
///
177+
/// This is similar to [`ResolutionGraph`], but represents a resolution for a single platform.
178+
///
179+
/// We retain filtered nodes as we still need to be able to trace dependencies through the graph
180+
/// (e.g., to determine why a package was install in the resolution).
181+
#[derive(Debug, Clone)]
182+
pub enum Node {
183+
Root,
184+
Dist {
185+
dist: ResolvedDist,
186+
hashes: Vec<HashDigest>,
187+
install: bool,
188+
},
189+
}
190+
191+
/// An edge in the resolution graph, along with the marker that must be satisfied to traverse it.
192+
#[derive(Debug, Clone)]
193+
pub enum Edge {
194+
Prod(MarkerTree),
195+
Optional(ExtraName, MarkerTree),
196+
Dev(GroupName, MarkerTree),
197+
}
198+
199+
impl Edge {
200+
/// Return the [`MarkerTree`] for this edge.
201+
pub fn marker(&self) -> &MarkerTree {
202+
match self {
203+
Self::Prod(marker) => marker,
204+
Self::Optional(_, marker) => marker,
205+
Self::Dev(_, marker) => marker,
206+
}
207+
}
208+
}
209+
169210
impl From<&ResolvedDist> for RequirementSource {
170211
fn from(resolved_dist: &ResolvedDist) -> Self {
171212
match resolved_dist {
172-
ResolvedDist::Installable { dist, .. } => match dist {
213+
ResolvedDist::Installable { dist, version } => match dist {
173214
Dist::Built(BuiltDist::Registry(wheels)) => RequirementSource::Registry {
174215
specifier: uv_pep440::VersionSpecifiers::from(
175-
uv_pep440::VersionSpecifier::equals_version(
176-
wheels.best_wheel().filename.version.clone(),
177-
),
216+
uv_pep440::VersionSpecifier::equals_version(version.clone()),
178217
),
179218
index: Some(wheels.best_wheel().index.url().clone()),
180219
},

crates/uv-resolver/src/lock/requirements_txt.rs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::path::{Component, Path, PathBuf};
66

77
use either::Either;
88
use petgraph::visit::IntoNodeReferences;
9-
use petgraph::{Directed, Graph};
9+
use petgraph::Graph;
1010
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
1111
use url::Url;
1212

@@ -22,8 +22,6 @@ use crate::graph_ops::marker_reachability;
2222
use crate::lock::{Package, PackageId, Source};
2323
use crate::{InstallTarget, LockError};
2424

25-
type LockGraph<'lock> = Graph<Node<'lock>, Edge, Directed>;
26-
2725
/// An export of a [`Lock`] that renders in `requirements.txt` format.
2826
#[derive(Debug)]
2927
pub struct RequirementsTxtExport<'lock> {
@@ -42,7 +40,7 @@ impl<'lock> RequirementsTxtExport<'lock> {
4240
install_options: &'lock InstallOptions,
4341
) -> Result<Self, LockError> {
4442
let size_guess = target.lock().packages.len();
45-
let mut petgraph = LockGraph::with_capacity(size_guess, size_guess);
43+
let mut petgraph = Graph::with_capacity(size_guess, size_guess);
4644
let mut inverse = FxHashMap::with_capacity_and_hasher(size_guess, FxBuildHasher);
4745

4846
let mut queue: VecDeque<(&Package, Option<&ExtraName>)> = VecDeque::new();
@@ -282,16 +280,13 @@ impl std::fmt::Display for RequirementsTxtExport<'_> {
282280
}
283281
}
284282

285-
/// A node in the [`LockGraph`].
283+
/// A node in the graph.
286284
#[derive(Debug, Clone, PartialEq, Eq)]
287285
enum Node<'lock> {
288286
Root,
289287
Package(&'lock Package),
290288
}
291289

292-
/// The edges of the [`LockGraph`].
293-
type Edge = MarkerTree;
294-
295290
/// A flat requirement, with its associated marker.
296291
#[derive(Debug, Clone, PartialEq, Eq)]
297292
struct Requirement<'lock> {

0 commit comments

Comments
 (0)