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
194impl Node {
195 pub fn install(&self) -> bool {
197 match self {
198 Self::Root => false,
199 Self::Dist { install, .. } => *install,
200 }
201 }
202}
203
204#[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}