Handle cycles in uv pip tree (#8689)
## Summary Porting the improvements I made to `uv tree` over to `uv pip tree`. Closes #8678.
This commit is contained in:
Generated
+1
-1
@@ -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",
|
||||
|
||||
@@ -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 }
|
||||
|
||||
+231
-163
@@ -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<PackageName>,
|
||||
package: Vec<PackageName>,
|
||||
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<PackageName, Vec<Metadata>>,
|
||||
pub(crate) struct DisplayDependencyGraph<'env> {
|
||||
/// The constructed dependency graph.
|
||||
graph: petgraph::graph::Graph<
|
||||
&'env ResolutionMetadata,
|
||||
&'env Requirement<VerbatimParsedUrl>,
|
||||
petgraph::Directed,
|
||||
>,
|
||||
/// The packages considered as roots of the dependency tree.
|
||||
roots: Vec<NodeIndex>,
|
||||
/// Maximum display depth of the dependency tree
|
||||
depth: usize,
|
||||
/// Prune the given packages from the display of the dependency tree.
|
||||
prune: Vec<PackageName>,
|
||||
/// Display only the specified packages.
|
||||
package: Vec<PackageName>,
|
||||
/// 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<PackageName, Vec<PackageName>>,
|
||||
/// Map from requirement package name-to-parent-to-dependency metadata.
|
||||
dependencies: FxHashMap<PackageName, FxHashMap<PackageName, Dependency>>,
|
||||
/// 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<PackageName>,
|
||||
package: Vec<PackageName>,
|
||||
prune: &[PackageName],
|
||||
package: &[PackageName],
|
||||
no_dedupe: bool,
|
||||
invert: bool,
|
||||
show_version_specifiers: bool,
|
||||
markers: &ResolverMarkerEnvironment,
|
||||
packages: IndexMap<PackageName, Vec<Metadata>>,
|
||||
packages: &'env FxHashMap<&PackageName, Vec<ResolutionMetadata>>,
|
||||
) -> Self {
|
||||
let mut requirements: FxHashMap<_, Vec<_>> = FxHashMap::default();
|
||||
let mut dependencies: FxHashMap<PackageName, FxHashMap<PackageName, Dependency>> =
|
||||
FxHashMap::default();
|
||||
// Create a graph.
|
||||
let mut graph = petgraph::graph::Graph::<
|
||||
&ResolutionMetadata,
|
||||
&Requirement<VerbatimParsedUrl>,
|
||||
petgraph::Directed,
|
||||
>::new();
|
||||
|
||||
// Add all transitive requirements.
|
||||
// Step 1: Add each installed package.
|
||||
let mut inverse: FxHashMap<PackageName, Vec<NodeIndex>> = 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::<FxHashSet<_>>();
|
||||
let mut stack = reachable.iter().copied().collect::<VecDeque<_>>();
|
||||
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<EdgeIndex> = 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::<Vec<_>>();
|
||||
|
||||
// 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<PackageName>>,
|
||||
path: &mut Vec<&'env PackageName>,
|
||||
) -> Vec<String> {
|
||||
@@ -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::<Vec<_>>();
|
||||
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<String> {
|
||||
let mut visited: FxHashMap<&PackageName, Vec<PackageName>> = FxHashMap::default();
|
||||
let mut path: Vec<&PackageName> = Vec::new();
|
||||
let mut lines: Vec<String> = 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<EdgeIndex>);
|
||||
|
||||
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<EdgeIndex> {
|
||||
self.1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -643,8 +643,8 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
|
||||
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,
|
||||
|
||||
+72
-191
@@ -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 -----
|
||||
"###
|
||||
|
||||
Reference in New Issue
Block a user