From 7b59af638215363c69b342551f4700d3ac485fa6 Mon Sep 17 00:00:00 2001 From: j178 <10510431+j178@users.noreply.github.com> Date: Wed, 24 Jul 2024 12:28:03 +0800 Subject: [PATCH 1/2] Add `uv init --virtual` --- crates/uv-cli/src/lib.rs | 4 + crates/uv-workspace/src/lib.rs | 3 +- crates/uv-workspace/src/workspace.rs | 4 +- crates/uv/src/commands/project/init.rs | 140 +++++++++++++++++-------- crates/uv/src/lib.rs | 1 + crates/uv/src/settings.rs | 3 + crates/uv/tests/init.rs | 81 ++++++++++++-- 7 files changed, 186 insertions(+), 50 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index aff0af791c3e..0c589769bfbc 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -1794,6 +1794,10 @@ pub struct InitArgs { #[arg(long)] pub name: Option, + /// Create a virtual workspace instead of a project. + #[arg(long)] + pub r#virtual: bool, + /// Do not create a readme file. #[arg(long)] pub no_readme: bool, diff --git a/crates/uv-workspace/src/lib.rs b/crates/uv-workspace/src/lib.rs index db7979cfb920..f9bc90906388 100644 --- a/crates/uv-workspace/src/lib.rs +++ b/crates/uv-workspace/src/lib.rs @@ -1,5 +1,6 @@ pub use workspace::{ - DiscoveryOptions, ProjectWorkspace, VirtualProject, Workspace, WorkspaceError, WorkspaceMember, + check_nested_workspaces, DiscoveryOptions, ProjectWorkspace, VirtualProject, Workspace, + WorkspaceError, WorkspaceMember, }; pub mod pyproject; diff --git a/crates/uv-workspace/src/workspace.rs b/crates/uv-workspace/src/workspace.rs index 8f060dc5dbc6..9607f5fd9532 100644 --- a/crates/uv-workspace/src/workspace.rs +++ b/crates/uv-workspace/src/workspace.rs @@ -936,7 +936,7 @@ async fn find_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 .ancestors() .take_while(|path| { @@ -996,7 +996,7 @@ fn check_nested_workspaces(inner_workspace_root: &Path, options: &DiscoveryOptio if !is_excluded { warn_user!( "Nested workspaces are not supported, but outer workspace includes existing workspace: `{}`", - pyproject_toml_path.user_display().cyan(), + outer_workspace_root.simplified_display().cyan(), ); } } diff --git a/crates/uv/src/commands/project/init.rs b/crates/uv/src/commands/project/init.rs index 7ab1a702019a..1e8753340e68 100644 --- a/crates/uv/src/commands/project/init.rs +++ b/crates/uv/src/commands/project/init.rs @@ -1,5 +1,5 @@ use std::fmt::Write; -use std::path::PathBuf; +use std::path::Path; use anyhow::{Context, Result}; use owo_colors::OwoColorize; @@ -16,7 +16,7 @@ use uv_python::{ use uv_resolver::RequiresPython; use uv_warnings::warn_user_once; 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::reporters::PythonDownloadReporter; @@ -24,10 +24,11 @@ use crate::commands::ExitStatus; use crate::printer::Printer; /// 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( explicit_path: Option, name: Option, + r#virtual: bool, no_readme: bool, python: Option, isolated: bool, @@ -46,7 +47,7 @@ pub(crate) async fn init( // Default to the current directory if a path was not provided. let path = match explicit_path { 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. @@ -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. let name = match name { Some(name) => name, @@ -77,6 +75,94 @@ 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) +} + +fn init_virtual_workspace(path: &Path, isolated: bool) -> Result<()> { + // Check nested workspaces. + 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(()) +} + +async fn init_project( + path: &Path, + name: &PackageName, + no_readme: bool, + python: Option, + 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. let workspace = if isolated { None @@ -86,7 +172,7 @@ pub(crate) async fn init( match Workspace::discover( parent, &DiscoveryOptions { - ignore: std::iter::once(path.as_ref()).collect(), + ignore: std::iter::once(path).collect(), ..DiscoveryOptions::default() }, ) @@ -180,7 +266,8 @@ pub(crate) async fn init( readme = if no_readme { "" } else { "\nreadme = \"README.md\"" }, 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)?; // Create `src/{name}/__init__.py` if it does not already exist. @@ -197,16 +284,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 workspace.excludes(&path)? { + if workspace.excludes(path)? { // If the member is excluded by the workspace, ignore it. writeln!( printer.stderr(), @@ -214,7 +293,7 @@ pub(crate) async fn init( name.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. writeln!( printer.stderr(), @@ -242,26 +321,5 @@ pub(crate) async fn init( } } - 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 project `{}` at `{}`", - name.cyan(), - path.display().cyan() - )?; - } - } - - Ok(ExitStatus::Success) + Ok(()) } diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index f35ab03f949b..bd889d152853 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -857,6 +857,7 @@ async fn run_project( commands::init( args.path, args.name, + args.r#virtual, args.no_readme, args.python, globals.isolated, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index aa1ae5e72456..2815d4597166 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -153,6 +153,7 @@ impl CacheSettings { pub(crate) struct InitSettings { pub(crate) path: Option, pub(crate) name: Option, + pub(crate) r#virtual: bool, pub(crate) no_readme: bool, pub(crate) python: Option, } @@ -164,6 +165,7 @@ impl InitSettings { let InitArgs { path, name, + r#virtual, no_readme, python, } = args; @@ -171,6 +173,7 @@ impl InitSettings { Self { path, name, + r#virtual, no_readme, python, } diff --git a/crates/uv/tests/init.rs b/crates/uv/tests/init.rs index 212552d51efc..25139f13e936 100644 --- a/crates/uv/tests/init.rs +++ b/crates/uv/tests/init.rs @@ -627,7 +627,7 @@ fn init_workspace_isolated() -> Result<()> { } #[test] -fn init_nested_workspace() -> Result<()> { +fn init_project_inside_project() -> Result<()> { let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -760,6 +760,64 @@ fn init_explicit_workspace() -> Result<()> { fn init_virtual_workspace() -> Result<()> { 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"); pyproject_toml.write_str(indoc! { r" @@ -768,18 +826,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(&child), @r###" + uv_snapshot!(context.filters(), context.init().current_dir(&context.temp_dir).arg("--virtual").arg("foo"), @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` + warning: Nested workspaces are not supported, but outer workspace includes existing workspace: `[TEMP_DIR]/` + 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"))?; insta::with_settings!({ filters => context.filters(), @@ -787,7 +856,7 @@ fn init_virtual_workspace() -> Result<()> { assert_snapshot!( workspace, @r###" [tool.uv.workspace] - members = ["foo"] + members = [] "### ); }); From 60338a42cdb1abdce0218bc04fce6259f9c2b512 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 24 Jul 2024 14:43:12 -0400 Subject: [PATCH 2/2] Small tweaks --- crates/uv-cli/src/lib.rs | 2 +- crates/uv-workspace/src/workspace.rs | 10 +++++++--- crates/uv/src/commands/project/init.rs | 4 +++- crates/uv/tests/init.rs | 2 +- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 0c589769bfbc..64982e255513 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -1798,7 +1798,7 @@ pub struct InitArgs { #[arg(long)] pub r#virtual: bool, - /// Do not create a readme file. + /// Do not create a `README.md` file. #[arg(long)] pub no_readme: bool, diff --git a/crates/uv-workspace/src/workspace.rs b/crates/uv-workspace/src/workspace.rs index 6858a5a670cc..42d23a6ae91f 100644 --- a/crates/uv-workspace/src/workspace.rs +++ b/crates/uv-workspace/src/workspace.rs @@ -160,6 +160,8 @@ impl Workspace { workspace_root.simplified_display() ); + check_nested_workspaces(&workspace_root, options); + // Unlike in `ProjectWorkspace` discovery, we might be in a virtual workspace root without // being in any specific project. let current_project = pyproject_toml @@ -170,6 +172,7 @@ impl Workspace { project, pyproject_toml, }); + Self::collect_members( workspace_root.clone(), // This method supports only absolute paths. @@ -526,8 +529,6 @@ impl Workspace { .and_then(|uv| uv.sources) .unwrap_or_default(); - check_nested_workspaces(&workspace_root, options); - Ok(Workspace { install_path: workspace_root, lock_path, @@ -1025,8 +1026,9 @@ pub fn check_nested_workspaces(inner_workspace_root: &Path, options: &DiscoveryO }; if !is_excluded { warn_user!( - "Nested workspaces are not supported, but outer workspace includes existing workspace: `{}`", + "Nested workspaces are not supported, but outer workspace (`{}`) includes `{}`", outer_workspace_root.simplified_display().cyan(), + inner_workspace_root.simplified_display().cyan() ); } } @@ -1159,6 +1161,8 @@ impl VirtualProject { .map_err(WorkspaceError::Normalize)? .to_path_buf(); + check_nested_workspaces(&project_path, options); + let workspace = Workspace::collect_members( project_path, PathBuf::new(), diff --git a/crates/uv/src/commands/project/init.rs b/crates/uv/src/commands/project/init.rs index cd49ff6c5893..87d612377dbc 100644 --- a/crates/uv/src/commands/project/init.rs +++ b/crates/uv/src/commands/project/init.rs @@ -132,8 +132,9 @@ pub(crate) async fn init( Ok(ExitStatus::Success) } +/// Initialize a virtual workspace at the given path. fn init_virtual_workspace(path: &Path, isolated: bool) -> Result<()> { - // Check nested workspaces. + // Ensure that we aren't creating a nested workspace. if !isolated { check_nested_workspaces(path, &DiscoveryOptions::default()); } @@ -150,6 +151,7 @@ fn init_virtual_workspace(path: &Path, isolated: bool) -> Result<()> { Ok(()) } +/// Initialize a project (and, implicitly, a workspace root) at the given path. async fn init_project( path: &Path, name: &PackageName, diff --git a/crates/uv/tests/init.rs b/crates/uv/tests/init.rs index a0eb605adcba..c60fc87f7621 100644 --- a/crates/uv/tests/init.rs +++ b/crates/uv/tests/init.rs @@ -806,7 +806,7 @@ fn init_nested_virtual_workspace() -> Result<()> { ----- stderr ----- warning: `uv init` is experimental and may change without warning - warning: Nested workspaces are not supported, but outer workspace includes existing workspace: `[TEMP_DIR]/` + warning: Nested workspaces are not supported, but outer workspace (`[TEMP_DIR]/`) includes `[TEMP_DIR]/foo` Initialized workspace `foo` at `[TEMP_DIR]/foo` "###);