From 2825ee343560bd59f1860cffba9690eff56269f9 Mon Sep 17 00:00:00 2001 From: Harshith VH Date: Tue, 16 Sep 2025 18:54:57 +0530 Subject: [PATCH] Add `--no-clear` to `uv venv` to disable removal prompts (#15795) Closes #15485 --------- Co-authored-by: Aditya-PS-05 Co-authored-by: Zanie Blue --- crates/uv-cli/src/lib.rs | 12 +++ crates/uv-virtualenv/src/virtualenv.rs | 61 ++++++------ crates/uv/src/lib.rs | 6 +- crates/uv/src/settings.rs | 3 + crates/uv/tests/it/venv.rs | 123 ++++++++++++++++++++++++- 5 files changed, 173 insertions(+), 32 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index c624c32dc..a5f05ec09 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -2754,6 +2754,18 @@ pub struct VenvArgs { #[clap(long, short, overrides_with = "allow_existing", value_parser = clap::builder::BoolishValueParser::new(), env = EnvVars::UV_VENV_CLEAR)] pub clear: bool, + /// Fail without prompting if any existing files or directories are present at the target path. + /// + /// By default, when a TTY is available, `uv venv` will prompt to clear a non-empty directory. + /// When `--no-clear` is used, the command will exit with an error instead of prompting. + #[clap( + long, + overrides_with = "clear", + conflicts_with = "allow_existing", + hide = true + )] + pub no_clear: bool, + /// Preserve any existing files or directories at the target path. /// /// By default, `uv venv` will exit with an error if the given path is non-empty. The diff --git a/crates/uv-virtualenv/src/virtualenv.rs b/crates/uv-virtualenv/src/virtualenv.rs index 0075526f0..63122d1fe 100644 --- a/crates/uv-virtualenv/src/virtualenv.rs +++ b/crates/uv-virtualenv/src/virtualenv.rs @@ -116,6 +116,22 @@ pub(crate) fn create( } else { "directory" }; + let hint = format!( + "Use the `{}` flag or set `{}` to replace the existing {name}", + "--clear".green(), + "UV_VENV_CLEAR=1".green() + ); + // TODO(zanieb): We may want to consider omitting the hint in some of these cases, e.g., + // when `--no-clear` is used do we want to suggest `--clear`? + let err = Err(Error::Io(io::Error::new( + io::ErrorKind::AlreadyExists, + format!( + "A {name} already exists at: {}\n\n{}{} {hint}", + location.user_display(), + "hint".bold().cyan(), + ":".bold(), + ), + ))); match on_existing { OnExisting::Allow => { debug!("Allowing existing {name} due to `--allow-existing`"); @@ -131,15 +147,11 @@ pub(crate) fn create( remove_virtualenv(&location)?; fs::create_dir_all(&location)?; } - OnExisting::Fail => { - let confirmation = if is_virtualenv { - confirm_clear(location, name)? - } else { - // Refuse to remove a non-virtual environment; don't even prompt. - Some(false) - }; - - match confirmation { + OnExisting::Fail => return err, + // If not a virtual environment, fail without prompting. + OnExisting::Prompt if !is_virtualenv => return err, + OnExisting::Prompt => { + match confirm_clear(location, name)? { Some(true) => { debug!("Removing existing {name} due to confirmation"); // Before removing the virtual environment, we need to canonicalize the @@ -151,22 +163,7 @@ pub(crate) fn create( remove_virtualenv(&location)?; fs::create_dir_all(&location)?; } - Some(false) => { - let hint = format!( - "Use the `{}` flag or set `{}` to replace the existing {name}", - "--clear".green(), - "UV_VENV_CLEAR=1".green() - ); - return Err(Error::Io(io::Error::new( - io::ErrorKind::AlreadyExists, - format!( - "A {name} already exists at: {}\n\n{}{} {hint}", - location.user_display(), - "hint".bold().cyan(), - ":".bold(), - ), - ))); - } + Some(false) => return err, // When we don't have a TTY, warn that the behavior will change in the future None => { warn_user_once!( @@ -637,10 +634,14 @@ pub fn remove_virtualenv(location: &Path) -> Result<(), Error> { Ok(()) } -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)] pub enum OnExisting { - /// Fail if the directory already exists and is non-empty. + /// Prompt before removing an existing directory. + /// + /// If a TTY is not available, fail. #[default] + Prompt, + /// Fail if the directory already exists and is non-empty. Fail, /// Allow an existing directory, overwriting virtual environment files while retaining other /// files in the directory. @@ -650,13 +651,15 @@ pub enum OnExisting { } impl OnExisting { - pub fn from_args(allow_existing: bool, clear: bool) -> Self { + pub fn from_args(allow_existing: bool, clear: bool, no_clear: bool) -> Self { if allow_existing { Self::Allow } else if clear { Self::Remove + } else if no_clear { + Self::Fail } else { - Self::default() + Self::Prompt } } } diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 2b842b28a..0c0d73bcc 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1107,7 +1107,11 @@ async fn run(mut cli: Cli) -> Result { let python_request: Option = args.settings.python.as_deref().map(PythonRequest::parse); - let on_existing = uv_virtualenv::OnExisting::from_args(args.allow_existing, args.clear); + let on_existing = uv_virtualenv::OnExisting::from_args( + args.allow_existing, + args.clear, + args.no_clear, + ); commands::venv( &project_dir, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 607c3ccb2..ece441099 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -2719,6 +2719,7 @@ pub(crate) struct VenvSettings { pub(crate) seed: bool, pub(crate) allow_existing: bool, pub(crate) clear: bool, + pub(crate) no_clear: bool, pub(crate) path: Option, pub(crate) prompt: Option, pub(crate) system_site_packages: bool, @@ -2738,6 +2739,7 @@ impl VenvSettings { seed, allow_existing, clear, + no_clear, path, prompt, system_site_packages, @@ -2757,6 +2759,7 @@ impl VenvSettings { seed, allow_existing, clear, + no_clear, path, prompt, system_site_packages, diff --git a/crates/uv/tests/it/venv.rs b/crates/uv/tests/it/venv.rs index deadad0bd..7f2e0f6eb 100644 --- a/crates/uv/tests/it/venv.rs +++ b/crates/uv/tests/it/venv.rs @@ -978,8 +978,7 @@ fn non_empty_dir_exists() -> Result<()> { Caused by: A directory already exists at: .venv hint: Use the `--clear` flag or set `UV_VENV_CLEAR=1` to replace the existing directory - " - ); + "); uv_snapshot!(context.filters(), context.venv() .arg(context.venv.as_os_str()) @@ -1675,3 +1674,123 @@ fn create_venv_current_working_directory() { " ); } + +#[test] +fn no_clear_with_existing_directory() { + let context = TestContext::new_with_versions(&["3.12"]); + + // Create a virtual environment first + uv_snapshot!(context.filters(), context.venv() + .arg(context.venv.as_os_str()) + .arg("--python") + .arg("3.12"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtual environment at: .venv + Activate with: source .venv/[BIN]/activate + "### + ); + + // Try to create again with --no-clear (should fail) + uv_snapshot!(context.filters(), context.venv() + .arg(context.venv.as_os_str()) + .arg("--no-clear") + .arg("--python") + .arg("3.12"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtual environment at: .venv + error: Failed to create virtual environment + Caused by: A virtual environment already exists at: .venv + + hint: Use the `--clear` flag or set `UV_VENV_CLEAR=1` to replace the existing virtual environment + " + ); +} + +#[test] +fn no_clear_with_non_existent_directory() { + let context = TestContext::new_with_versions(&["3.12"]); + + // Create with --no-clear on non-existent directory (should succeed) + uv_snapshot!(context.filters(), context.venv() + .arg(context.venv.as_os_str()) + .arg("--no-clear") + .arg("--python") + .arg("3.12"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtual environment at: .venv + Activate with: source .venv/[BIN]/activate + "### + ); + + context.venv.assert(predicates::path::is_dir()); +} + +#[test] +fn no_clear_overrides_clear() { + let context = TestContext::new_with_versions(&["3.12"]); + + // Create a non-empty directory at `.venv` + context.venv.create_dir_all().unwrap(); + context.venv.child("file").touch().unwrap(); + + // --no-clear should override --clear and fail without prompting + uv_snapshot!(context.filters(), context.venv() + .arg(context.venv.as_os_str()) + .arg("--clear") + .arg("--no-clear") + .arg("--python") + .arg("3.12"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtual environment at: .venv + error: Failed to create virtual environment + Caused by: A directory already exists at: .venv + + hint: Use the `--clear` flag or set `UV_VENV_CLEAR=1` to replace the existing directory + " + ); +} + +#[test] +fn no_clear_conflicts_with_allow_existing() { + let context = TestContext::new_with_versions(&["3.12"]); + + // Try to use --no-clear with --allow-existing (should fail) + uv_snapshot!(context.filters(), context.venv() + .arg(context.venv.as_os_str()) + .arg("--no-clear") + .arg("--allow-existing") + .arg("--python") + .arg("3.12"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: the argument '--no-clear' cannot be used with '--allow-existing' + + Usage: uv venv --cache-dir [CACHE_DIR] --python --exclude-newer + + For more information, try '--help'. + " + ); +}