uv init should not create nested workspace (#5293)

## Summary

Resolves #5251
This commit is contained in:
Jo 2024-07-23 07:48:40 +08:00 committed by GitHub
parent 26e042a794
commit d232bfea00
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 265 additions and 21 deletions

View file

@ -62,6 +62,8 @@ pub struct Workspace {
///
/// This table is overridden by the project sources.
sources: BTreeMap<PackageName, Source>,
/// The `pyproject.toml` of the workspace root.
pyproject_toml: PyProjectToml,
}
impl Workspace {
@ -323,6 +325,11 @@ impl Workspace {
&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.
async fn collect_members(
workspace_root: PathBuf,
@ -440,6 +447,7 @@ impl Workspace {
}
let workspace_sources = workspace_pyproject_toml
.tool
.clone()
.and_then(|tool| tool.uv)
.and_then(|uv| uv.sources)
.unwrap_or_default();
@ -451,6 +459,7 @@ impl Workspace {
lock_path,
packages: workspace_members,
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
// workspace sources.
sources: BTreeMap::default(),
pyproject_toml: project_pyproject_toml.clone(),
},
});
};
@ -1150,7 +1160,15 @@ mod tests {
"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]"
}
},
"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,
"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]"
}
},
"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]"
}
},
"sources": {}
"sources": {},
"pyproject_toml": {
"project": {
"name": "albatross",
"requires-python": ">=3.12",
"optional-dependencies": null
},
"tool": null
}
}
}
"###);

View file

@ -3,12 +3,13 @@ use std::path::PathBuf;
use anyhow::Result;
use owo_colors::OwoColorize;
use pep508_rs::PackageName;
use uv_configuration::PreviewMode;
use uv_fs::Simplified;
use uv_warnings::warn_user_once;
use uv_workspace::pyproject_mut::PyProjectTomlMut;
use uv_workspace::{ProjectWorkspace, WorkspaceError};
use uv_workspace::{Workspace, WorkspaceError};
use crate::commands::ExitStatus;
use crate::printer::Printer;
@ -53,7 +54,7 @@ pub(crate) async fn init(
.unwrap_or_else(|_| path.simplified().to_path_buf());
anyhow::bail!(
"Project is already initialized in {}",
"Project is already initialized in `{}`",
path.display().cyan()
);
}
@ -69,8 +70,9 @@ pub(crate) async fn init(
let workspace = if isolated {
None
} else {
match ProjectWorkspace::discover(&path, None).await {
Ok(project) => Some(project),
// Attempt to find a workspace root.
match Workspace::discover(&path, None).await {
Ok(workspace) => Some(workspace),
Err(WorkspaceError::MissingPyprojectToml) => None,
Err(err) => return Err(err.into()),
}
@ -114,25 +116,20 @@ pub(crate) async fn init(
if let Some(workspace) = workspace {
// Add the package to the workspace.
let mut pyproject =
PyProjectTomlMut::from_toml(workspace.current_project().pyproject_toml())?;
pyproject.add_workspace(path.strip_prefix(workspace.project_root())?)?;
let mut pyproject = PyProjectTomlMut::from_toml(workspace.pyproject_toml())?;
pyproject.add_workspace(path.strip_prefix(workspace.install_path())?)?;
// Save the modified `pyproject.toml`.
fs_err::write(
workspace.current_project().root().join("pyproject.toml"),
workspace.install_path().join("pyproject.toml"),
pyproject.to_string(),
)?;
writeln!(
printer.stderr(),
"Adding {} as member of workspace {}",
"Adding `{}` as member of workspace `{}`",
name.cyan(),
workspace
.workspace()
.install_path()
.simplified_display()
.cyan()
workspace.install_path().simplified_display().cyan()
)?;
}

View file

@ -200,7 +200,7 @@ fn init_workspace() -> Result<()> {
----- stderr -----
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`
"###);
@ -295,7 +295,7 @@ fn init_workspace_relative_sub_package() -> Result<()> {
----- stderr -----
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`
"###);
@ -391,7 +391,7 @@ fn init_workspace_outside() -> Result<()> {
----- stderr -----
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`
"###);
@ -556,3 +556,171 @@ fn init_workspace_isolated() -> Result<()> {
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(())
}