1use std::collections::BTreeSet;
2
3use owo_colors::OwoColorize;
4use petgraph::visit::EdgeRef;
5use petgraph::{Directed, Direction, Graph};
6use rustc_hash::{FxBuildHasher, FxHashMap};
7
8use uv_distribution_types::{DistributionMetadata, Name, SourceAnnotation, SourceAnnotations};
9use uv_normalize::PackageName;
10use uv_pep508::MarkerTree;
11
12use crate::resolution::{RequirementsTxtDist, ResolutionGraphNode};
13use crate::{ResolverEnvironment, ResolverOutput};
14
15#[derive(Debug)]
17pub struct DisplayResolutionGraph<'a> {
18 resolution: &'a ResolverOutput,
20 env: &'a ResolverEnvironment,
22 no_emit_packages: &'a [PackageName],
24 show_hashes: bool,
26 include_extras: bool,
28 include_markers: bool,
30 include_annotations: bool,
33 include_index_annotation: bool,
35 annotation_style: AnnotationStyle,
38}
39
40#[derive(Debug)]
41enum DisplayResolutionGraphNode<'dist> {
42 Root,
43 Dist(RequirementsTxtDist<'dist>),
44}
45
46impl<'a> DisplayResolutionGraph<'a> {
47 #[allow(clippy::fn_params_excessive_bools)]
54 pub fn new(
55 underlying: &'a ResolverOutput,
56 env: &'a ResolverEnvironment,
57 no_emit_packages: &'a [PackageName],
58 show_hashes: bool,
59 include_extras: bool,
60 include_markers: bool,
61 include_annotations: bool,
62 include_index_annotation: bool,
63 annotation_style: AnnotationStyle,
64 ) -> Self {
65 for fork_marker in &underlying.fork_markers {
66 assert!(
67 fork_marker.conflict().is_true(),
68 "found fork marker {fork_marker:?} with non-trivial conflicting marker, \
69 cannot display resolver output with conflicts in requirements.txt format",
70 );
71 }
72 Self {
73 resolution: underlying,
74 env,
75 no_emit_packages,
76 show_hashes,
77 include_extras,
78 include_markers,
79 include_annotations,
80 include_index_annotation,
81 annotation_style,
82 }
83 }
84}
85
86impl std::fmt::Display for DisplayResolutionGraph<'_> {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 let sources = if self.include_annotations {
91 let mut sources = SourceAnnotations::default();
92
93 for requirement in self.resolution.requirements.iter().filter(|requirement| {
94 requirement.evaluate_markers(self.env.marker_environment(), &[])
95 }) {
96 if let Some(origin) = &requirement.origin {
97 sources.add(
98 &requirement.name,
99 SourceAnnotation::Requirement(origin.clone()),
100 );
101 }
102 }
103
104 for requirement in self
105 .resolution
106 .constraints
107 .requirements()
108 .filter(|requirement| {
109 requirement.evaluate_markers(self.env.marker_environment(), &[])
110 })
111 {
112 if let Some(origin) = &requirement.origin {
113 sources.add(
114 &requirement.name,
115 SourceAnnotation::Constraint(origin.clone()),
116 );
117 }
118 }
119
120 for requirement in self
121 .resolution
122 .overrides
123 .requirements()
124 .filter(|requirement| {
125 requirement.evaluate_markers(self.env.marker_environment(), &[])
126 })
127 {
128 if let Some(origin) = &requirement.origin {
129 sources.add(
130 &requirement.name,
131 SourceAnnotation::Override(origin.clone()),
132 );
133 }
134 }
135
136 sources
137 } else {
138 SourceAnnotations::default()
139 };
140
141 let graph = self.resolution.graph.map(
149 |_index, node| match node {
150 ResolutionGraphNode::Root => DisplayResolutionGraphNode::Root,
151 ResolutionGraphNode::Dist(dist) => {
152 let dist = RequirementsTxtDist::from_annotated_dist(dist);
153 DisplayResolutionGraphNode::Dist(dist)
154 }
155 },
156 |_index, _edge| (),
159 );
160
161 let graph = if self.include_extras {
163 combine_extras(&graph)
164 } else {
165 strip_extras(&graph)
166 };
167
168 let mut nodes = graph
170 .node_indices()
171 .filter_map(|index| {
172 let dist = &graph[index];
173 let name = dist.name();
174 if self.no_emit_packages.contains(name) {
175 return None;
176 }
177
178 Some((index, dist))
179 })
180 .collect::<Vec<_>>();
181
182 nodes.sort_unstable_by_key(|(index, node)| (node.to_comparator(), *index));
184
185 for (index, node) in nodes {
187 let mut line = node
189 .to_requirements_txt(&self.resolution.requires_python, self.include_markers)
190 .to_string();
191
192 let mut has_hashes = false;
194 if self.show_hashes {
195 for hash in node.hashes {
196 has_hashes = true;
197 line.push_str(" \\\n");
198 line.push_str(" --hash=");
199 line.push_str(&hash.to_string());
200 }
201 }
202
203 let mut annotation = None;
205
206 if self.include_annotations {
209 let dependents = {
211 let mut dependents = graph
212 .edges_directed(index, Direction::Incoming)
213 .map(|edge| &graph[edge.source()])
214 .map(uv_distribution_types::Name::name)
215 .collect::<Vec<_>>();
216 dependents.sort_unstable();
217 dependents.dedup();
218 dependents
219 };
220
221 let default = BTreeSet::default();
223 let source = sources.get(node.name()).unwrap_or(&default);
224
225 match self.annotation_style {
226 AnnotationStyle::Line => match dependents.as_slice() {
227 [] if source.is_empty() => {}
228 [] if source.len() == 1 => {
229 let separator = if has_hashes { "\n " } else { " " };
230 let comment = format!("# via {}", source.iter().next().unwrap())
231 .green()
232 .to_string();
233 annotation = Some((separator, comment));
234 }
235 dependents => {
236 let separator = if has_hashes { "\n " } else { " " };
237 let dependents = dependents
238 .iter()
239 .map(ToString::to_string)
240 .chain(source.iter().map(ToString::to_string))
241 .collect::<Vec<_>>()
242 .join(", ");
243 let comment = format!("# via {dependents}").green().to_string();
244 annotation = Some((separator, comment));
245 }
246 },
247 AnnotationStyle::Split => match dependents.as_slice() {
248 [] if source.is_empty() => {}
249 [] if source.len() == 1 => {
250 let separator = "\n";
251 let comment = format!(" # via {}", source.iter().next().unwrap())
252 .green()
253 .to_string();
254 annotation = Some((separator, comment));
255 }
256 [dependent] if source.is_empty() => {
257 let separator = "\n";
258 let comment = format!(" # via {dependent}").green().to_string();
259 annotation = Some((separator, comment));
260 }
261 dependents => {
262 let separator = "\n";
263 let dependent = source
264 .iter()
265 .map(ToString::to_string)
266 .chain(dependents.iter().map(ToString::to_string))
267 .map(|name| format!(" # {name}"))
268 .collect::<Vec<_>>()
269 .join("\n");
270 let comment = format!(" # via\n{dependent}").green().to_string();
271 annotation = Some((separator, comment));
272 }
273 },
274 }
275 }
276
277 if let Some((separator, comment)) = annotation {
278 for line in format!("{line:24}{separator}{comment}").lines() {
280 let line = line.trim_end();
281 writeln!(f, "{line}")?;
282 }
283 } else {
284 writeln!(f, "{line}")?;
286 }
287
288 if self.include_index_annotation {
291 if let Some(index) = node.dist.index() {
292 let url = index.without_credentials();
293 writeln!(f, "{}", format!(" # from {url}").green())?;
294 }
295 }
296 }
297
298 Ok(())
299 }
300}
301
302#[derive(Debug, Default, Copy, Clone, PartialEq, serde::Deserialize)]
305#[serde(deny_unknown_fields, rename_all = "kebab-case")]
306#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
307#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
308pub enum AnnotationStyle {
309 Line,
311 #[default]
313 Split,
314}
315
316type IntermediatePetGraph<'dist> = Graph<DisplayResolutionGraphNode<'dist>, (), Directed>;
318
319type RequirementsTxtGraph<'dist> = Graph<RequirementsTxtDist<'dist>, (), Directed>;
320
321fn combine_extras<'dist>(graph: &IntermediatePetGraph<'dist>) -> RequirementsTxtGraph<'dist> {
333 fn version_marker<'dist>(dist: &'dist RequirementsTxtDist) -> (&'dist PackageName, MarkerTree) {
335 (dist.name(), dist.markers)
336 }
337
338 let mut next = RequirementsTxtGraph::with_capacity(graph.node_count(), graph.edge_count());
339 let mut inverse = FxHashMap::with_capacity_and_hasher(graph.node_count(), FxBuildHasher);
340
341 for index in graph.node_indices() {
343 let DisplayResolutionGraphNode::Dist(dist) = &graph[index] else {
344 continue;
345 };
346
347 match inverse.entry(version_marker(dist)) {
350 std::collections::hash_map::Entry::Occupied(entry) => {
351 let index = *entry.get();
352 let node: &mut RequirementsTxtDist = &mut next[index];
353 node.extras.extend(dist.extras.iter().cloned());
354 node.extras.sort_unstable();
355 node.extras.dedup();
356 }
357 std::collections::hash_map::Entry::Vacant(entry) => {
358 let index = next.add_node(dist.clone());
359 entry.insert(index);
360 }
361 }
362 }
363
364 for edge in graph.edge_indices() {
366 let (source, target) = graph.edge_endpoints(edge).unwrap();
367 let DisplayResolutionGraphNode::Dist(source_node) = &graph[source] else {
368 continue;
369 };
370 let DisplayResolutionGraphNode::Dist(target_node) = &graph[target] else {
371 continue;
372 };
373 let source = inverse[&version_marker(source_node)];
374 let target = inverse[&version_marker(target_node)];
375
376 next.update_edge(source, target, ());
377 }
378
379 next
380}
381
382fn strip_extras<'dist>(graph: &IntermediatePetGraph<'dist>) -> RequirementsTxtGraph<'dist> {
390 let mut next = RequirementsTxtGraph::with_capacity(graph.node_count(), graph.edge_count());
391 let mut inverse = FxHashMap::with_capacity_and_hasher(graph.node_count(), FxBuildHasher);
392
393 for index in graph.node_indices() {
395 let DisplayResolutionGraphNode::Dist(dist) = &graph[index] else {
396 continue;
397 };
398
399 match inverse.entry(dist.version_id()) {
402 std::collections::hash_map::Entry::Occupied(entry) => {
403 let index = *entry.get();
404 let node: &mut RequirementsTxtDist = &mut next[index];
405 node.extras.clear();
406 node.markers.or(dist.markers);
413 }
414 std::collections::hash_map::Entry::Vacant(entry) => {
415 let index = next.add_node(dist.clone());
416 entry.insert(index);
417 }
418 }
419 }
420
421 for edge in graph.edge_indices() {
423 let (source, target) = graph.edge_endpoints(edge).unwrap();
424 let DisplayResolutionGraphNode::Dist(source_node) = &graph[source] else {
425 continue;
426 };
427 let DisplayResolutionGraphNode::Dist(target_node) = &graph[target] else {
428 continue;
429 };
430 let source = inverse[&source_node.version_id()];
431 let target = inverse[&target_node.version_id()];
432
433 next.update_edge(source, target, ());
434 }
435
436 next
437}