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_configuration::AnnotationStyle;
9use uv_distribution_types::{DistributionMetadata, Name, SourceAnnotation, SourceAnnotations};
10use uv_normalize::PackageName;
11use uv_pep508::MarkerTree;
12
13use crate::resolution::{RequirementsTxtDist, ResolutionGraphNode};
14use crate::{ResolverEnvironment, ResolverOutput};
15
16#[derive(Debug)]
18pub struct DisplayResolutionGraph<'a> {
19 resolution: &'a ResolverOutput,
21 env: &'a ResolverEnvironment,
23 no_emit_packages: &'a [PackageName],
25 show_hashes: bool,
27 include_extras: bool,
29 include_markers: bool,
31 include_annotations: bool,
34 include_index_annotation: bool,
36 annotation_style: AnnotationStyle,
39}
40
41#[derive(Debug)]
42enum DisplayResolutionGraphNode<'dist> {
43 Root,
44 Dist(RequirementsTxtDist<'dist>),
45}
46
47impl<'a> DisplayResolutionGraph<'a> {
48 #[expect(clippy::fn_params_excessive_bools)]
55 pub fn new(
56 underlying: &'a ResolverOutput,
57 env: &'a ResolverEnvironment,
58 no_emit_packages: &'a [PackageName],
59 show_hashes: bool,
60 include_extras: bool,
61 include_markers: bool,
62 include_annotations: bool,
63 include_index_annotation: bool,
64 annotation_style: AnnotationStyle,
65 ) -> Self {
66 for fork_marker in &underlying.fork_markers {
67 assert!(
68 fork_marker.conflict().is_true(),
69 "found fork marker {fork_marker:?} with non-trivial conflicting marker, \
70 cannot display resolver output with conflicts in requirements.txt format",
71 );
72 }
73 Self {
74 resolution: underlying,
75 env,
76 no_emit_packages,
77 show_hashes,
78 include_extras,
79 include_markers,
80 include_annotations,
81 include_index_annotation,
82 annotation_style,
83 }
84 }
85}
86
87impl std::fmt::Display for DisplayResolutionGraph<'_> {
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 let sources = if self.include_annotations {
92 let mut sources = SourceAnnotations::default();
93
94 for requirement in self.resolution.requirements.iter().filter(|requirement| {
95 requirement.evaluate_markers(self.env.marker_environment(), &[])
96 }) {
97 if let Some(origin) = &requirement.origin {
98 sources.add(
99 &requirement.name,
100 SourceAnnotation::Requirement(origin.clone()),
101 );
102 }
103 }
104
105 for requirement in self
106 .resolution
107 .constraints
108 .requirements()
109 .filter(|requirement| {
110 requirement.evaluate_markers(self.env.marker_environment(), &[])
111 })
112 {
113 if let Some(origin) = &requirement.origin {
114 sources.add(
115 &requirement.name,
116 SourceAnnotation::Constraint(origin.clone()),
117 );
118 }
119 }
120
121 for requirement in self
122 .resolution
123 .modifiers
124 .global_overrides()
125 .filter(|requirement| {
126 requirement.evaluate_markers(self.env.marker_environment(), &[])
127 })
128 {
129 if let Some(origin) = &requirement.origin {
130 sources.add(
131 &requirement.name,
132 SourceAnnotation::Override(origin.clone()),
133 );
134 }
135 }
136
137 for edge in self.resolution.graph.edge_references() {
138 let (ResolutionGraphNode::Dist(parent), ResolutionGraphNode::Dist(dependency)) = (
139 self.resolution.graph.node_weight(edge.source()).unwrap(),
140 self.resolution.graph.node_weight(edge.target()).unwrap(),
141 ) else {
142 continue;
143 };
144 for requirement in self
145 .resolution
146 .modifiers
147 .scoped_overrides_for(&parent.name, &parent.version)
148 .filter(|requirement| requirement.name == dependency.name)
149 .filter(|requirement| {
150 requirement.evaluate_markers(self.env.marker_environment(), &[])
151 })
152 {
153 if let Some(origin) = &requirement.origin {
154 sources.add(
155 &requirement.name,
156 SourceAnnotation::Override(origin.clone()),
157 );
158 }
159 }
160 }
161
162 sources
163 } else {
164 SourceAnnotations::default()
165 };
166
167 let graph = self.resolution.graph.map(
175 |_index, node| match node {
176 ResolutionGraphNode::Root => DisplayResolutionGraphNode::Root,
177 ResolutionGraphNode::Dist(dist) => {
178 let dist = RequirementsTxtDist::from_annotated_dist(dist);
179 DisplayResolutionGraphNode::Dist(dist)
180 }
181 },
182 |_index, _edge| (),
185 );
186
187 let graph = if self.include_extras {
189 combine_extras(&graph)
190 } else {
191 strip_extras(&graph)
192 };
193
194 let mut nodes = graph
196 .node_indices()
197 .filter_map(|index| {
198 let dist = &graph[index];
199 let name = dist.name();
200 if self.no_emit_packages.contains(name) {
201 return None;
202 }
203
204 Some((index, dist))
205 })
206 .collect::<Vec<_>>();
207
208 nodes.sort_unstable_by_key(|(index, node)| (node.to_comparator(), *index));
210
211 for (index, node) in nodes {
213 let mut line = node
215 .to_requirements_txt(&self.resolution.requires_python, self.include_markers)
216 .to_string();
217
218 let mut has_hashes = false;
220 if self.show_hashes {
221 for hash in node.hashes {
222 has_hashes = true;
223 line.push_str(" \\\n");
224 line.push_str(" --hash=");
225 line.push_str(&hash.to_string());
226 }
227 }
228
229 let mut annotation = None;
231
232 if self.include_annotations {
235 let dependents = {
237 let mut dependents = graph
238 .edges_directed(index, Direction::Incoming)
239 .map(|edge| &graph[edge.source()])
240 .map(uv_distribution_types::Name::name)
241 .collect::<Vec<_>>();
242 dependents.sort_unstable();
243 dependents.dedup();
244 dependents
245 };
246
247 let default = BTreeSet::default();
249 let source = sources.get(node.name()).unwrap_or(&default);
250
251 match self.annotation_style {
252 AnnotationStyle::Line => match dependents.as_slice() {
253 [] if source.is_empty() => {}
254 [] if source.len() == 1 => {
255 let separator = if has_hashes { "\n " } else { " " };
256 let comment = format!("# via {}", source.iter().next().unwrap())
257 .green()
258 .to_string();
259 annotation = Some((separator, comment));
260 }
261 dependents => {
262 let separator = if has_hashes { "\n " } else { " " };
263 let dependents = dependents
264 .iter()
265 .map(ToString::to_string)
266 .chain(source.iter().map(ToString::to_string))
267 .collect::<Vec<_>>()
268 .join(", ");
269 let comment = format!("# via {dependents}").green().to_string();
270 annotation = Some((separator, comment));
271 }
272 },
273 AnnotationStyle::Split => match dependents.as_slice() {
274 [] if source.is_empty() => {}
275 [] if source.len() == 1 => {
276 let separator = "\n";
277 let comment = format!(" # via {}", source.iter().next().unwrap())
278 .green()
279 .to_string();
280 annotation = Some((separator, comment));
281 }
282 [dependent] if source.is_empty() => {
283 let separator = "\n";
284 let comment = format!(" # via {dependent}").green().to_string();
285 annotation = Some((separator, comment));
286 }
287 dependents => {
288 let separator = "\n";
289 let dependent = source
290 .iter()
291 .map(ToString::to_string)
292 .chain(dependents.iter().map(ToString::to_string))
293 .map(|name| format!(" # {name}"))
294 .collect::<Vec<_>>()
295 .join("\n");
296 let comment = format!(" # via\n{dependent}").green().to_string();
297 annotation = Some((separator, comment));
298 }
299 },
300 }
301 }
302
303 if let Some((separator, comment)) = annotation {
304 for line in format!("{line:24}{separator}{comment}").lines() {
306 let line = line.trim_end();
307 writeln!(f, "{line}")?;
308 }
309 } else {
310 writeln!(f, "{line}")?;
312 }
313
314 if self.include_index_annotation {
317 if let Some(index) = node.dist.index() {
318 let url = index.without_credentials();
319 writeln!(f, "{}", format!(" # from {url}").green())?;
320 }
321 }
322 }
323
324 Ok(())
325 }
326}
327
328type IntermediatePetGraph<'dist> = Graph<DisplayResolutionGraphNode<'dist>, (), Directed>;
330
331type RequirementsTxtGraph<'dist> = Graph<RequirementsTxtDist<'dist>, (), Directed>;
332
333fn combine_extras<'dist>(graph: &IntermediatePetGraph<'dist>) -> RequirementsTxtGraph<'dist> {
345 fn version_marker<'dist>(dist: &'dist RequirementsTxtDist) -> (&'dist PackageName, MarkerTree) {
347 (dist.name(), dist.markers)
348 }
349
350 let mut next = RequirementsTxtGraph::with_capacity(graph.node_count(), graph.edge_count());
351 let mut inverse = FxHashMap::with_capacity_and_hasher(graph.node_count(), FxBuildHasher);
352
353 for index in graph.node_indices() {
355 let DisplayResolutionGraphNode::Dist(dist) = &graph[index] else {
356 continue;
357 };
358
359 match inverse.entry(version_marker(dist)) {
362 std::collections::hash_map::Entry::Occupied(entry) => {
363 let index = *entry.get();
364 let node: &mut RequirementsTxtDist = &mut next[index];
365 node.extras.extend(dist.extras.iter().cloned());
366 node.extras.sort_unstable();
367 node.extras.dedup();
368 }
369 std::collections::hash_map::Entry::Vacant(entry) => {
370 let index = next.add_node(dist.clone());
371 entry.insert(index);
372 }
373 }
374 }
375
376 for edge in graph.edge_indices() {
378 let (source, target) = graph.edge_endpoints(edge).unwrap();
379 let DisplayResolutionGraphNode::Dist(source_node) = &graph[source] else {
380 continue;
381 };
382 let DisplayResolutionGraphNode::Dist(target_node) = &graph[target] else {
383 continue;
384 };
385 let source = inverse[&version_marker(source_node)];
386 let target = inverse[&version_marker(target_node)];
387
388 next.update_edge(source, target, ());
389 }
390
391 next
392}
393
394fn strip_extras<'dist>(graph: &IntermediatePetGraph<'dist>) -> RequirementsTxtGraph<'dist> {
402 let mut next = RequirementsTxtGraph::with_capacity(graph.node_count(), graph.edge_count());
403 let mut inverse = FxHashMap::with_capacity_and_hasher(graph.node_count(), FxBuildHasher);
404
405 for index in graph.node_indices() {
407 let DisplayResolutionGraphNode::Dist(dist) = &graph[index] else {
408 continue;
409 };
410
411 match inverse.entry(dist.version_id()) {
414 std::collections::hash_map::Entry::Occupied(entry) => {
415 let index = *entry.get();
416 let node: &mut RequirementsTxtDist = &mut next[index];
417 node.extras.clear();
418 node.markers = node.markers.or(dist.markers);
425 }
426 std::collections::hash_map::Entry::Vacant(entry) => {
427 let index = next.add_node(dist.clone());
428 entry.insert(index);
429 }
430 }
431 }
432
433 for edge in graph.edge_indices() {
435 let (source, target) = graph.edge_endpoints(edge).unwrap();
436 let DisplayResolutionGraphNode::Dist(source_node) = &graph[source] else {
437 continue;
438 };
439 let DisplayResolutionGraphNode::Dist(target_node) = &graph[target] else {
440 continue;
441 };
442 let source = inverse[&source_node.version_id()];
443 let target = inverse[&target_node.version_id()];
444
445 next.update_edge(source, target, ());
446 }
447
448 next
449}