mirror of
				https://github.com/astral-sh/uv.git
				synced 2025-10-31 12:06:13 +00:00 
			
		
		
		
	uv init should not create nested workspace (#5293)
				
					
				
			## Summary Resolves #5251
This commit is contained in:
		
							parent
							
								
									26e042a794
								
							
						
					
					
						commit
						d232bfea00
					
				
					 3 changed files with 265 additions and 21 deletions
				
			
		|  | @ -62,6 +62,8 @@ pub struct Workspace { | ||||||
|     ///
 |     ///
 | ||||||
|     /// This table is overridden by the project sources.
 |     /// This table is overridden by the project sources.
 | ||||||
|     sources: BTreeMap<PackageName, Source>, |     sources: BTreeMap<PackageName, Source>, | ||||||
|  |     /// The `pyproject.toml` of the workspace root.
 | ||||||
|  |     pyproject_toml: PyProjectToml, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl Workspace { | impl Workspace { | ||||||
|  | @ -323,6 +325,11 @@ impl Workspace { | ||||||
|         &self.sources |         &self.sources | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /// The `pyproject.toml` of the workspace.
 | ||||||
|  |     pub fn pyproject_toml(&self) -> &PyProjectToml { | ||||||
|  |         &self.pyproject_toml | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /// Collect the workspace member projects from the `members` and `excludes` entries.
 |     /// Collect the workspace member projects from the `members` and `excludes` entries.
 | ||||||
|     async fn collect_members( |     async fn collect_members( | ||||||
|         workspace_root: PathBuf, |         workspace_root: PathBuf, | ||||||
|  | @ -440,6 +447,7 @@ impl Workspace { | ||||||
|         } |         } | ||||||
|         let workspace_sources = workspace_pyproject_toml |         let workspace_sources = workspace_pyproject_toml | ||||||
|             .tool |             .tool | ||||||
|  |             .clone() | ||||||
|             .and_then(|tool| tool.uv) |             .and_then(|tool| tool.uv) | ||||||
|             .and_then(|uv| uv.sources) |             .and_then(|uv| uv.sources) | ||||||
|             .unwrap_or_default(); |             .unwrap_or_default(); | ||||||
|  | @ -451,6 +459,7 @@ impl Workspace { | ||||||
|             lock_path, |             lock_path, | ||||||
|             packages: workspace_members, |             packages: workspace_members, | ||||||
|             sources: workspace_sources, |             sources: workspace_sources, | ||||||
|  |             pyproject_toml: workspace_pyproject_toml, | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -753,6 +762,7 @@ impl ProjectWorkspace { | ||||||
|                     // There may be package sources, but we don't need to duplicate them into the
 |                     // There may be package sources, but we don't need to duplicate them into the
 | ||||||
|                     // workspace sources.
 |                     // workspace sources.
 | ||||||
|                     sources: BTreeMap::default(), |                     sources: BTreeMap::default(), | ||||||
|  |                     pyproject_toml: project_pyproject_toml.clone(), | ||||||
|                 }, |                 }, | ||||||
|             }); |             }); | ||||||
|         }; |         }; | ||||||
|  | @ -1150,7 +1160,15 @@ mod tests { | ||||||
|                 "pyproject_toml": "[PYPROJECT_TOML]" |                 "pyproject_toml": "[PYPROJECT_TOML]" | ||||||
|               } |               } | ||||||
|             }, |             }, | ||||||
|             "sources": {} |             "sources": {}, | ||||||
|  |             "pyproject_toml": { | ||||||
|  |               "project": { | ||||||
|  |                 "name": "bird-feeder", | ||||||
|  |                 "requires-python": ">=3.12", | ||||||
|  |                 "optional-dependencies": null | ||||||
|  |               }, | ||||||
|  |               "tool": null | ||||||
|  |             } | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|         "###);
 |         "###);
 | ||||||
|  | @ -1186,7 +1204,15 @@ mod tests { | ||||||
|                     "pyproject_toml": "[PYPROJECT_TOML]" |                     "pyproject_toml": "[PYPROJECT_TOML]" | ||||||
|                   } |                   } | ||||||
|                 }, |                 }, | ||||||
|                 "sources": {} |                 "sources": {}, | ||||||
|  |                 "pyproject_toml": { | ||||||
|  |                   "project": { | ||||||
|  |                     "name": "bird-feeder", | ||||||
|  |                     "requires-python": ">=3.12", | ||||||
|  |                     "optional-dependencies": null | ||||||
|  |                   }, | ||||||
|  |                   "tool": null | ||||||
|  |                 } | ||||||
|               } |               } | ||||||
|             } |             } | ||||||
|             "###);
 |             "###);
 | ||||||
