Fix root of uv tree when --package is used with circular dependencies (#15908)

Closes #15907

Best viewed with
https://github.com/astral-sh/uv/pull/15908/files?diff=unified&w=1

When `--package` is used, just use those as the roots rather than
calculating them. I'm not sure if there will be undesirable
side-effects, but it's the naive solution.
This commit is contained in:
Zanie Blue 2025-10-26 21:01:00 -05:00 committed by GitHub
parent 175be60727
commit e2eea6d7db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 144 additions and 30 deletions

View file

@ -362,40 +362,58 @@ impl<'env> TreeDisplay<'env> {
// Compute the list of roots.
let roots = {
let mut edges = vec![];
// If specific packages were requested, use them as roots.
if !packages.is_empty() {
let mut roots = graph
.node_indices()
.filter(|index| {
let Node::Package(package_id) = graph[*index] else {
return false;
};
packages.contains(&package_id.name)
})
.collect::<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));
// Sort the roots.
roots.sort_by_key(|index| &graph[*index]);
roots
} else {
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: nodes with no incoming edges, or only an edge from the proxy.
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| &graph[*index]);
// Re-add the removed edges.
for (source, target, weight) in edges {
graph.add_edge(source, target, weight);
}
roots
}
// Find the root nodes: nodes with no incoming edges, or only an edge from the proxy.
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| &graph[*index]);
// Re-add the removed edges.
for (source, target, weight) in edges {
graph.add_edge(source, target, weight);
}
roots
};
Self {

View file

@ -1703,3 +1703,99 @@ fn show_sizes() -> Result<()> {
Ok(())
}
#[test]
fn workspace_circular_dependencies() -> Result<()> {
let context = TestContext::new("3.12");
// Create workspace root
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[tool.uv.workspace]
members = ["packages/*"]
"#,
)?;
// Create package-a that depends on package-b
let package_a_dir = context.temp_dir.child("packages").child("package-a");
package_a_dir.create_dir_all()?;
let package_a_pyproject = package_a_dir.child("pyproject.toml");
package_a_pyproject.write_str(
r#"
[project]
name = "package-a"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["package-b"]
[tool.uv.sources]
package-b = { workspace = true }
"#,
)?;
// Create package-b that depends on package-a (circular dependency)
let package_b_dir = context.temp_dir.child("packages").child("package-b");
package_b_dir.create_dir_all()?;
let package_b_pyproject = package_b_dir.child("pyproject.toml");
package_b_pyproject.write_str(
r#"
[project]
name = "package-b"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["package-a"]
[tool.uv.sources]
package-a = { workspace = true }
"#,
)?;
// Test that package-a is at the root when requested
uv_snapshot!(context.filters(), context.tree().arg("--package").arg("package-a"), @r###"
success: true
exit_code: 0
----- stdout -----
package-a v0.1.0
package-b v0.1.0
package-a v0.1.0 (*)
(*) Package tree already displayed
----- stderr -----
Resolved 2 packages in [TIME]
"###
);
// Test that package-b is at the root when requested
uv_snapshot!(context.filters(), context.tree().arg("--package").arg("package-b"), @r###"
success: true
exit_code: 0
----- stdout -----
package-b v0.1.0
package-a v0.1.0
package-b v0.1.0 (*)
(*) Package tree already displayed
----- stderr -----
Resolved 2 packages in [TIME]
"###
);
// Test that both packages are shown as roots when both are requested
uv_snapshot!(context.filters(), context.tree().arg("--package").arg("package-a").arg("--package").arg("package-b"), @r###"
success: true
exit_code: 0
----- stdout -----
package-a v0.1.0
package-b v0.1.0
package-a v0.1.0 (*)
package-b v0.1.0 (*)
(*) Package tree already displayed
----- stderr -----
Resolved 2 packages in [TIME]
"###
);
Ok(())
}