uv_distribution_types/
resolution.rs

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