Treat workspace members with an intermediate pyproject.toml as standalone (#19926)

## Summary

Based on #19924.

Handle the case where a `pyproject.toml` sits between a member and its
workspace root.

This unintentionally supported case regressed when adding workspace
caching, which memorises which projects are members of a workspace
without requiring re-discovery, ignoring a `pyproject.toml` in the
middle. Before caching, workspace discovery for this project starting
from the project itself would terminate at the intermediate
`pyroject.toml` and treat it as standalone project, which this hack
restores.

Fixes #19916

## Test Plan

Added a regression test, unlike for Konsti's version of the PR, we have
existing coverage for the case where we mess this up so we don't need a
second test to cover it.

Co-authored-by: konstin <konstin@mailbox.org>
This commit is contained in:
Tomasz Kramkowski
2026-06-19 18:21:46 +01:00
committed by GitHub
parent a058272654
commit 3723315ee6
2 changed files with 138 additions and 0 deletions
+25
View File
@@ -60,6 +60,13 @@ impl WorkspaceCache {
match result {
Ok(workspace) => {
for package in workspace.packages.values() {
// Historically, upward workspace discovery stopped at an intermediate
// `pyproject.toml`, so don't map this member to the outer workspace in that
// case.
// See: <https://github.com/astral-sh/uv/issues/19916>
if has_intermediate_pyproject(&workspace.install_path, &package.root) {
continue;
}
self.workspaces
.done(package.root.clone(), Ok(workspace.clone()));
}
@@ -99,6 +106,24 @@ impl WorkspaceCache {
}
}
/// Returns `true` when a `pyproject.toml` sits between the member project directory and the
/// workspace root.
fn has_intermediate_pyproject(workspace_root: &Path, project_dir: &Path) -> bool {
if project_dir == workspace_root {
return false;
}
let Ok(_) = project_dir.strip_prefix(workspace_root) else {
return false;
};
project_dir
.ancestors()
.skip(1)
.take_while(|ancestor| *ancestor != workspace_root)
.any(|ancestor| ancestor.join("pyproject.toml").is_file())
}
#[derive(Debug, Clone)]
pub struct WorkspaceError(Arc<WorkspaceErrorKind>);
+113
View File
@@ -10228,6 +10228,119 @@ fn lock_no_workspace_source() -> Result<()> {
Ok(())
}
/// Regression test for <https://github.com/astral-sh/uv/issues/19916>.
///
/// Lock a workspace with a member that also supports standalone installation via platform-specific
/// path sources.
#[test]
fn lock_workspace_member_with_standalone_path_source() -> Result<()> {
let context = uv_test::test_context!("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[tool.uv.workspace]
members = ["root/app", "root/lib"]
[tool.uv.sources]
lib = { workspace = true }
"#,
)?;
let root = context.temp_dir.child("root");
root.child("pyproject.toml").write_str(
r#"
[project]
name = "root"
version = "0.1.0"
[tool.uv.sources]
lib = { path = "lib" }
"#,
)?;
root.child("app").child("pyproject.toml").write_str(
r#"
[project]
name = "app"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["lib"]
[tool.uv.sources]
lib = [
{ path = "../lib", editable = true, marker = "sys_platform == 'darwin'" },
{ path = "../lib", editable = true, marker = "sys_platform != 'darwin'" },
]
"#,
)?;
root.child("lib").child("pyproject.toml").write_str(
r#"
[project]
name = "lib"
version = "0.1.0"
"#,
)?;
uv_snapshot!(context.filters(), context.lock(), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
");
let lock = context.read("uv.lock");
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
lock, @r#"
version = 1
revision = 3
requires-python = ">=3.12"
resolution-markers = [
"sys_platform == 'darwin'",
"sys_platform != 'darwin'",
]
[options]
exclude-newer = "2024-03-25T00:00:00Z"
[manifest]
members = [
"app",
"lib",
]
[[package]]
name = "app"
version = "0.1.0"
source = { virtual = "root/app" }
dependencies = [
{ name = "lib" },
]
[package.metadata]
requires-dist = [
{ name = "lib", marker = "sys_platform != 'darwin'", editable = "root/lib" },
{ name = "lib", marker = "sys_platform == 'darwin'", editable = "root/lib" },
]
[[package]]
name = "lib"
version = "0.1.0"
source = { editable = "root/lib" }
"#
);
});
Ok(())
}
/// Lock a workspace with a member that's a peer to the root.
#[test]
fn lock_peer_member() -> Result<()> {