Skip to content

Commit b348720

Browse files
committed
fix: start workspace search from project_path itself (PR #352)
Address @zsol's review: find_workspace_for_project now iterates from project_path (not parent) so a project that also defines [tool.uv.workspace] is discovered. The workspace root is always considered a member of its own workspace (early return on empty relative path). Two new tests cover the self-workspace case.
1 parent 1c5cbf0 commit b348720

File tree

1 file changed

+52
-3
lines changed

1 file changed

+52
-3
lines changed

crates/pet-uv/src/lib.rs

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,11 +154,12 @@ impl Locator for Uv {
154154
}
155155
}
156156

157-
/// Walks up from `project_path` looking for a parent workspace that this project belongs to.
157+
/// Walks up from `project_path` looking for a workspace that this project belongs to.
158+
/// Starts from `project_path` itself because a project can also define `[tool.uv.workspace]`
159+
/// alongside `[project]` (i.e. the workspace root is itself a package).
158160
/// Returns the workspace environment if found and the project is a valid member.
159161
fn find_workspace_for_project(project_path: &Path) -> Option<PythonEnvironment> {
160-
let parent = project_path.parent()?;
161-
for candidate in parent.ancestors() {
162+
for candidate in project_path.ancestors() {
162163
let pyproject = parse_pyproject_toml_in(candidate);
163164
let workspace = pyproject
164165
.as_ref()
@@ -316,6 +317,11 @@ fn is_workspace_member(
316317
Err(_) => return false,
317318
};
318319

320+
// The workspace root itself is always a member of its own workspace
321+
if relative.as_os_str().is_empty() {
322+
return true;
323+
}
324+
319325
// Normalise to forward slashes for glob matching
320326
let relative_str = relative.to_string_lossy().replace('\\', "/");
321327

@@ -723,6 +729,16 @@ name = "my-project""#;
723729
assert!(!is_workspace_member(root, project, &ws));
724730
}
725731

732+
#[test]
733+
fn test_is_workspace_member_workspace_root_is_always_member() {
734+
let root = Path::new("/workspace");
735+
let ws = UvWorkspace {
736+
members: vec!["packages/*".to_string()],
737+
exclude: vec![],
738+
};
739+
assert!(is_workspace_member(root, root, &ws));
740+
}
741+
726742
#[test]
727743
fn test_is_workspace_member_exclude_takes_precedence() {
728744
let root = Path::new("/workspace");
@@ -767,6 +783,39 @@ prompt = my-workspace"#;
767783
assert_eq!(env.name, Some("my-workspace".to_string()));
768784
}
769785

786+
#[test]
787+
fn test_find_workspace_for_project_self_workspace() {
788+
// Edge case: project_path itself defines both [project] and [tool.uv.workspace]
789+
let temp_dir = TempDir::new().unwrap();
790+
let workspace_root = temp_dir.path();
791+
792+
// Create pyproject.toml with both [project] and [tool.uv.workspace]
793+
let pyproject_contents = r#"[project]
794+
name = "mono-repo"
795+
796+
[tool.uv.workspace]
797+
members = ["packages/*"]"#;
798+
std::fs::write(workspace_root.join("pyproject.toml"), pyproject_contents).unwrap();
799+
800+
// Create workspace .venv with uv pyvenv.cfg
801+
let venv_path = workspace_root.join(".venv");
802+
std::fs::create_dir_all(&venv_path).unwrap();
803+
let pyvenv_contents = r#"uv = 0.5.0
804+
version_info = 3.12.0
805+
prompt = mono-repo"#;
806+
std::fs::write(venv_path.join("pyvenv.cfg"), pyvenv_contents).unwrap();
807+
808+
// find_workspace_for_project should find the workspace at project_path itself
809+
let env = find_workspace_for_project(workspace_root);
810+
assert!(
811+
env.is_some(),
812+
"Should discover workspace at project_path itself"
813+
);
814+
let env = env.unwrap();
815+
assert_eq!(env.kind, Some(PythonEnvironmentKind::UvWorkspace));
816+
assert_eq!(env.name, Some("mono-repo".to_string()));
817+
}
818+
770819
#[test]
771820
fn test_find_workspace_for_project_excluded_member() {
772821
let temp_dir = TempDir::new().unwrap();

0 commit comments

Comments
 (0)