1use std::collections::{BTreeSet, VecDeque};
2use std::fmt::Write;
3
4use either::Either;
5use itertools::Itertools;
6use owo_colors::OwoColorize;
7use petgraph::graph::{EdgeIndex, NodeIndex};
8use petgraph::prelude::EdgeRef;
9use petgraph::{Direction, Graph};
10use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
11
12use uv_configuration::DependencyGroupsWithDefaults;
13use uv_console::human_readable_bytes;
14use uv_normalize::{ExtraName, GroupName, PackageName};
15use uv_pep440::Version;
16use uv_pep508::MarkerTree;
17use uv_pypi_types::ResolverMarkerEnvironment;
18
19use crate::lock::{Package, PackageId};
20use crate::{Lock, PackageMap};
21
22#[derive(Debug)]
23pub struct TreeDisplay<'env> {
24 graph: petgraph::graph::Graph<Node<'env>, Edge<'env>, petgraph::Directed>,
26 roots: Vec<NodeIndex>,
28 latest: &'env PackageMap<Version>,
30 depth: usize,
32 no_dedupe: bool,
34 invert: bool,
36 lock: &'env Lock,
38 show_sizes: bool,
40}
41
42impl<'env> TreeDisplay<'env> {
43 pub fn new(
45 lock: &'env Lock,
46 markers: Option<&'env ResolverMarkerEnvironment>,
47 latest: &'env PackageMap<Version>,
48 depth: usize,
49 prune: &[PackageName],
50 packages: &[PackageName],
51 groups: &DependencyGroupsWithDefaults,
52 no_dedupe: bool,
53 invert: bool,
54 show_sizes: bool,
55 ) -> Self {
56 let members: BTreeSet<&PackageId> = if lock.members().is_empty() {
64 lock.root().into_iter().map(|package| &package.id).collect()
65 } else {
66 lock.packages
67 .iter()
68 .filter_map(|package| {
69 if lock.members().contains(&package.id.name) {
70 Some(&package.id)
71 } else {
72 None
73 }
74 })
75 .collect()
76 };
77
78 let size_guess = lock.packages.len();
80 let mut graph =
81 Graph::<Node, Edge, petgraph::Directed>::with_capacity(size_guess, size_guess);
82 let mut inverse = FxHashMap::with_capacity_and_hasher(size_guess, FxBuildHasher);
83 let mut queue: VecDeque<(&PackageId, Option<&ExtraName>)> = VecDeque::new();
84 let mut seen = FxHashSet::default();
85
86 let root = graph.add_node(Node::Root);
87
88 for id in members.iter().copied() {
90 if prune.contains(&id.name) {
91 continue;
92 }
93
94 let dist = lock.find_by_id(id);
95
96 let index = *inverse
100 .entry(id)
101 .or_insert_with(|| graph.add_node(Node::Package(id)));
102
103 graph.add_edge(root, index, Edge::Prod(None));
105
106 if groups.prod() {
107 if seen.insert((id, None)) {
109 queue.push_back((id, None));
110 }
111
112 for extra in dist.optional_dependencies.keys() {
114 if seen.insert((id, Some(extra))) {
115 queue.push_back((id, Some(extra)));
116 }
117 }
118 }
119
120 for (group, dep) in dist
122 .dependency_groups
123 .iter()
124 .filter_map(|(group, deps)| {
125 if groups.contains(group) {
126 Some(deps.iter().map(move |dep| (group, dep)))
127 } else {
128 None
129 }
130 })
131 .flatten()
132 {
133 if prune.contains(&dep.package_id.name) {
134 continue;
135 }
136
137 if markers
138 .is_some_and(|markers| !dep.complexified_marker.evaluate_no_extras(markers))
139 {
140 continue;
141 }
142
143 let dep_index = *inverse
145 .entry(&dep.package_id)
146 .or_insert_with(|| graph.add_node(Node::Package(&dep.package_id)));
147
148 graph.add_edge(index, dep_index, Edge::Dev(group, Some(&dep.extra)));
150
151 if seen.insert((&dep.package_id, None)) {
153 queue.push_back((&dep.package_id, None));
154 }
155 for extra in &dep.extra {
156 if seen.insert((&dep.package_id, Some(extra))) {
157 queue.push_back((&dep.package_id, Some(extra)));
158 }
159 }
160 }
161 }
162
163 {
172 let by_name: FxHashMap<_, Vec<_>> = {
174 lock.packages().iter().fold(
175 FxHashMap::with_capacity_and_hasher(lock.len(), FxBuildHasher),
176 |mut map, package| {
177 map.entry(&package.id.name).or_default().push(package);
178 map
179 },
180 )
181 };
182
183 for requirement in lock.requirements() {
185 for package in by_name.get(&requirement.name).into_iter().flatten() {
186 let marker = if package.fork_markers.is_empty() {
189 requirement.marker
190 } else {
191 let mut combined = MarkerTree::FALSE;
192 for fork_marker in &package.fork_markers {
193 combined.or(fork_marker.pep508());
194 }
195 combined.and(requirement.marker);
196 combined
197 };
198 if marker.is_false() {
199 continue;
200 }
201 if markers.is_some_and(|markers| !marker.evaluate(markers, &[])) {
202 continue;
203 }
204 let index = inverse
206 .entry(&package.id)
207 .or_insert_with(|| graph.add_node(Node::Package(&package.id)));
208
209 graph.add_edge(root, *index, Edge::Prod(None));
211
212 if seen.insert((&package.id, None)) {
214 queue.push_back((&package.id, None));
215 }
216 }
217 }
218
219 for (group, requirements) in lock.dependency_groups() {
221 for requirement in requirements {
222 for package in by_name.get(&requirement.name).into_iter().flatten() {
223 let marker = if package.fork_markers.is_empty() {
226 requirement.marker
227 } else {
228 let mut combined = MarkerTree::FALSE;
229 for fork_marker in &package.fork_markers {
230 combined.or(fork_marker.pep508());
231 }
232 combined.and(requirement.marker);
233 combined
234 };
235 if marker.is_false() {
236 continue;
237 }
238 if markers.is_some_and(|markers| !marker.evaluate(markers, &[])) {
239 continue;
240 }
241 let index = inverse
243 .entry(&package.id)
244 .or_insert_with(|| graph.add_node(Node::Package(&package.id)));
245
246 graph.add_edge(root, *index, Edge::Dev(group, None));
248
249 if seen.insert((&package.id, None)) {
251 queue.push_back((&package.id, None));
252 }
253 }
254 }
255 }
256 }
257
258 while let Some((id, extra)) = queue.pop_front() {
260 let index = inverse[&id];
261 let package = lock.find_by_id(id);
262
263 let deps = if let Some(extra) = extra {
264 Either::Left(
265 package
266 .optional_dependencies
267 .get(extra)
268 .into_iter()
269 .flatten(),
270 )
271 } else {
272 Either::Right(package.dependencies.iter())
273 };
274
275 for dep in deps {
276 if prune.contains(&dep.package_id.name) {
277 continue;
278 }
279
280 if markers
281 .is_some_and(|markers| !dep.complexified_marker.evaluate_no_extras(markers))
282 {
283 continue;
284 }
285
286 let dep_index = *inverse
288 .entry(&dep.package_id)
289 .or_insert_with(|| graph.add_node(Node::Package(&dep.package_id)));
290
291 graph.add_edge(
293 index,
294 dep_index,
295 if let Some(extra) = extra {
296 Edge::Optional(extra, Some(&dep.extra))
297 } else {
298 Edge::Prod(Some(&dep.extra))
299 },
300 );
301
302 if seen.insert((&dep.package_id, None)) {
304 queue.push_back((&dep.package_id, None));
305 }
306 for extra in &dep.extra {
307 if seen.insert((&dep.package_id, Some(extra))) {
308 queue.push_back((&dep.package_id, Some(extra)));
309 }
310 }
311 }
312 }
313
314 {
316 let mut reachable = graph
317 .node_indices()
318 .filter(|index| match graph[*index] {
319 Node::Package(package_id) => members.contains(package_id),
320 Node::Root => true,
321 })
322 .collect::<FxHashSet<_>>();
323 let mut stack = reachable.iter().copied().collect::<VecDeque<_>>();
324 while let Some(node) = stack.pop_front() {
325 for edge in graph.edges_directed(node, Direction::Outgoing) {
326 if reachable.insert(edge.target()) {
327 stack.push_back(edge.target());
328 }
329 }
330 }
331
332 graph.retain_nodes(|_, index| reachable.contains(&index));
334 }
335
336 if invert {
338 graph.reverse();
339 }
340
341 if !packages.is_empty() {
343 let mut reachable = graph
344 .node_indices()
345 .filter(|index| {
346 let Node::Package(package_id) = graph[*index] else {
347 return false;
348 };
349 packages.contains(&package_id.name)
350 })
351 .collect::<FxHashSet<_>>();
352 let mut stack = reachable.iter().copied().collect::<VecDeque<_>>();
353 while let Some(node) = stack.pop_front() {
354 for edge in graph.edges_directed(node, Direction::Outgoing) {
355 if reachable.insert(edge.target()) {
356 stack.push_back(edge.target());
357 }
358 }
359 }
360
361 graph.retain_nodes(|_, index| reachable.contains(&index));
363 }
364
365 let roots = {
367 if !packages.is_empty() {
369 let mut roots = graph
370 .node_indices()
371 .filter(|index| {
372 let Node::Package(package_id) = graph[*index] else {
373 return false;
374 };
375 packages.contains(&package_id.name)
376 })
377 .collect::<Vec<_>>();
378
379 roots.sort_by_key(|index| &graph[*index]);
381
382 roots
383 } else {
384 let mut roots = if invert {
385 graph
388 .node_indices()
389 .filter(|index| {
390 graph
391 .edges_directed(*index, Direction::Incoming)
392 .next()
393 .is_none()
394 })
395 .collect::<Vec<_>>()
396 } else {
397 graph
399 .node_indices()
400 .filter(|index| matches!(graph[*index], Node::Root))
401 .collect::<Vec<_>>()
402 };
403
404 roots.sort_by_key(|index| &graph[*index]);
405 roots
406 }
407 };
408
409 Self {
410 graph,
411 roots,
412 latest,
413 depth,
414 no_dedupe,
415 invert,
416 lock,
417 show_sizes,
418 }
419 }
420
421 fn visit(
423 &'env self,
424 cursor: Cursor,
425 visited: &mut FxHashMap<VisitedNode<'env>, Vec<&'env PackageId>>,
426 path: &mut Vec<VisitedNode<'env>>,
427 ) -> Vec<String> {
428 if path.len() > self.depth {
430 return Vec::new();
431 }
432
433 let Node::Package(package_id) = self.graph[cursor.node()] else {
434 return Vec::new();
435 };
436 let edge = cursor.edge().map(|edge_id| &self.graph[edge_id]);
437 let package = self.lock.find_by_id(package_id);
438
439 let expanded_extras = self.expanded_extras(package, edge);
440 let visited_node = VisitedNode {
441 package_id,
442 expanded_extras: expanded_extras.clone(),
443 };
444
445 let line = {
446 let mut line = format!("{}", package_id.name);
447
448 if let Some(extras) = edge.and_then(Edge::extras) {
449 if !extras.is_empty() {
450 line.push('[');
451 line.push_str(extras.iter().join(", ").as_str());
452 line.push(']');
453 }
454 }
455
456 if let Some(version) = package_id.version.as_ref() {
457 line.push(' ');
458 line.push('v');
459 let _ = write!(line, "{version}");
460 }
461
462 if let Some(edge) = edge {
463 match edge {
464 Edge::Prod(_) => {}
465 Edge::Optional(extra, _) => {
466 let _ = write!(line, " (extra: {extra})");
467 }
468 Edge::Dev(group, _) => {
469 let _ = write!(line, " (group: {group})");
470 }
471 }
472 }
473
474 if self.show_sizes {
477 if let Some(size_bytes) = package.wheels.iter().find_map(|wheel| wheel.size) {
478 let (bytes, unit) = human_readable_bytes(size_bytes);
479 line.push(' ');
480 line.push_str(format!("{}", format!("({bytes:.1}{unit})").dimmed()).as_str());
481 }
482 }
483
484 line
485 };
486
487 if path.contains(&visited_node) {
491 return vec![format!("{line} (*)")];
492 }
493 if !self.no_dedupe
494 && let Some(requirements) = visited.get(&visited_node)
495 {
496 return if requirements.is_empty() {
497 vec![line]
498 } else {
499 vec![format!("{line} (*)")]
500 };
501 }
502
503 let line = if let Some(version) = self.latest.get(package_id) {
505 format!("{line} {}", format!("(latest: v{version})").bold().cyan())
506 } else {
507 line
508 };
509
510 let mut dependencies = self
511 .graph
512 .edges_directed(cursor.node(), Direction::Outgoing)
513 .filter_map(|edge| match self.graph[edge.target()] {
514 Node::Root => None,
515 Node::Package(_) => {
516 if !self.invert
519 && let Edge::Optional(required_extra, _) = &self.graph[edge.id()]
520 {
521 if !expanded_extras.contains(required_extra) {
522 return None;
523 }
524 }
525 Some(Cursor::new(edge.target(), edge.id()))
526 }
527 })
528 .collect::<Vec<_>>();
529 dependencies.sort_by_key(|cursor| {
530 let node = &self.graph[cursor.node()];
531 let edge = cursor
532 .edge()
533 .map(|edge_id| &self.graph[edge_id])
534 .map(Edge::kind);
535 (edge, node)
536 });
537
538 let mut lines = vec![line];
539
540 if path.len() < self.depth {
543 visited.insert(
544 visited_node.clone(),
545 dependencies
546 .iter()
547 .filter_map(|node| match self.graph[node.node()] {
548 Node::Package(package_id) => Some(package_id),
549 Node::Root => None,
550 })
551 .collect(),
552 );
553 }
554 path.push(visited_node);
555
556 for (index, dep) in dependencies.iter().enumerate() {
557 let (prefix_top, prefix_rest) = if dependencies.len() - 1 == index {
577 ("└── ", " ")
578 } else {
579 ("├── ", "│ ")
580 };
581 for (visited_index, visited_line) in self.visit(*dep, visited, path).iter().enumerate()
582 {
583 let prefix = if visited_index == 0 {
584 prefix_top
585 } else {
586 prefix_rest
587 };
588 lines.push(format!("{prefix}{visited_line}"));
589 }
590 }
591
592 path.pop();
593
594 lines
595 }
596
597 fn render(&self) -> Vec<String> {
599 let mut path = Vec::new();
600 let mut lines = Vec::with_capacity(self.graph.node_count());
601 let mut visited =
602 FxHashMap::with_capacity_and_hasher(self.graph.node_count(), FxBuildHasher);
603
604 for node in &self.roots {
605 match self.graph[*node] {
606 Node::Root => {
607 for edge in self.graph.edges_directed(*node, Direction::Outgoing) {
608 let node = edge.target();
609 path.clear();
610 lines.extend(self.visit(
611 Cursor::new(node, edge.id()),
612 &mut visited,
613 &mut path,
614 ));
615 }
616 }
617 Node::Package(_) => {
618 path.clear();
619 lines.extend(self.visit(Cursor::root(*node), &mut visited, &mut path));
620 }
621 }
622 }
623
624 lines
625 }
626
627 fn expanded_extras(
629 &self,
630 package: &'env Package,
631 edge: Option<&Edge<'env>>,
632 ) -> BTreeSet<&'env ExtraName> {
633 if self.invert {
634 return BTreeSet::default();
638 }
639
640 let Some(requested_extras) = edge.and_then(Edge::extras) else {
641 return package.optional_dependencies.keys().collect();
643 };
644
645 requested_extras
646 .iter()
647 .filter(|extra| package.optional_dependencies.contains_key(*extra))
648 .collect()
649 }
650}
651
652#[derive(Debug, Clone, PartialEq, Eq, Hash)]
653struct VisitedNode<'env> {
654 package_id: &'env PackageId,
655 expanded_extras: BTreeSet<&'env ExtraName>,
656}
657
658#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
659enum Node<'env> {
660 Root,
662 Package(&'env PackageId),
664}
665
666#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
667enum Edge<'env> {
668 Prod(Option<&'env BTreeSet<ExtraName>>),
669 Optional(&'env ExtraName, Option<&'env BTreeSet<ExtraName>>),
670 Dev(&'env GroupName, Option<&'env BTreeSet<ExtraName>>),
671}
672
673impl<'env> Edge<'env> {
674 fn extras(&self) -> Option<&'env BTreeSet<ExtraName>> {
675 match self {
676 Self::Prod(extras) => *extras,
677 Self::Optional(_, extras) => *extras,
678 Self::Dev(_, extras) => *extras,
679 }
680 }
681
682 fn kind(&self) -> EdgeKind<'env> {
683 match self {
684 Self::Prod(_) => EdgeKind::Prod,
685 Self::Optional(extra, _) => EdgeKind::Optional(extra),
686 Self::Dev(group, _) => EdgeKind::Dev(group),
687 }
688 }
689}
690
691#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
692enum EdgeKind<'env> {
693 Prod,
694 Optional(&'env ExtraName),
695 Dev(&'env GroupName),
696}
697
698#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)]
700struct Cursor(NodeIndex, Option<EdgeIndex>);
701
702impl Cursor {
703 fn new(node: NodeIndex, edge: EdgeIndex) -> Self {
705 Self(node, Some(edge))
706 }
707
708 fn root(node: NodeIndex) -> Self {
710 Self(node, None)
711 }
712
713 fn node(&self) -> NodeIndex {
715 self.0
716 }
717
718 fn edge(&self) -> Option<EdgeIndex> {
720 self.1
721 }
722}
723
724impl std::fmt::Display for TreeDisplay<'_> {
725 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
726 use owo_colors::OwoColorize;
727
728 let mut deduped = false;
729 for line in self.render() {
730 deduped |= line.contains('*');
731 writeln!(f, "{line}")?;
732 }
733
734 if deduped {
735 let message = if self.no_dedupe {
736 "(*) Package tree is a cycle and cannot be shown".italic()
737 } else {
738 "(*) Package tree already displayed".italic()
739 };
740 writeln!(f, "{message}")?;
741 }
742
743 Ok(())
744 }
745}