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 #[expect(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
121 self.resolution
122 .overrides
123 .global_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 for edge in self.resolution.graph.edge_references() {
137 let (ResolutionGraphNode::Dist(parent), ResolutionGraphNode::Dist(dependency)) = (
138 self.resolution.graph.node_weight(edge.source()).unwrap(),
139 self.resolution.graph.node_weight(edge.target()).unwrap(),
140 ) else {
141 continue;
142 };
143 for requirement in self
144 .resolution
145 .overrides
146 .scoped_requirements_for(&parent.name, &parent.version)
147 .filter(|requirement| requirement.name == dependency.name)
148 .filter(|requirement| {
149 requirement.evaluate_markers(self.env.marker_environment(), &[])
150 })
151 {
152 if let Some(origin) = &requirement.origin {
153 sources.add(
154 &requirement.name,
155 SourceAnnotation::Override(origin.clone()),
156 );
157 }
158 }
159 }
160
161 sources
162 } else {
163 SourceAnnotations::default()
164 };
165
166 let graph = self.resolution.graph.map(
174 |_index, node| match node {
175 ResolutionGraphNode::Root => DisplayResolutionGraphNode::Root,
176 ResolutionGraphNode::Dist(dist) => {
177 let dist = RequirementsTxtDist::from_annotated_dist(dist);
178 DisplayResolutionGraphNode::Dist(dist)
179 }
180 },
181 |_index, _edge| (),
184 );
185
186 let graph = if self.include_extras {
188 combine_extras(&graph)
189 } else {
190 strip_extras(&graph)
191 };
192
193 let mut nodes = graph
195 .node_indices()
196 .filter_map(|index| {
197 let dist = &graph[index];
198 let name = dist.name();
199 if self.no_emit_packages.contains(name) {
200 return None;
201 }
202
203 Some((index, dist))
204 })
205 .collect::<Vec<_>>();
206
207 nodes.sort_unstable_by_key(|(index, node)| (node.to_comparator(), *index));
209
210 for (index, node) in nodes {
212 let mut line = node
214 .to_requirements_txt(&self.resolution.requires_python, self.include_markers)
215 .to_string();
216
217 let mut has_hashes = false;
219 if self.show_hashes {
220 for hash in node.hashes {
221 has_hashes = true;
222 line.push_str(" \\\n");
223 line.push_str(" --hash=");
224 line.push_str(&hash.to_string());
225 }
226 }
227
228 let mut annotation = None;
230
231 if self.include_annotations {
234 let dependents = {
236 let mut dependents = graph
237 .edges_directed(index, Direction::Incoming)
238 .map(|edge| &graph[edge.source()])
239 .map(uv_distribution_types::Name::name)
240 .collect::<Vec<_>>();
241 dependents.sort_unstable();
242 dependents.dedup();
243 dependents
244 };
245
246 let default = BTreeSet::default();
248 let source = sources.get(node.name()).unwrap_or(&default);
249
250 match self.annotation_style {
251 AnnotationStyle::Line => match dependents.as_slice() {
252 [] if source.is_empty() => {}
253 [] if source.len() == 1 => {
254 let separator = if has_hashes { "\n " } else { " " };
255 let comment = format!("# via {}", source.iter().next().unwrap())
256 .green()
257 .to_string();
258 annotation = Some((separator, comment));
259 }
260 dependents => {
261 let separator = if has_hashes { "\n " } else { " " };
262 let dependents = dependents
263 .iter()
264 .map(ToString::to_string)
265 .chain(source.iter().map(ToString::to_string))
266 .collect::<Vec<_>>()
267 .join(", ");
268 let comment = format!("# via {dependents}").green().to_string();
269 annotation = Some((separator, comment));
270 }
271 },
272 AnnotationStyle::Split => match dependents.as_slice() {
273 [] if source.is_empty() => {}
274 [] if source.len() == 1 => {
275 let separator = "\n";
276 let comment = format!(" # via {}", source.iter().next().unwrap())
277 .green()
278 .to_string();
279 annotation = Some((separator, comment));
280 }
281 [dependent] if source.is_empty() => {
282 let separator = "\n";
283 let comment = format!(" # via {dependent}").green().to_string();
284 annotation = Some((separator, comment));
285 }
286 dependents => {
287 let separator = "\n";
288 let dependent = source
289 .iter()
290 .map(ToString::to_string)
291 .chain(dependents.iter().map(ToString::to_string))
292 .map(|name| format!(" # {name}"))
293 .collect::<Vec<_>>()
294 .join("\n");
295 let comment = format!(" # via\n{dependent}").green().to_string();
296 annotation = Some((separator, comment));
297 }
298 },
299 }
300 }
301
302 if let Some((separator, comment)) = annotation {
303 for line in format!("{line:24}{separator}{comment}").lines() {
305 let line = line.trim_end();
306 writeln!(f, "{line}")?;
307 }
308 } else {
309 writeln!(f, "{line}")?;
311 }
312
313 if self.include_index_annotation {
316 if let Some(index) = node.dist.index() {
317 let url = index.without_credentials();
318 writeln!(f, "{}", format!(" # from {url}").green())?;
319 }
320 }
321 }
322
323 Ok(())
324 }
325}
326
327#[derive(Debug, Default, Copy, Clone, PartialEq, serde::Deserialize)]
330#[serde(deny_unknown_fields, rename_all = "kebab-case")]
331#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
332#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
333pub enum AnnotationStyle {
334 Line,
336 #[default]
338 Split,
339}
340
341type IntermediatePetGraph<'dist> = Graph<DisplayResolutionGraphNode<'dist>, (), Directed>;
343
344type RequirementsTxtGraph<'dist> = Graph<RequirementsTxtDist<'dist>, (), Directed>;
345
346fn combine_extras<'dist>(graph: &IntermediatePetGraph<'dist>) -> RequirementsTxtGraph<'dist> {
358 fn version_marker<'dist>(dist: &'dist RequirementsTxtDist) -> (&'dist PackageName, MarkerTree) {
360 (dist.name(), dist.markers)
361 }
362
363 let mut next = RequirementsTxtGraph::with_capacity(graph.node_count(), graph.edge_count());
364 let mut inverse = FxHashMap::with_capacity_and_hasher(graph.node_count(), FxBuildHasher);
365
366 for index in graph.node_indices() {
368 let DisplayResolutionGraphNode::Dist(dist) = &graph[index] else {
369 continue;
370 };
371
372 match inverse.entry(version_marker(dist)) {
375 std::collections::hash_map::Entry::Occupied(entry) => {
376 let index = *entry.get();
377 let node: &mut RequirementsTxtDist = &mut next[index];
378 node.extras.extend(dist.extras.iter().cloned());
379 node.extras.sort_unstable();
380 node.extras.dedup();
381 }
382 std::collections::hash_map::Entry::Vacant(entry) => {
383 let index = next.add_node(dist.clone());
384 entry.insert(index);
385 }
386 }
387 }
388
389 for edge in graph.edge_indices() {
391 let (source, target) = graph.edge_endpoints(edge).unwrap();
392 let DisplayResolutionGraphNode::Dist(source_node) = &graph[source] else {
393 continue;
394 };
395 let DisplayResolutionGraphNode::Dist(target_node) = &graph[target] else {
396 continue;
397 };
398 let source = inverse[&version_marker(source_node)];
399 let target = inverse[&version_marker(target_node)];
400
401 next.update_edge(source, target, ());
402 }
403
404 next
405}
406
407fn strip_extras<'dist>(graph: &IntermediatePetGraph<'dist>) -> RequirementsTxtGraph<'dist> {
415 let mut next = RequirementsTxtGraph::with_capacity(graph.node_count(), graph.edge_count());
416 let mut inverse = FxHashMap::with_capacity_and_hasher(graph.node_count(), FxBuildHasher);
417
418 for index in graph.node_indices() {
420 let DisplayResolutionGraphNode::Dist(dist) = &graph[index] else {
421 continue;
422 };
423
424 match inverse.entry(dist.version_id()) {
427 std::collections::hash_map::Entry::Occupied(entry) => {
428 let index = *entry.get();
429 let node: &mut RequirementsTxtDist = &mut next[index];
430 node.extras.clear();
431 node.markers.or(dist.markers);
438 }
439 std::collections::hash_map::Entry::Vacant(entry) => {
440 let index = next.add_node(dist.clone());
441 entry.insert(index);
442 }
443 }
444 }
445
446 for edge in graph.edge_indices() {
448 let (source, target) = graph.edge_endpoints(edge).unwrap();
449 let DisplayResolutionGraphNode::Dist(source_node) = &graph[source] else {
450 continue;
451 };
452 let DisplayResolutionGraphNode::Dist(target_node) = &graph[target] else {
453 continue;
454 };
455 let source = inverse[&source_node.version_id()];
456 let target = inverse[&target_node.version_id()];
457
458 next.update_edge(source, target, ());
459 }
460
461 next
462}