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#[derive(Debug, Default, Clone)]
15pub struct Resolution {
16 graph: petgraph::graph::DiGraph<Node, Edge>,
17 diagnostics: Vec<ResolutionDiagnostic>,
18}
19
20impl Resolution {
21 pub fn new(graph: petgraph::graph::DiGraph<Node, Edge>) -> Self {
23 Self {
24 graph,
25 diagnostics: Vec::new(),
26 }
27 }
28
29 pub fn graph(&self) -> &petgraph::graph::DiGraph<Node, Edge> {
31 &self.graph
32 }
33
34 #[must_use]
36 pub fn with_diagnostics(mut self, diagnostics: Vec<ResolutionDiagnostic>) -> Self {
37 self.diagnostics.extend(diagnostics);
38 self
39 }
40
41 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 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 pub fn len(&self) -> usize {
69 self.distributions().count()
70 }
71
72 pub fn is_empty(&self) -> bool {
74 self.distributions().next().is_none()
75 }
76
77 pub fn diagnostics(&self) -> &[ResolutionDiagnostic] {
79 &self.diagnostics
80 }
81
82 #[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 #[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 dist: ResolvedDist,
118 extra: ExtraName,
120 },
121 MissingGroup {
122 dist: ResolvedDist,
124 group: GroupName,
126 },
127 YankedVersion {
128 dist: ResolvedDist,
130 reason: Option<String>,
132 },
133 MissingLowerBound {
134 package_name: PackageName,
137 },
138}
139
140impl Diagnostic for ResolutionDiagnostic {
141 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 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#[derive(Debug, Clone)]
185pub enum Node {
186 Root,
187 Dist {
188 dist: ResolvedDist,
189 hashes: HashDigests,
190 install: bool,
191 },
192}
193
194#[derive(Debug, Clone)]
196pub enum Edge {
197 Prod,
198 Optional(ExtraName),
199 Dev(GroupName),
200}
201
202impl From<&ResolvedDist> for RequirementSource {
203 fn from(resolved_dist: &ResolvedDist) -> Self {
204 match resolved_dist {
205 ResolvedDist::Installable { dist, .. } => match dist.as_ref() {
206 Dist::Built(BuiltDist::Registry(wheels)) => {
207 let wheel = wheels.best_wheel();
208 Self::Registry {
209 specifier: uv_pep440::VersionSpecifiers::from(
210 uv_pep440::VersionSpecifier::equals_version(
211 wheel.filename.version.clone(),
212 ),
213 ),
214 index: Some(IndexMetadata::from(wheel.index.clone())),
215 conflict: None,
216 }
217 }
218 Dist::Built(BuiltDist::DirectUrl(wheel)) => {
219 let mut location = wheel.url.to_url();
220 location.set_fragment(None);
221 Self::Url {
222 url: wheel.url.clone(),
223 location,
224 subdirectory: None,
225 ext: DistExtension::Wheel,
226 }
227 }
228 Dist::Built(BuiltDist::Path(wheel)) => Self::Path {
229 install_path: wheel.install_path.clone(),
230 url: wheel.url.clone(),
231 ext: DistExtension::Wheel,
232 },
233 Dist::Built(BuiltDist::GitPath(wheel)) => Self::GitPath {
234 url: wheel.url.clone(),
235 git: (*wheel.git).clone(),
236 install_path: wheel.install_path.clone(),
237 ext: DistExtension::Wheel,
238 },
239 Dist::Source(SourceDist::Registry(sdist)) => Self::Registry {
240 specifier: uv_pep440::VersionSpecifiers::from(
241 uv_pep440::VersionSpecifier::equals_version(sdist.version.clone()),
242 ),
243 index: Some(IndexMetadata::from(sdist.index.clone())),
244 conflict: None,
245 },
246 Dist::Source(SourceDist::DirectUrl(sdist)) => {
247 let mut location = sdist.url.to_url();
248 location.set_fragment(None);
249 Self::Url {
250 url: sdist.url.clone(),
251 location,
252 subdirectory: sdist.subdirectory.clone(),
253 ext: DistExtension::Source(sdist.ext),
254 }
255 }
256 Dist::Source(SourceDist::GitPath(sdist)) => Self::GitPath {
257 url: sdist.url.clone(),
258 git: (*sdist.git).clone(),
259 install_path: sdist.install_path.clone(),
260 ext: DistExtension::Source(sdist.ext),
261 },
262 Dist::Source(SourceDist::GitDirectory(sdist)) => Self::GitDirectory {
263 git: (*sdist.git).clone(),
264 url: sdist.url.clone(),
265 subdirectory: sdist.subdirectory.clone(),
266 },
267 Dist::Source(SourceDist::Path(sdist)) => Self::Path {
268 install_path: sdist.install_path.clone(),
269 url: sdist.url.clone(),
270 ext: DistExtension::Source(sdist.ext),
271 },
272 Dist::Source(SourceDist::Directory(sdist)) => Self::Directory {
273 install_path: sdist.install_path.clone(),
274 url: sdist.url.clone(),
275 editable: sdist.editable,
276 r#virtual: sdist.r#virtual,
277 },
278 },
279 ResolvedDist::Installed { dist } => Self::Registry {
280 specifier: uv_pep440::VersionSpecifiers::from(
281 uv_pep440::VersionSpecifier::equals_version(dist.version().clone()),
282 ),
283 index: None,
284 conflict: None,
285 },
286 }
287 }
288}