|  | @ -1244,6 +1270,33 @@ mod tests { | ||||||
|                     "workspace": true, |                     "workspace": true, | ||||||
|                     "editable": null |                     "editable": null | ||||||
|                   } |                   } | ||||||
|  |                 }, | ||||||
|  |                 "pyproject_toml": { | ||||||
|  |                   "project": { | ||||||
|  |                     "name": "albatross", | ||||||
|  |                     "requires-python": ">=3.12", | ||||||
|  |                     "optional-dependencies": null | ||||||
|  |                   }, | ||||||
|  |                   "tool": { | ||||||
|  |                     "uv": { | ||||||
|  |                       "sources": { | ||||||
|  |                         "bird-feeder": { | ||||||
|  |                           "workspace": true, | ||||||
|  |                           "editable": null | ||||||
|  |                         } | ||||||
|  |                       }, | ||||||
|  |                       "workspace": { | ||||||
|  |                         "members": [ | ||||||
|  |                           "packages/*" | ||||||
|  |                         ], | ||||||
|  |                         "exclude": null | ||||||
|  |                       }, | ||||||
|  |                       "managed": null, | ||||||
|  |                       "dev-dependencies": null, | ||||||
|  |                       "override-dependencies": null, | ||||||
|  |                       "constraint-dependencies": null | ||||||
|  |                     } | ||||||
|  |                   } | ||||||
|                 } |                 } | ||||||
|               } |               } | ||||||
|             } |             } | ||||||
|  | @ -1298,7 +1351,25 @@ mod tests { | ||||||
|                     "pyproject_toml": "[PYPROJECT_TOML]" |                     "pyproject_toml": "[PYPROJECT_TOML]" | ||||||
|                   } |                   } | ||||||
|                 }, |                 }, | ||||||
|                 "sources": {} |                 "sources": {}, | ||||||
|  |                 "pyproject_toml": { | ||||||
|  |                   "project": null, | ||||||
|  |                   "tool": { | ||||||
|  |                     "uv": { | ||||||
|  |                       "sources": null, | ||||||
|  |                       "workspace": { | ||||||
|  |                         "members": [ | ||||||
|  |                           "packages/*" | ||||||
|  |                         ], | ||||||
|  |                         "exclude": null | ||||||
|  |                       }, | ||||||
|  |                       "managed": null, | ||||||
|  |                       "dev-dependencies": null, | ||||||
|  |                       "override-dependencies": null, | ||||||
|  |                       "constraint-dependencies": null | ||||||
|  |                     } | ||||||
|  |                   } | ||||||
|  |                 } | ||||||
|               } |               } | ||||||
|             } |             } | ||||||
|             "###);
 |             "###);
 | ||||||
|  | @ -1333,7 +1404,15 @@ mod tests { | ||||||
|                     "pyproject_toml": "[PYPROJECT_TOML]" |                     "pyproject_toml": "[PYPROJECT_TOML]" | ||||||
|                   } |                   } | ||||||
|                 }, |                 }, | ||||||
|                 "sources": {} |                 "sources": {}, | ||||||
|  |                 "pyproject_toml": { | ||||||
|  |                   "project": { | ||||||
|  |                     "name": "albatross", | ||||||
|  |                     "requires-python": ">=3.12", | ||||||
|  |                     "optional-dependencies": null | ||||||
|  |                   }, | ||||||
|  |                   "tool": null | ||||||
|  |                 } | ||||||
|               } |               } | ||||||
|             } |             } | ||||||
|             "###);
 |             "###);
 | ||||||
|  |  | ||||||
|  | @ -3,12 +3,13 @@ use std::path::PathBuf; | ||||||
| 
 | 
 | ||||||
| use anyhow::Result; | use anyhow::Result; | ||||||
| use owo_colors::OwoColorize; | use owo_colors::OwoColorize; | ||||||
|  | 
 | ||||||
| use pep508_rs::PackageName; | use pep508_rs::PackageName; | ||||||
| use uv_configuration::PreviewMode; | use uv_configuration::PreviewMode; | ||||||
| use uv_fs::Simplified; | use uv_fs::Simplified; | ||||||
| use uv_warnings::warn_user_once; | use uv_warnings::warn_user_once; | ||||||
| use uv_workspace::pyproject_mut::PyProjectTomlMut; | use uv_workspace::pyproject_mut::PyProjectTomlMut; | ||||||
| use uv_workspace::{ProjectWorkspace, WorkspaceError}; | use uv_workspace::{Workspace, WorkspaceError}; | ||||||
| 
 | 
 | ||||||
