diff --git a/Cargo.lock b/Cargo.lock index a9dd8f59a..71363faa0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4159,7 +4159,6 @@ dependencies = [ "futures", "http", "ignore", - "indexmap", "indicatif", "indoc", "insta", @@ -4167,6 +4166,7 @@ dependencies = [ "jiff", "miette", "owo-colors", + "petgraph", "predicates", "rayon", "regex", diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index 8e39d4465..7268fbf16 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -67,13 +67,13 @@ flate2 = { workspace = true, default-features = false } fs-err = { workspace = true, features = ["tokio"] } futures = { workspace = true } http = { workspace = true } -indexmap = { workspace = true } indicatif = { workspace = true } indoc = { workspace = true } itertools = { workspace = true } jiff = { workspace = true } miette = { workspace = true, features = ["fancy-no-backtrace"] } owo-colors = { workspace = true } +petgraph = { workspace = true } rayon = { workspace = true } regex = { workspace = true } reqwest = { workspace = true } diff --git a/crates/uv/src/commands/pip/tree.rs b/crates/uv/src/commands/pip/tree.rs index a36370d39..703ebef50 100644 --- a/crates/uv/src/commands/pip/tree.rs +++ b/crates/uv/src/commands/pip/tree.rs @@ -1,16 +1,19 @@ +use std::collections::VecDeque; use std::fmt::Write; use anyhow::Result; -use indexmap::IndexMap; use owo_colors::OwoColorize; +use petgraph::graph::{EdgeIndex, NodeIndex}; +use petgraph::prelude::EdgeRef; +use petgraph::Direction; use rustc_hash::{FxHashMap, FxHashSet}; use uv_cache::Cache; -use uv_distribution::Metadata; use uv_distribution_types::{Diagnostic, Name}; use uv_installer::SitePackages; use uv_normalize::PackageName; -use uv_pypi_types::{RequirementSource, ResolverMarkerEnvironment}; +use uv_pep508::{Requirement, VersionOrUrl}; +use uv_pypi_types::{ResolutionMetadata, ResolverMarkerEnvironment, VerbatimParsedUrl}; use uv_python::{EnvironmentPreference, PythonEnvironment, PythonRequest}; use crate::commands::pip::operations::report_target_environment; @@ -22,8 +25,8 @@ use crate::printer::Printer; pub(crate) fn pip_tree( show_version_specifiers: bool, depth: u8, - prune: Vec, - package: Vec, + prune: &[PackageName], + package: &[PackageName], no_dedupe: bool, invert: bool, strict: bool, @@ -43,14 +46,17 @@ pub(crate) fn pip_tree( // Read packages from the virtual environment. let site_packages = SitePackages::from_environment(&environment)?; - let mut packages: IndexMap<_, Vec<_>> = IndexMap::new(); - for package in site_packages.iter() { - let metadata = Metadata::from_metadata23(package.metadata()?); + + let packages = { + let mut packages: FxHashMap<_, Vec<_>> = FxHashMap::default(); + for package in site_packages.iter() { + packages + .entry(package.name()) + .or_default() + .push(package.metadata()?); + } packages - .entry(package.name().clone()) - .or_default() - .push(metadata); - } + }; // Determine the markers to use for the resolution. let markers = environment.interpreter().resolver_markers(); @@ -64,7 +70,7 @@ pub(crate) fn pip_tree( invert, show_version_specifiers, &markers, - packages, + &packages, ) .render() .join("\n"); @@ -97,91 +103,172 @@ pub(crate) fn pip_tree( } #[derive(Debug)] -pub(crate) struct DisplayDependencyGraph { - packages: IndexMap>, +pub(crate) struct DisplayDependencyGraph<'env> { + /// The constructed dependency graph. + graph: petgraph::graph::Graph< + &'env ResolutionMetadata, + &'env Requirement, + petgraph::Directed, + >, + /// The packages considered as roots of the dependency tree. + roots: Vec, /// Maximum display depth of the dependency tree depth: usize, - /// Prune the given packages from the display of the dependency tree. - prune: Vec, - /// Display only the specified packages. - package: Vec, /// Whether to de-duplicate the displayed dependencies. no_dedupe: bool, - /// Map from package name to its requirements. - /// - /// If `--invert` is given the map is inverted. - requirements: FxHashMap>, - /// Map from requirement package name-to-parent-to-dependency metadata. - dependencies: FxHashMap>, + /// Whether to invert the dependency tree. + invert: bool, + /// Whether to include the version specifiers in the tree. + show_version_specifiers: bool, } -impl DisplayDependencyGraph { +impl<'env> DisplayDependencyGraph<'env> { /// Create a new [`DisplayDependencyGraph`] for the set of installed distributions. pub(crate) fn new( depth: usize, - prune: Vec, - package: Vec, + prune: &[PackageName], + package: &[PackageName], no_dedupe: bool, invert: bool, show_version_specifiers: bool, markers: &ResolverMarkerEnvironment, - packages: IndexMap>, + packages: &'env FxHashMap<&PackageName, Vec>, ) -> Self { - let mut requirements: FxHashMap<_, Vec<_>> = FxHashMap::default(); - let mut dependencies: FxHashMap> = - FxHashMap::default(); + // Create a graph. + let mut graph = petgraph::graph::Graph::< + &ResolutionMetadata, + &Requirement, + petgraph::Directed, + >::new(); - // Add all transitive requirements. + // Step 1: Add each installed package. + let mut inverse: FxHashMap> = FxHashMap::default(); for metadata in packages.values().flatten() { - // Ignore any optional dependencies. - for required in metadata - .requires_dist - .iter() - .filter(|requirement| requirement.marker.evaluate(markers, &[])) - { - let dependency = if invert { - Dependency::Inverted( - required.name.clone(), - metadata.name.clone(), - required.source.clone(), - ) - } else { - Dependency::Normal( - metadata.name.clone(), - required.name.clone(), - required.source.clone(), - ) - }; + if prune.contains(&metadata.name) { + continue; + } - requirements - .entry(dependency.parent().clone()) - .or_default() - .push(dependency.child().clone()); + let index = graph.add_node(metadata); + inverse + .entry(metadata.name.clone()) + .or_default() + .push(index); + } - if show_version_specifiers { - dependencies - .entry(dependency.parent().clone()) - .or_default() - .insert(dependency.child().clone(), dependency); + // Step 2: Add all dependencies. + for index in graph.node_indices() { + let metadata = &graph[index]; + + for requirement in &metadata.requires_dist { + if prune.contains(&requirement.name) { + continue; + } + if !requirement.marker.evaluate(markers, &[]) { + continue; + } + + for dep_index in inverse + .get(&requirement.name) + .into_iter() + .flatten() + .copied() + { + let dep = &graph[dep_index]; + + // Avoid adding an edge if the dependency is not required by the current package. + if let Some(VersionOrUrl::VersionSpecifier(specifier)) = + requirement.version_or_url.as_ref() + { + if !specifier.contains(&dep.version) { + continue; + } + } + + graph.add_edge(index, dep_index, requirement); } } } + // Step 2: Reverse the graph. + if invert { + graph.reverse(); + } + + // Step 3: Filter the graph to those nodes reachable from the target packages. + if !package.is_empty() { + // Perform a DFS from the root nodes to find the reachable nodes. + let mut reachable = graph + .node_indices() + .filter(|index| package.contains(&graph[*index].name)) + .collect::>(); + let mut stack = reachable.iter().copied().collect::>(); + while let Some(node) = stack.pop_front() { + for edge in graph.edges_directed(node, Direction::Outgoing) { + if reachable.insert(edge.target()) { + stack.push_back(edge.target()); + } + } + } + + // Remove the unreachable nodes from the graph. + graph.retain_nodes(|_, index| reachable.contains(&index)); + } + + // Compute the list of roots. + let roots = { + let mut edges = vec![]; + + // Remove any cycles. + let feedback_set: Vec = petgraph::algo::greedy_feedback_arc_set(&graph) + .map(|e| e.id()) + .collect(); + for edge_id in feedback_set { + if let Some((source, target)) = graph.edge_endpoints(edge_id) { + if let Some(weight) = graph.remove_edge(edge_id) { + edges.push((source, target, weight)); + } + } + } + + // Find the root nodes. + let mut roots = graph + .node_indices() + .filter(|index| { + graph + .edges_directed(*index, Direction::Incoming) + .next() + .is_none() + }) + .collect::>(); + + // Sort the roots. + roots.sort_by_key(|index| { + let metadata = &graph[*index]; + (&metadata.name, &metadata.version) + }); + + // Re-add the removed edges. + for (source, target, weight) in edges { + graph.add_edge(source, target, weight); + } + + roots + }; + Self { - packages, + graph, + roots, depth, - prune, - package, no_dedupe, - requirements, - dependencies, + invert, + show_version_specifiers, } } /// Perform a depth-first traversal of the given distribution and its dependencies. - fn visit<'env>( - &'env self, - metadata: &'env Metadata, + fn visit( + &self, + cursor: Cursor, visited: &mut FxHashMap<&'env PackageName, Vec>, path: &mut Vec<&'env PackageName>, ) -> Vec { @@ -190,18 +277,31 @@ impl DisplayDependencyGraph { return Vec::new(); } + let metadata = &self.graph[cursor.node()]; let package_name = &metadata.name; let mut line = format!("{} v{}", package_name, metadata.version); // If the current package is not top-level (i.e., it has a parent), include the specifiers. - if let Some(last) = path.last().copied() { - if let Some(dependency) = self - .dependencies - .get(last) - .and_then(|deps| deps.get(package_name)) - { + if self.show_version_specifiers { + if let Some(edge) = cursor.edge() { line.push(' '); - line.push_str(&format!("[{dependency}]")); + + let source = &self.graph[edge]; + if self.invert { + let parent = self.graph.edge_endpoints(edge).unwrap().0; + let parent = &self.graph[parent].name; + let version = match source.version_or_url.as_ref() { + None => "*".to_string(), + Some(version) => version.to_string(), + }; + line.push_str(&format!("[requires: {parent} {version}]")); + } else { + let version = match source.version_or_url.as_ref() { + None => "*".to_string(), + Some(version) => version.to_string(), + }; + line.push_str(&format!("[required: {version}]")); + } } } @@ -218,25 +318,35 @@ impl DisplayDependencyGraph { } } - let requirements = self - .requirements - .get(package_name) - .into_iter() - .flatten() - .filter(|&req| { - // Skip if the current package is not one of the installed distributions. - !self.prune.contains(req) && self.packages.contains_key(req) + let mut dependencies = self + .graph + .edges_directed(cursor.node(), Direction::Outgoing) + .map(|edge| { + let node = edge.target(); + Cursor::new(node, edge.id()) }) - .cloned() .collect::>(); + dependencies.sort_by_key(|node| { + let metadata = &self.graph[node.node()]; + (&metadata.name, &metadata.version) + }); let mut lines = vec![line]; // Keep track of the dependency path to avoid cycles. - visited.insert(package_name, requirements.clone()); + visited.insert( + package_name, + dependencies + .iter() + .map(|node| { + let metadata = &self.graph[node.node()]; + metadata.name.clone() + }) + .collect(), + ); path.push(package_name); - for (index, req) in requirements.iter().enumerate() { + for (index, dep) in dependencies.iter().enumerate() { // For sub-visited packages, add the prefix to make the tree display user-friendly. // The key observation here is you can group the tree as follows when you're at the // root of the tree: @@ -256,24 +366,21 @@ impl DisplayDependencyGraph { // those in Group 3 have `└── ` at the top and ` ` at the rest. // This observation is true recursively even when looking at the subtree rooted // at `level_1_0`. - let (prefix_top, prefix_rest) = if requirements.len() - 1 == index { + let (prefix_top, prefix_rest) = if dependencies.len() - 1 == index { ("└── ", " ") } else { ("├── ", "│ ") }; - for distribution in self.packages.get(req).into_iter().flatten() { - for (visited_index, visited_line) in - self.visit(distribution, visited, path).iter().enumerate() - { - let prefix = if visited_index == 0 { - prefix_top - } else { - prefix_rest - }; + for (visited_index, visited_line) in self.visit(*dep, visited, path).iter().enumerate() + { + let prefix = if visited_index == 0 { + prefix_top + } else { + prefix_rest + }; - lines.push(format!("{prefix}{visited_line}")); - } + lines.push(format!("{prefix}{visited_line}")); } } path.pop(); @@ -283,81 +390,42 @@ impl DisplayDependencyGraph { /// Depth-first traverse the nodes to render the tree. pub(crate) fn render(&self) -> Vec { - let mut visited: FxHashMap<&PackageName, Vec> = FxHashMap::default(); - let mut path: Vec<&PackageName> = Vec::new(); - let mut lines: Vec = Vec::new(); + let mut path = Vec::new(); + let mut lines = Vec::with_capacity(self.graph.node_count()); + let mut visited = + FxHashMap::with_capacity_and_hasher(self.graph.node_count(), rustc_hash::FxBuildHasher); - if self.package.is_empty() { - // The root nodes are those that are not required by any other package. - let children: FxHashSet<_> = self.requirements.values().flatten().collect(); - for package in self.packages.values().flatten() { - // If the current package is not required by any other package, start the traversal - // with the current package as the root. - if !children.contains(&package.name) { - path.clear(); - lines.extend(self.visit(package, &mut visited, &mut path)); - } - } - } else { - for (index, package) in self.package.iter().enumerate() { - if index != 0 { - lines.push(String::new()); - } - - for package in self.packages.get(package).into_iter().flatten() { - path.clear(); - lines.extend(self.visit(package, &mut visited, &mut path)); - } - } + for node in &self.roots { + path.clear(); + lines.extend(self.visit(Cursor::root(*node), &mut visited, &mut path)); } lines } } -#[derive(Debug)] -enum Dependency { - /// Show dependencies from parent to the child package that it requires. - Normal(PackageName, PackageName, RequirementSource), - /// Show dependencies from the child package to the parent that requires it. - Inverted(PackageName, PackageName, RequirementSource), -} +/// A node in the dependency graph along with the edge that led to it, or `None` for root nodes. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)] +struct Cursor(NodeIndex, Option); -impl Dependency { - /// Return the parent in the tree. - fn parent(&self) -> &PackageName { - match self { - Self::Normal(parent, _, _) => parent, - Self::Inverted(parent, _, _) => parent, - } +impl Cursor { + /// Create a [`Cursor`] representing a node in the dependency tree. + fn new(node: NodeIndex, edge: EdgeIndex) -> Self { + Self(node, Some(edge)) } - /// Return the child in the tree. - fn child(&self) -> &PackageName { - match self { - Self::Normal(_, child, _) => child, - Self::Inverted(_, child, _) => child, - } + /// Create a [`Cursor`] representing a root node in the dependency tree. + fn root(node: NodeIndex) -> Self { + Self(node, None) } -} -impl std::fmt::Display for Dependency { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Normal(_, _, source) => { - let version = match source.version_or_url() { - None => "*".to_string(), - Some(version) => version.to_string(), - }; - write!(f, "required: {version}") - } - Self::Inverted(parent, _, source) => { - let version = match source.version_or_url() { - None => "*".to_string(), - Some(version) => version.to_string(), - }; - write!(f, "requires: {parent} {version}") - } - } + /// Return the [`NodeIndex`] of the node. + fn node(&self) -> NodeIndex { + self.0 + } + + /// Return the [`EdgeIndex`] of the edge that led to the node, if any. + fn edge(&self) -> Option { + self.1 } } diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index e29db5545..f5b362b65 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -643,8 +643,8 @@ async fn run(mut cli: Cli) -> Result { commands::pip_tree( args.show_version_specifiers, args.depth, - args.prune, - args.package, + &args.prune, + &args.package, args.no_dedupe, args.invert, args.shared.strict, diff --git a/crates/uv/tests/it/pip_tree.rs b/crates/uv/tests/it/pip_tree.rs index 3ca3d2b4d..15f84b47b 100644 --- a/crates/uv/tests/it/pip_tree.rs +++ b/crates/uv/tests/it/pip_tree.rs @@ -103,10 +103,10 @@ fn single_package() { exit_code: 0 ----- stdout ----- requests v2.31.0 + ├── certifi v2024.2.2 ├── charset-normalizer v3.3.2 ├── idna v3.6 - ├── urllib3 v2.2.1 - └── certifi v2024.2.2 + └── urllib3 v2.2.1 ----- stderr ----- "### @@ -148,13 +148,13 @@ fn nested_dependencies() { exit_code: 0 ----- stdout ----- flask v3.0.2 - ├── werkzeug v3.0.1 - │ └── markupsafe v2.1.5 + ├── blinker v1.7.0 + ├── click v8.1.7 + ├── itsdangerous v2.1.2 ├── jinja2 v3.1.3 │ └── markupsafe v2.1.5 - ├── itsdangerous v2.1.2 - ├── click v8.1.7 - └── blinker v1.7.0 + └── werkzeug v3.0.1 + └── markupsafe v2.1.5 ----- stderr ----- "### @@ -196,17 +196,17 @@ fn reverse() { success: true exit_code: 0 ----- stdout ----- - markupsafe v2.1.5 - ├── jinja2 v3.1.3 - │ └── flask v3.0.2 - └── werkzeug v3.0.1 - └── flask v3.0.2 blinker v1.7.0 └── flask v3.0.2 click v8.1.7 └── flask v3.0.2 itsdangerous v2.1.2 └── flask v3.0.2 + markupsafe v2.1.5 + ├── jinja2 v3.1.3 + │ └── flask v3.0.2 + └── werkzeug v3.0.1 + └── flask v3.0.2 ----- stderr ----- "### @@ -247,17 +247,17 @@ fn invert() { success: true exit_code: 0 ----- stdout ----- - markupsafe v2.1.5 - ├── jinja2 v3.1.3 - │ └── flask v3.0.2 - └── werkzeug v3.0.1 - └── flask v3.0.2 blinker v1.7.0 └── flask v3.0.2 click v8.1.7 └── flask v3.0.2 itsdangerous v2.1.2 └── flask v3.0.2 + markupsafe v2.1.5 + ├── jinja2 v3.1.3 + │ └── flask v3.0.2 + └── werkzeug v3.0.1 + └── flask v3.0.2 ----- stderr ----- "### @@ -326,11 +326,11 @@ fn depth() { exit_code: 0 ----- stdout ----- flask v3.0.2 - ├── werkzeug v3.0.1 - ├── jinja2 v3.1.3 - ├── itsdangerous v2.1.2 + ├── blinker v1.7.0 ├── click v8.1.7 - └── blinker v1.7.0 + ├── itsdangerous v2.1.2 + ├── jinja2 v3.1.3 + └── werkzeug v3.0.1 ----- stderr ----- "### @@ -350,13 +350,13 @@ fn depth() { exit_code: 0 ----- stdout ----- flask v3.0.2 - ├── werkzeug v3.0.1 - │ └── markupsafe v2.1.5 + ├── blinker v1.7.0 + ├── click v8.1.7 + ├── itsdangerous v2.1.2 ├── jinja2 v3.1.3 │ └── markupsafe v2.1.5 - ├── itsdangerous v2.1.2 - ├── click v8.1.7 - └── blinker v1.7.0 + └── werkzeug v3.0.1 + └── markupsafe v2.1.5 ----- stderr ----- "### @@ -406,11 +406,11 @@ fn prune() { exit_code: 0 ----- stdout ----- flask v3.0.2 - ├── jinja2 v3.1.3 - │ └── markupsafe v2.1.5 - ├── itsdangerous v2.1.2 + ├── blinker v1.7.0 ├── click v8.1.7 - └── blinker v1.7.0 + ├── itsdangerous v2.1.2 + └── jinja2 v3.1.3 + └── markupsafe v2.1.5 ----- stderr ----- "### @@ -432,9 +432,10 @@ fn prune() { exit_code: 0 ----- stdout ----- flask v3.0.2 - ├── itsdangerous v2.1.2 + ├── blinker v1.7.0 ├── click v8.1.7 - └── blinker v1.7.0 + └── itsdangerous v2.1.2 + markupsafe v2.1.5 ----- stderr ----- "### @@ -454,65 +455,11 @@ fn prune() { exit_code: 0 ----- stdout ----- flask v3.0.2 - ├── jinja2 v3.1.3 - │ └── markupsafe v2.1.5 - ├── itsdangerous v2.1.2 + ├── blinker v1.7.0 ├── click v8.1.7 - └── blinker v1.7.0 - - ----- stderr ----- - "### - ); -} - -/// Ensure `pip tree` behaves correctly with a package that has a cyclic dependency. -/// -/// Package `uv-cyclic-dependencies-a` and `uv-cyclic-dependencies-b` depend on each other, -/// which creates a dependency cycle. -/// -/// Additionally, package `uv-cyclic-dependencies-c` is included (depends on `uv-cyclic-dependencies-a`) -/// to make this test case more realistic and meaningful. -#[test] -fn cyclic_dependency() { - let context = TestContext::new("3.12"); - - let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt - .write_str("uv-cyclic-dependencies-c") - .unwrap(); - - let mut command = context.pip_install(); - command.env_remove(EnvVars::UV_EXCLUDE_NEWER); - command - .arg("-r") - .arg("requirements.txt") - .arg("--index-url") - .arg("https://test.pypi.org/simple/"); - - uv_snapshot!(context.filters(), command, @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 3 packages in [TIME] - Prepared 3 packages in [TIME] - Installed 3 packages in [TIME] - + uv-cyclic-dependencies-a==0.1.0 - + uv-cyclic-dependencies-b==0.1.0 - + uv-cyclic-dependencies-c==0.1.0 - "### - ); - - uv_snapshot!(context.filters(), context.pip_tree(), @r###" - success: true - exit_code: 0 - ----- stdout ----- - uv-cyclic-dependencies-c v0.1.0 - └── uv-cyclic-dependencies-a v0.1.0 - └── uv-cyclic-dependencies-b v0.1.0 - └── uv-cyclic-dependencies-a v0.1.0 (*) - (*) Package tree already displayed + ├── itsdangerous v2.1.2 + └── jinja2 v3.1.3 + └── markupsafe v2.1.5 ----- stderr ----- "### @@ -618,16 +565,17 @@ fn multiple_packages() { ----- stdout ----- click v8.1.7 requests v2.31.0 + ├── certifi v2024.2.2 ├── charset-normalizer v3.3.2 ├── idna v3.6 - ├── urllib3 v2.2.1 - └── certifi v2024.2.2 + └── urllib3 v2.2.1 ----- stderr ----- "### ); } +/// Show the installed tree in the presence of a cycle. #[test] fn cycle() { let context = TestContext::new("3.12"); @@ -672,7 +620,22 @@ fn cycle() { success: true exit_code: 0 ----- stdout ----- - + testtools v2.3.0 + ├── extras v1.0.0 + ├── fixtures v3.0.0 + │ ├── pbr v6.0.0 + │ ├── six v1.16.0 + │ └── testtools v2.3.0 (*) + ├── pbr v6.0.0 + ├── python-mimeparse v1.6.0 + ├── six v1.16.0 + ├── traceback2 v1.4.0 + │ └── linecache2 v1.0.0 + └── unittest2 v1.1.0 + ├── argparse v1.4.0 + ├── six v1.16.0 + └── traceback2 v1.4.0 (*) + (*) Package tree already displayed ----- stderr ----- "### @@ -722,9 +685,9 @@ fn multiple_packages_shared_descendant() { pendulum v3.0.0 ├── python-dateutil v2.9.0.post0 │ └── six v1.16.0 - ├── tzdata v2024.1 - └── time-machine v2.14.1 - └── python-dateutil v2.9.0.post0 (*) + ├── time-machine v2.14.1 + │ └── python-dateutil v2.9.0.post0 (*) + └── tzdata v2024.1 (*) Package tree already displayed ----- stderr ----- @@ -785,87 +748,6 @@ fn no_dedupe_and_invert() { ); } -/// Ensure that --no-dedupe behaves as expected in the presence of dependency cycles. -#[test] -fn no_dedupe_and_cycle() { - let context = TestContext::new("3.12"); - - let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt - .write_str( - r" - pendulum - time-machine - ", - ) - .unwrap(); - - uv_snapshot!(context - .pip_install() - .arg("-r") - .arg("requirements.txt") - .arg("--strict"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 5 packages in [TIME] - Prepared 5 packages in [TIME] - Installed 5 packages in [TIME] - + pendulum==3.0.0 - + python-dateutil==2.9.0.post0 - + six==1.16.0 - + time-machine==2.14.1 - + tzdata==2024.1 - "### - ); - - let mut command = context.pip_install(); - command.env_remove(EnvVars::UV_EXCLUDE_NEWER); - command - .arg("uv-cyclic-dependencies-c==0.1.0") - .arg("--index-url") - .arg("https://test.pypi.org/simple/"); - - uv_snapshot!(context.filters(), command, @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 3 packages in [TIME] - Prepared 3 packages in [TIME] - Installed 3 packages in [TIME] - + uv-cyclic-dependencies-a==0.1.0 - + uv-cyclic-dependencies-b==0.1.0 - + uv-cyclic-dependencies-c==0.1.0 - "### - ); - - uv_snapshot!(context.filters(), context.pip_tree() - .arg("--no-dedupe"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - pendulum v3.0.0 - ├── python-dateutil v2.9.0.post0 - │ └── six v1.16.0 - ├── tzdata v2024.1 - └── time-machine v2.14.1 - └── python-dateutil v2.9.0.post0 - └── six v1.16.0 - uv-cyclic-dependencies-c v0.1.0 - └── uv-cyclic-dependencies-a v0.1.0 - └── uv-cyclic-dependencies-b v0.1.0 - └── uv-cyclic-dependencies-a v0.1.0 (*) - (*) Package tree is a cycle and cannot be shown - - ----- stderr ----- - "### - ); -} - #[test] fn no_dedupe() { let context = TestContext::new("3.12"); @@ -909,10 +791,10 @@ fn no_dedupe() { pendulum v3.0.0 ├── python-dateutil v2.9.0.post0 │ └── six v1.16.0 - ├── tzdata v2024.1 - └── time-machine v2.14.1 - └── python-dateutil v2.9.0.post0 - └── six v1.16.0 + ├── time-machine v2.14.1 + │ └── python-dateutil v2.9.0.post0 + │ └── six v1.16.0 + └── tzdata v2024.1 ----- stderr ----- "### @@ -1017,11 +899,10 @@ fn package_flag() { success: true exit_code: 0 ----- stdout ----- - werkzeug v3.0.1 - └── markupsafe v2.1.5 - jinja2 v3.1.3 └── markupsafe v2.1.5 + werkzeug v3.0.1 + └── markupsafe v2.1.5 ----- stderr ----- "### @@ -1061,10 +942,10 @@ fn show_version_specifiers_simple() { exit_code: 0 ----- stdout ----- requests v2.31.0 + ├── certifi v2024.2.2 [required: >=2017.4.17] ├── charset-normalizer v3.3.2 [required: >=2, <4] ├── idna v3.6 [required: >=2.5, <4] - ├── urllib3 v2.2.1 [required: >=1.21.1, <3] - └── certifi v2024.2.2 [required: >=2017.4.17] + └── urllib3 v2.2.1 [required: >=1.21.1, <3] ----- stderr ----- "### @@ -1109,17 +990,17 @@ fn show_version_specifiers_with_invert() { success: true exit_code: 0 ----- stdout ----- - markupsafe v2.1.5 - ├── jinja2 v3.1.3 [requires: markupsafe >=2.0] - │ └── flask v3.0.2 [requires: jinja2 >=3.1.2] - └── werkzeug v3.0.1 [requires: markupsafe >=2.1.1] - └── flask v3.0.2 [requires: werkzeug >=3.0.0] blinker v1.7.0 └── flask v3.0.2 [requires: blinker >=1.6.2] click v8.1.7 └── flask v3.0.2 [requires: click >=8.1.3] itsdangerous v2.1.2 └── flask v3.0.2 [requires: itsdangerous >=2.1.2] + markupsafe v2.1.5 + ├── jinja2 v3.1.3 [requires: markupsafe >=2.0] + │ └── flask v3.0.2 [requires: jinja2 >=3.1.2] + └── werkzeug v3.0.1 [requires: markupsafe >=2.1.1] + └── flask v3.0.2 [requires: werkzeug >=3.0.0] ----- stderr ----- "###