mirror of
https://github.com/astral-sh/uv.git
synced 2025-10-29 03:02:55 +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