diff --git a/crates/uv-workspace/src/workspace.rs b/crates/uv-workspace/src/workspace.rs index 5dc0c533fbc6c..068edcd61632b 100644 --- a/crates/uv-workspace/src/workspace.rs +++ b/crates/uv-workspace/src/workspace.rs @@ -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: + 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); diff --git a/crates/uv/tests/lock/lock.rs b/crates/uv/tests/lock/lock.rs index 025133a473873..75f3863970e01 100644 --- a/crates/uv/tests/lock/lock.rs +++ b/crates/uv/tests/lock/lock.rs @@ -10228,6 +10228,119 @@ fn lock_no_workspace_source() -> Result<()> { Ok(()) } +/// Regression test for . +/// +/// 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<()> {