mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-30 08:33:49 +00:00
Add uv init --virtual
(#5396)
## Summary Add `uv init --virtual` to create an explicit virtual workspace. Relates to #5338
This commit is contained in:
parent
ac614ee70f
commit
7bcafec778
7 changed files with 196 additions and 54 deletions
|
@ -1794,7 +1794,11 @@ pub struct InitArgs {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub name: Option<PackageName>,
|
pub name: Option<PackageName>,
|
||||||
|
|
||||||
/// Do not create a readme file.
|
/// Create a virtual workspace instead of a project.
|
||||||
|
#[arg(long)]
|
||||||
|
pub r#virtual: bool,
|
||||||
|
|
||||||
|
/// Do not create a `README.md` file.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub no_readme: bool,
|
pub no_readme: bool,
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
pub use workspace::{
|
pub use workspace::{
|
||||||
DiscoveryOptions, ProjectWorkspace, VirtualProject, Workspace, WorkspaceError, WorkspaceMember,
|
check_nested_workspaces, DiscoveryOptions, ProjectWorkspace, VirtualProject, Workspace,
|
||||||
|
WorkspaceError, WorkspaceMember,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod pyproject;
|
pub mod pyproject;
|
||||||
|
|
|
@ -160,6 +160,8 @@ impl Workspace {
|
||||||
workspace_root.simplified_display()
|
workspace_root.simplified_display()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
check_nested_workspaces(&workspace_root, options);
|
||||||
|
|
||||||
// Unlike in `ProjectWorkspace` discovery, we might be in a virtual workspace root without
|
// Unlike in `ProjectWorkspace` discovery, we might be in a virtual workspace root without
|
||||||
// being in any specific project.
|
// being in any specific project.
|
||||||
let current_project = pyproject_toml
|
let current_project = pyproject_toml
|
||||||
|
@ -170,6 +172,7 @@ impl Workspace {
|
||||||
project,
|
project,
|
||||||
pyproject_toml,
|
pyproject_toml,
|
||||||
});
|
});
|
||||||
|
|
||||||
Self::collect_members(
|
Self::collect_members(
|
||||||
workspace_root.clone(),
|
workspace_root.clone(),
|
||||||
// This method supports only absolute paths.
|
// This method supports only absolute paths.
|
||||||
|
@ -526,8 +529,6 @@ impl Workspace {
|
||||||
.and_then(|uv| uv.sources)
|
.and_then(|uv| uv.sources)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
check_nested_workspaces(&workspace_root, options);
|
|
||||||
|
|
||||||
Ok(Workspace {
|
Ok(Workspace {
|
||||||
install_path: workspace_root,
|
install_path: workspace_root,
|
||||||
lock_path,
|
lock_path,
|
||||||
|
@ -966,7 +967,7 @@ async fn find_workspace(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Warn when the valid workspace is included in another workspace.
|
/// Warn when the valid workspace is included in another workspace.
|
||||||
fn check_nested_workspaces(inner_workspace_root: &Path, options: &DiscoveryOptions) {
|
pub fn check_nested_workspaces(inner_workspace_root: &Path, options: &DiscoveryOptions) {
|
||||||
for outer_workspace_root in inner_workspace_root
|
for outer_workspace_root in inner_workspace_root
|
||||||
.ancestors()
|
.ancestors()
|
||||||
.take_while(|path| {
|
.take_while(|path| {
|
||||||
|
@ -1025,8 +1026,9 @@ fn check_nested_workspaces(inner_workspace_root: &Path, options: &DiscoveryOptio
|
||||||
};
|
};
|
||||||
if !is_excluded {
|
if !is_excluded {
|
||||||
warn_user!(
|
warn_user!(
|
||||||
"Nested workspaces are not supported, but outer workspace includes existing workspace: `{}`",
|
"Nested workspaces are not supported, but outer workspace (`{}`) includes `{}`",
|
||||||
pyproject_toml_path.user_display().cyan(),
|
outer_workspace_root.simplified_display().cyan(),
|
||||||
|
inner_workspace_root.simplified_display().cyan()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1159,6 +1161,8 @@ impl VirtualProject {
|
||||||
.map_err(WorkspaceError::Normalize)?
|
.map_err(WorkspaceError::Normalize)?
|
||||||
.to_path_buf();
|
.to_path_buf();
|
||||||
|
|
||||||
|
check_nested_workspaces(&project_path, options);
|
||||||
|
|
||||||
let workspace = Workspace::collect_members(
|
let workspace = Workspace::collect_members(
|
||||||
project_path,
|
project_path,
|
||||||
PathBuf::new(),
|
PathBuf::new(),
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
use std::path::PathBuf;
|
use std::path::Path;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use owo_colors::OwoColorize;
|
use owo_colors::OwoColorize;
|
||||||
|
@ -16,7 +16,7 @@ use uv_python::{
|
||||||
use uv_resolver::RequiresPython;
|
use uv_resolver::RequiresPython;
|
||||||
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::{DiscoveryOptions, Workspace, WorkspaceError};
|
use uv_workspace::{check_nested_workspaces, DiscoveryOptions, Workspace, WorkspaceError};
|
||||||
|
|
||||||
use crate::commands::project::find_requires_python;
|
use crate::commands::project::find_requires_python;
|
||||||
use crate::commands::reporters::PythonDownloadReporter;
|
use crate::commands::reporters::PythonDownloadReporter;
|
||||||
|
@ -24,10 +24,11 @@ use crate::commands::ExitStatus;
|
||||||
use crate::printer::Printer;
|
use crate::printer::Printer;
|
||||||
|
|
||||||
/// Add one or more packages to the project requirements.
|
/// Add one or more packages to the project requirements.
|
||||||
#[allow(clippy::single_match_else)]
|
#[allow(clippy::single_match_else, clippy::fn_params_excessive_bools)]
|
||||||
pub(crate) async fn init(
|
pub(crate) async fn init(
|
||||||
explicit_path: Option<String>,
|
explicit_path: Option<String>,
|
||||||
name: Option<PackageName>,
|
name: Option<PackageName>,
|
||||||
|
r#virtual: bool,
|
||||||
no_readme: bool,
|
no_readme: bool,
|
||||||
python: Option<String>,
|
python: Option<String>,
|
||||||
isolated: bool,
|
isolated: bool,
|
||||||
|
@ -46,7 +47,7 @@ pub(crate) async fn init(
|
||||||
// Default to the current directory if a path was not provided.
|
// Default to the current directory if a path was not provided.
|
||||||
let path = match explicit_path {
|
let path = match explicit_path {
|
||||||
None => std::env::current_dir()?.canonicalize()?,
|
None => std::env::current_dir()?.canonicalize()?,
|
||||||
Some(ref path) => PathBuf::from(path),
|
Some(ref path) => absolutize_path(Path::new(path))?.to_path_buf(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Make sure a project does not already exist in the given directory.
|
// Make sure a project does not already exist in the given directory.
|
||||||
|
@ -61,9 +62,6 @@ pub(crate) async fn init(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Canonicalize the path to the project.
|
|
||||||
let path = absolutize_path(&path)?;
|
|
||||||
|
|
||||||
// Default to the directory name if a name was not provided.
|
// Default to the directory name if a name was not provided.
|
||||||
let name = match name {
|
let name = match name {
|
||||||
Some(name) => name,
|
Some(name) => name,
|
||||||
|
@ -77,6 +75,96 @@ pub(crate) async fn init(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if r#virtual {
|
||||||
|
init_virtual_workspace(&path, isolated)?;
|
||||||
|
} else {
|
||||||
|
init_project(
|
||||||
|
&path,
|
||||||
|
&name,
|
||||||
|
no_readme,
|
||||||
|
python,
|
||||||
|
isolated,
|
||||||
|
python_preference,
|
||||||
|
python_fetch,
|
||||||
|
connectivity,
|
||||||
|
native_tls,
|
||||||
|
cache,
|
||||||
|
printer,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the `README.md` if it does not already exist.
|
||||||
|
if !no_readme {
|
||||||
|
let readme = path.join("README.md");
|
||||||
|
if !readme.exists() {
|
||||||
|
fs_err::write(readme, String::new())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let project = if r#virtual { "workspace" } else { "project" };
|
||||||
|
match explicit_path {
|
||||||
|
// Initialized a project in the current directory.
|
||||||
|
None => {
|
||||||
|
writeln!(
|
||||||
|
printer.stderr(),
|
||||||
|
"Initialized {} `{}`",
|
||||||
|
project,
|
||||||
|
name.cyan()
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
// Initialized a project in the given directory.
|
||||||
|
Some(path) => {
|
||||||
|
let path = path
|
||||||
|
.simple_canonicalize()
|
||||||
|
.unwrap_or_else(|_| path.simplified().to_path_buf());
|
||||||
|
|
||||||
|
writeln!(
|
||||||
|
printer.stderr(),
|
||||||
|
"Initialized {} `{}` at `{}`",
|
||||||
|
project,
|
||||||
|
name.cyan(),
|
||||||
|
path.display().cyan()
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ExitStatus::Success)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize a virtual workspace at the given path.
|
||||||
|
fn init_virtual_workspace(path: &Path, isolated: bool) -> Result<()> {
|
||||||
|
// Ensure that we aren't creating a nested workspace.
|
||||||
|
if !isolated {
|
||||||
|
check_nested_workspaces(path, &DiscoveryOptions::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the `pyproject.toml`.
|
||||||
|
let pyproject = indoc::indoc! {r"
|
||||||
|
[tool.uv.workspace]
|
||||||
|
members = []
|
||||||
|
"};
|
||||||
|
|
||||||
|
fs_err::create_dir_all(path)?;
|
||||||
|
fs_err::write(path.join("pyproject.toml"), pyproject)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize a project (and, implicitly, a workspace root) at the given path.
|
||||||
|
async fn init_project(
|
||||||
|
path: &Path,
|
||||||
|
name: &PackageName,
|
||||||
|
no_readme: bool,
|
||||||
|
python: Option<String>,
|
||||||
|
isolated: bool,
|
||||||
|
python_preference: PythonPreference,
|
||||||
|
python_fetch: PythonFetch,
|
||||||
|
connectivity: Connectivity,
|
||||||
|
native_tls: bool,
|
||||||
|
cache: &Cache,
|
||||||
|
printer: Printer,
|
||||||
|
) -> Result<()> {
|
||||||
// Discover the current workspace, if it exists.
|
// Discover the current workspace, if it exists.
|
||||||
let workspace = if isolated {
|
let workspace = if isolated {
|
||||||
None
|
None
|
||||||
|
@ -86,7 +174,7 @@ pub(crate) async fn init(
|
||||||
match Workspace::discover(
|
match Workspace::discover(
|
||||||
parent,
|
parent,
|
||||||
&DiscoveryOptions {
|
&DiscoveryOptions {
|
||||||
ignore: std::iter::once(path.as_ref()).collect(),
|
ignore: std::iter::once(path).collect(),
|
||||||
..DiscoveryOptions::default()
|
..DiscoveryOptions::default()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -177,7 +265,8 @@ pub(crate) async fn init(
|
||||||
readme = if no_readme { "" } else { "\nreadme = \"README.md\"" },
|
readme = if no_readme { "" } else { "\nreadme = \"README.md\"" },
|
||||||
requires_python = requires_python.specifiers(),
|
requires_python = requires_python.specifiers(),
|
||||||
};
|
};
|
||||||
fs_err::create_dir_all(&path)?;
|
|
||||||
|
fs_err::create_dir_all(path)?;
|
||||||
fs_err::write(path.join("pyproject.toml"), pyproject)?;
|
fs_err::write(path.join("pyproject.toml"), pyproject)?;
|
||||||
|
|
||||||
// Create `src/{name}/__init__.py` if it does not already exist.
|
// Create `src/{name}/__init__.py` if it does not already exist.
|
||||||
|
@ -194,16 +283,8 @@ pub(crate) async fn init(
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the `README.md` if it does not already exist.
|
|
||||||
if !no_readme {
|
|
||||||
let readme = path.join("README.md");
|
|
||||||
if !readme.exists() {
|
|
||||||
fs_err::write(readme, String::new())?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(workspace) = workspace {
|
if let Some(workspace) = workspace {
|
||||||
if workspace.excludes(&path)? {
|
if workspace.excludes(path)? {
|
||||||
// If the member is excluded by the workspace, ignore it.
|
// If the member is excluded by the workspace, ignore it.
|
||||||
writeln!(
|
writeln!(
|
||||||
printer.stderr(),
|
printer.stderr(),
|
||||||
|
@ -211,7 +292,7 @@ pub(crate) async fn init(
|
||||||
name.cyan(),
|
name.cyan(),
|
||||||
workspace.install_path().simplified_display().cyan()
|
workspace.install_path().simplified_display().cyan()
|
||||||
)?;
|
)?;
|
||||||
} else if workspace.includes(&path)? {
|
} else if workspace.includes(path)? {
|
||||||
// If the member is already included in the workspace, skip the `members` addition.
|
// If the member is already included in the workspace, skip the `members` addition.
|
||||||
writeln!(
|
writeln!(
|
||||||
printer.stderr(),
|
printer.stderr(),
|
||||||
|
@ -239,26 +320,5 @@ pub(crate) async fn init(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match explicit_path {
|
Ok(())
|
||||||
// Initialized a project in the current directory.
|
|
||||||
None => {
|
|
||||||
writeln!(printer.stderr(), "Initialized project `{}`", name.cyan())?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialized a project in the given directory.
|
|
||||||
Some(path) => {
|
|
||||||
let path = path
|
|
||||||
.simple_canonicalize()
|
|
||||||
.unwrap_or_else(|_| path.simplified().to_path_buf());
|
|
||||||
|
|
||||||
writeln!(
|
|
||||||
printer.stderr(),
|
|
||||||
"Initialized project `{}` at `{}`",
|
|
||||||
name.cyan(),
|
|
||||||
path.display().cyan()
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(ExitStatus::Success)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -857,6 +857,7 @@ async fn run_project(
|
||||||
commands::init(
|
commands::init(
|
||||||
args.path,
|
args.path,
|
||||||
args.name,
|
args.name,
|
||||||
|
args.r#virtual,
|
||||||
args.no_readme,
|
args.no_readme,
|
||||||
args.python,
|
args.python,
|
||||||
globals.isolated,
|
globals.isolated,
|
||||||
|
|
|
@ -153,6 +153,7 @@ impl CacheSettings {
|
||||||
pub(crate) struct InitSettings {
|
pub(crate) struct InitSettings {
|
||||||
pub(crate) path: Option<String>,
|
pub(crate) path: Option<String>,
|
||||||
pub(crate) name: Option<PackageName>,
|
pub(crate) name: Option<PackageName>,
|
||||||
|
pub(crate) r#virtual: bool,
|
||||||
pub(crate) no_readme: bool,
|
pub(crate) no_readme: bool,
|
||||||
pub(crate) python: Option<String>,
|
pub(crate) python: Option<String>,
|
||||||
}
|
}
|
||||||
|
@ -164,6 +165,7 @@ impl InitSettings {
|
||||||
let InitArgs {
|
let InitArgs {
|
||||||
path,
|
path,
|
||||||
name,
|
name,
|
||||||
|
r#virtual,
|
||||||
no_readme,
|
no_readme,
|
||||||
python,
|
python,
|
||||||
} = args;
|
} = args;
|
||||||
|
@ -171,6 +173,7 @@ impl InitSettings {
|
||||||
Self {
|
Self {
|
||||||
path,
|
path,
|
||||||
name,
|
name,
|
||||||
|
r#virtual,
|
||||||
no_readme,
|
no_readme,
|
||||||
python,
|
python,
|
||||||
}
|
}
|
||||||
|
|
|
@ -603,7 +603,7 @@ fn init_workspace_isolated() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn init_nested_workspace() -> Result<()> {
|
fn init_project_inside_project() -> Result<()> {
|
||||||
let context = TestContext::new("3.12");
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||||
|
@ -733,6 +733,64 @@ fn init_explicit_workspace() -> Result<()> {
|
||||||
fn init_virtual_workspace() -> Result<()> {
|
fn init_virtual_workspace() -> Result<()> {
|
||||||
let context = TestContext::new("3.12");
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
let child = context.temp_dir.child("foo");
|
||||||
|
child.create_dir_all()?;
|
||||||
|
|
||||||
|
let pyproject_toml = child.join("pyproject.toml");
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.init().current_dir(&child).arg("--virtual"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: `uv init` is experimental and may change without warning
|
||||||
|
Initialized workspace `foo`
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let pyproject = fs_err::read_to_string(&pyproject_toml)?;
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
assert_snapshot!(
|
||||||
|
pyproject, @r###"
|
||||||
|
[tool.uv.workspace]
|
||||||
|
members = []
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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]/foo`
|
||||||
|
Initialized project `bar` at `[TEMP_DIR]/foo/bar`
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let pyproject = fs_err::read_to_string(pyproject_toml)?;
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
assert_snapshot!(
|
||||||
|
pyproject, @r###"
|
||||||
|
[tool.uv.workspace]
|
||||||
|
members = ["bar"]
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run `uv init --virtual` from within a workspace.
|
||||||
|
#[test]
|
||||||
|
fn init_nested_virtual_workspace() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||||
pyproject_toml.write_str(indoc! {
|
pyproject_toml.write_str(indoc! {
|
||||||
r"
|
r"
|
||||||
|
@ -741,18 +799,29 @@ fn init_virtual_workspace() -> Result<()> {
|
||||||
",
|
",
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let child = context.temp_dir.join("foo");
|
uv_snapshot!(context.filters(), context.init().current_dir(&context.temp_dir).arg("--virtual").arg("foo"), @r###"
|
||||||
uv_snapshot!(context.filters(), context.init().current_dir(&context.temp_dir).arg(&child), @r###"
|
|
||||||
success: true
|
success: true
|
||||||
exit_code: 0
|
exit_code: 0
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
||||||
----- 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]/`
|
warning: Nested workspaces are not supported, but outer workspace (`[TEMP_DIR]/`) includes `[TEMP_DIR]/foo`
|
||||||
Initialized project `foo` at `[TEMP_DIR]/foo`
|
Initialized workspace `foo` at `[TEMP_DIR]/foo`
|
||||||
"###);
|
"###);
|
||||||
|
|
||||||
|
let pyproject = fs_err::read_to_string(context.temp_dir.join("foo").join("pyproject.toml"))?;
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
assert_snapshot!(
|
||||||
|
pyproject, @r###"
|
||||||
|
[tool.uv.workspace]
|
||||||
|
members = []
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
let workspace = fs_err::read_to_string(context.temp_dir.join("pyproject.toml"))?;
|
let workspace = fs_err::read_to_string(context.temp_dir.join("pyproject.toml"))?;
|
||||||
insta::with_settings!({
|
insta::with_settings!({
|
||||||
filters => context.filters(),
|
filters => context.filters(),
|
||||||
|
@ -760,7 +829,7 @@ fn init_virtual_workspace() -> Result<()> {
|
||||||
assert_snapshot!(
|
assert_snapshot!(
|
||||||
workspace, @r###"
|
workspace, @r###"
|
||||||
[tool.uv.workspace]
|
[tool.uv.workspace]
|
||||||
members = ["foo"]
|
members = []
|
||||||
"###
|
"###
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue