@@ -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.
159161fn 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