| use crate::commands::ExitStatus; | use crate::commands::ExitStatus; | ||||||
| use crate::printer::Printer; | use crate::printer::Printer; | ||||||
|  | @ -53,7 +54,7 @@ pub(crate) async fn init( | ||||||
|             .unwrap_or_else(|_| path.simplified().to_path_buf()); |             .unwrap_or_else(|_| path.simplified().to_path_buf()); | ||||||
| 
 | 
 | ||||||
|         anyhow::bail!( |         anyhow::bail!( | ||||||
|             "Project is already initialized in {}", |             "Project is already initialized in `{}`", | ||||||
|             path.display().cyan() |             path.display().cyan() | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
|  | @ -69,8 +70,9 @@ pub(crate) async fn init( | ||||||
|     let workspace = if isolated { |     let workspace = if isolated { | ||||||
|         None |         None | ||||||
|     } else { |     } else { | ||||||
|         match ProjectWorkspace::discover(&path, None).await { |         // Attempt to find a workspace root.
 | ||||||
|             Ok(project) => Some(project), |         match Workspace::discover(&path, None).await { | ||||||
|  |             Ok(workspace) => Some(workspace), | ||||||
|             Err(WorkspaceError::MissingPyprojectToml) => None, |             Err(WorkspaceError::MissingPyprojectToml) => None, | ||||||
|             Err(err) => return Err(err.into()), |             Err(err) => return Err(err.into()), | ||||||
|         } |         } | ||||||
|  | @ -114,25 +116,20 @@ pub(crate) async fn init( | ||||||
| 
 | 
 | ||||||
|     if let Some(workspace) = workspace { |     if let Some(workspace) = workspace { | ||||||
|         // Add the package to the workspace.
 |         // Add the package to the workspace.
 | ||||||
|         let mut pyproject = |         let mut pyproject = PyProjectTomlMut::from_toml(workspace.pyproject_toml())?; | ||||||
|             PyProjectTomlMut::from_toml(workspace.current_project().pyproject_toml())?; |         pyproject.add_workspace(path.strip_prefix(workspace.install_path())?)?; | ||||||
|         pyproject.add_workspace(path.strip_prefix(workspace.project_root())?)?; |  | ||||||
| 
 | 
 | ||||||
|         // Save the modified `pyproject.toml`.
 |         // Save the modified `pyproject.toml`.
 | ||||||
|         fs_err::write( |         fs_err::write( | ||||||
|             workspace.current_project().root().join("pyproject.toml"), |             workspace.install_path().join("pyproject.toml"), | ||||||
|             pyproject.to_string(), |             pyproject.to_string(), | ||||||
|         )?; |         )?; | ||||||
| 
 | 
 | ||||||
|         writeln!( |         writeln!( | ||||||
|             printer.stderr(), |             printer.stderr(), | ||||||
|             "Adding {} as member of workspace {}", |             "Adding `{}` as member of workspace `{}`", | ||||||
|             name.cyan(), |             name.cyan(), | ||||||
|             workspace |             workspace.install_path().simplified_display().cyan() | ||||||
|                 .workspace() |  | ||||||
|                 .install_path() |  | ||||||
|                 .simplified_display() |  | ||||||
|                 .cyan() |  | ||||||
|         )?; |         )?; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -200,7 +200,7 @@ fn init_workspace() -> Result<()> { | ||||||
| 
 | 
 | ||||||
|     ----- stderr ----- |     ----- stderr ----- | ||||||
|     warning: `uv init` is experimental and may change without warning |     warning: `uv init` is experimental and may change without warning | ||||||
|     Adding foo as member of workspace [TEMP_DIR]/ |     Adding `foo` as member of workspace `[TEMP_DIR]/` | ||||||
|     Initialized project `foo` |     Initialized project `foo` | ||||||
|     "###);
 |     "###);
 | ||||||
| 
 | 
 | ||||||
|  | @ -295,7 +295,7 @@ fn init_workspace_relative_sub_package() -> Result<()> { | ||||||
| 
 | 
 | ||||||
|     ----- stderr ----- |     ----- stderr ----- | ||||||
|     warning: `uv init` is experimental and may change without warning |     warning: `uv init` is experimental and may change without warning | ||||||
|     Adding foo as member of workspace [TEMP_DIR]/ |     Adding `foo` as member of workspace `[TEMP_DIR]/` | ||||||
|     Initialized project `foo` at `[TEMP_DIR]/foo` |     Initialized project `foo` at `[TEMP_DIR]/foo` | ||||||
|     "###);
 |     "###);
 | ||||||
| 
 | 
 | ||||||
|  | @ -391,7 +391,7 @@ fn init_workspace_outside() -> Result<()> { | ||||||
| 
 | 
 | ||||||
|     ----- stderr ----- |     ----- stderr ----- | ||||||
|     warning: `uv init` is experimental and may change without warning |     warning: `uv init` is experimental and may change without warning | ||||||
|     Adding foo as member of workspace [TEMP_DIR]/ |     Adding `foo` as member of workspace `[TEMP_DIR]/` | ||||||
|     Initialized project `foo` at `[TEMP_DIR]/foo` |     Initialized project `foo` at `[TEMP_DIR]/foo` | ||||||
|     "###);
 |     "###);
 | ||||||
| 
 | 
 | ||||||
|  | @ -556,3 +556,171 @@ fn init_workspace_isolated() -> Result<()> { | ||||||
| 
 | 
 | ||||||
|     Ok(()) |     Ok(()) | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | #[test] | ||||||
|  | fn init_nested_workspace() -> Result<()> { | ||||||
|  |     let context = TestContext::new("3.12"); | ||||||
|  | 
 | ||||||
|  |     let pyproject_toml = context.temp_dir.child("pyproject.toml"); | ||||||
|  |     pyproject_toml.write_str(indoc! { | ||||||
|  |         r#" | ||||||
|  |         [project] | ||||||
|  |         name = "project" | ||||||
|  |         version = "0.1.0" | ||||||
|  |         requires-python = ">=3.12" | ||||||
|  |         "#,
 | ||||||
|  |     })?; | ||||||
|  | 
 | ||||||
|  |     // Create a child from the workspace root.
 | ||||||
|  |     let child = context.temp_dir.join("foo"); | ||||||
|  |     uv_snapshot!(context.filters(), context.init().current_dir(&context.temp_dir).arg(&child), @r###" | ||||||
|  |     success: true | ||||||
|  |     exit_code: 0 | ||||||
|  |     ----- stdout ----- | ||||||
|  | 
 | ||||||
|  |     ----- stderr ----- | ||||||
|  |     warning: `uv init` is experimental and may change without warning | ||||||
|  |     Adding `foo` as member of workspace `[TEMP_DIR]/` | ||||||
|  |     Initialized project `foo` at `[TEMP_DIR]/foo` | ||||||
|  |     "###);
 | ||||||
|  | 
 | ||||||
|  |     // Create a grandchild from the child directory.
 | ||||||
|  |     uv_snapshot!(context.filters(), context.init().current_dir(&child).arg("bar"), @r###" | ||||||
|  |     success: true | ||||||
|  |     exit_code: 0 | ||||||
|  |     ----- stdout ----- | ||||||
|  | 
 | ||||||
|  |     ----- stderr ----- | ||||||
|  |     warning: `uv init` is experimental and may change without warning | ||||||
|  |     Adding `bar` as member of workspace `[TEMP_DIR]/` | ||||||
|  |     Initialized project `bar` at `[TEMP_DIR]/foo/bar` | ||||||
|  |     "###);
 | ||||||
|  | 
 | ||||||
|  |     let workspace = fs_err::read_to_string(pyproject_toml)?; | ||||||
|  |     insta::with_settings!({ | ||||||
|  |         filters => context.filters(), | ||||||
|  |     }, { | ||||||
|  |         assert_snapshot!( | ||||||
|  |             workspace, @r###" | ||||||
|  |         [project] | ||||||
|  |         name = "project" | ||||||
|  |         version = "0.1.0" | ||||||
|  |         requires-python = ">=3.12" | ||||||
|  | 
 | ||||||
|  |         [tool.uv.workspace] | ||||||
|  |         members = ["foo", "foo/bar"] | ||||||
|  |         "###
 | ||||||
|  |         ); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     let pyproject = fs_err::read_to_string(child.join("pyproject.toml"))?; | ||||||
|  |     insta::with_settings!({ | ||||||
|  |         filters => context.filters(), | ||||||
|  |     }, { | ||||||
|  |         assert_snapshot!( | ||||||
|  |             pyproject, @r###" | ||||||
|  |         [project] | ||||||
|  |         name = "foo" | ||||||
|  |         version = "0.1.0" | ||||||
|  |         description = "Add your description here" | ||||||
|  |         readme = "README.md" | ||||||
|  |         dependencies = [] | ||||||
|  | 
 | ||||||
|  |         [tool.uv] | ||||||
|  |         dev-dependencies = [] | ||||||
|  |         "###
 | ||||||
|  |         ); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     Ok(()) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// Run `uv init` from within a workspace with an explicit root.
 | ||||||
|  | #[test] | ||||||
|  | fn init_explicit_workspace() -> Result<()> { | ||||||
|  |     let context = TestContext::new("3.12"); | ||||||
|  | 
 | ||||||
|  |     let pyproject_toml = context.temp_dir.child("pyproject.toml"); | ||||||
|  |     pyproject_toml.write_str(indoc! { | ||||||
|  |         r#" | ||||||
|  |         [project] | ||||||
|  |         name = "project" | ||||||
|  |         version = "0.1.0" | ||||||
|  |         requires-python = ">=3.12" | ||||||
|  | 
 | ||||||
|  |         [tool.uv.workspace] | ||||||
|  |         members = [] | ||||||
|  |         "#,
 | ||||||
|  |     })?; | ||||||
|  | 
 | ||||||
|  |     let child = context.temp_dir.join("foo"); | ||||||
|  |     uv_snapshot!(context.filters(), context.init().current_dir(&context.temp_dir).arg(&child), @r###" | ||||||
|  |     success: true | ||||||
|  |     exit_code: 0 | ||||||
|  |     ----- stdout ----- | ||||||
|  | 
 | ||||||
|  |     ----- stderr ----- | ||||||
|  |     warning: `uv init` is experimental and may change without warning | ||||||
|  |     Adding `foo` as member of workspace `[TEMP_DIR]/` | ||||||
|  |     Initialized project `foo` at `[TEMP_DIR]/foo` | ||||||
|  |     "###);
 | ||||||
|  | 
 | ||||||
|  |     let workspace = fs_err::read_to_string(context.temp_dir.join("pyproject.toml"))?; | ||||||
|  |     insta::with_settings!({ | ||||||
|  |         filters => context.filters(), | ||||||
|  |     }, { | ||||||
|  |         assert_snapshot!( | ||||||
|  |             workspace, @r###" | ||||||
|  |         [project] | ||||||
|  |         name = "project" | ||||||
|  |         version = "0.1.0" | ||||||
|  |         requires-python = ">=3.12" | ||||||
|  | 
 | ||||||
|  |         [tool.uv.workspace] | ||||||
|  |         members = ["foo"] | ||||||
|  |         "###
 | ||||||
|  |         ); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     Ok(()) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// Run `uv init` from within a virtual workspace.
 | ||||||
|  | #[test] | ||||||
|  | fn init_virtual_workspace() -> Result<()> { | ||||||
|  |     let context = TestContext::new("3.12"); | ||||||
|  | 
 | ||||||
|  |     let pyproject_toml = context.temp_dir.child("pyproject.toml"); | ||||||
|  |     pyproject_toml.write_str(indoc! { | ||||||
|  |         r" | ||||||
|  |         [tool.uv.workspace] | ||||||
|  |         members = [] | ||||||
|  |         ",
 | ||||||
|  |     })?; | ||||||
|  | 
 | ||||||
|  |     let child = context.temp_dir.join("foo"); | ||||||
|  |     uv_snapshot!(context.filters(), context.init().current_dir(&context.temp_dir).arg(&child), @r###" | ||||||
|  |     success: true | ||||||
|  |     exit_code: 0 | ||||||
|  |     ----- stdout ----- | ||||||
|  | 
 | ||||||
|  |     ----- stderr ----- | ||||||
|  |     warning: `uv init` is experimental and may change without warning | ||||||
|  |     Adding `foo` as member of workspace `[TEMP_DIR]/` | ||||||
|  |     Initialized project `foo` at `[TEMP_DIR]/foo` | ||||||
|  |     "###);
 | ||||||
|  | 
 | ||||||
|  |     let workspace = fs_err::read_to_string(context.temp_dir.join("pyproject.toml"))?; | ||||||
|  |     insta::with_settings!({ | ||||||
|  |         filters => context.filters(), | ||||||
|  |     }, { | ||||||
|  |         assert_snapshot!( | ||||||
|  |             workspace, @r###" | ||||||
|  |         [tool.uv.workspace] | ||||||
|  |         members = ["foo"] | ||||||
|  |         "###
 | ||||||
|  |         ); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     Ok(()) | ||||||
|  | } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Jo
						Jo