From 89e350558fc55ecdba35d45a1e30af07da57c53f Mon Sep 17 00:00:00 2001 From: Aditya-PS-05 Date: Sun, 24 Aug 2025 22:09:42 +0530 Subject: [PATCH 1/9] add no-clear option to uv venv --- crates/uv-cli/src/lib.rs | 8 ++++++++ crates/uv-virtualenv/src/virtualenv.rs | 24 ++++++++++++++++++++++-- crates/uv/src/lib.rs | 6 +++++- crates/uv/src/settings.rs | 3 +++ 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index c5c0bd3cbda55..43bb8a6795a1b 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -2689,6 +2689,14 @@ pub struct VenvArgs { #[clap(long, short, overrides_with = "allow_existing", value_parser = clap::builder::BoolishValueParser::new(), env = EnvVars::UV_VENV_CLEAR)] pub clear: bool, + /// Disable clearing and exit with error if target path is non-empty. + /// + /// By default, `uv venv` will prompt to clear a non-empty directory (when a TTY is available) + /// or exit with an error (when no TTY is available). The `--no-clear` option will force + /// an error exit without prompting, regardless of TTY availability. + #[clap(long, conflicts_with = "clear", conflicts_with = "allow_existing")] + 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 5b0c3cb34537f..e7459f0a3803b 100644 --- a/crates/uv-virtualenv/src/virtualenv.rs +++ b/crates/uv-virtualenv/src/virtualenv.rs @@ -157,6 +157,22 @@ pub(crate) fn create( } } } + OnExisting::FailNoPrompt => { + let hint = format!( + "Use the `{}` flag to clear the {name} or `{}` to allow overwriting", + "--clear".green(), + "--allow-existing".green() + ); + return Err(Error::Io(io::Error::new( + io::ErrorKind::AlreadyExists, + format!( + "A {name} already exists at {}\n\n{}{} {hint}", + location.user_display(), + "error".bold().red(), + ":".bold(), + ), + ))); + } } } Ok(_) => { @@ -633,11 +649,15 @@ pub enum OnExisting { Allow, /// Remove an existing directory. Remove, + /// Fail without prompting if the directory already exists and is non-empty. + FailNoPrompt, } impl OnExisting { - pub fn from_args(allow_existing: bool, clear: bool) -> Self { - if allow_existing { + pub fn from_args(allow_existing: bool, clear: bool, no_clear: bool) -> Self { + if no_clear { + Self::FailNoPrompt + } else if allow_existing { Self::Allow } else if clear { Self::Remove diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index ae2fc0e923118..8c75d3fe2ff9d 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1023,7 +1023,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 8cfe440135c18..951065325957d 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -2695,6 +2695,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, @@ -2714,6 +2715,7 @@ impl VenvSettings { seed, allow_existing, clear, + no_clear, path, prompt, system_site_packages, @@ -2733,6 +2735,7 @@ impl VenvSettings { seed, allow_existing, clear, + no_clear, path, prompt, system_site_packages, From 17558319c3ae5481d5abe4f29951665eaf2e7ea3 Mon Sep 17 00:00:00 2001 From: Aditya-PS-05 Date: Sun, 24 Aug 2025 22:32:24 +0530 Subject: [PATCH 2/9] add tests --- crates/uv-cli/src/lib.rs | 2 +- crates/uv-virtualenv/src/virtualenv.rs | 123 +++++++++++++------------ crates/uv/tests/it/venv.rs | 120 ++++++++++++++++++++++++ docs/reference/cli.md | 4 +- 4 files changed, 188 insertions(+), 61 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 43bb8a6795a1b..2aca51e2a87db 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -2694,7 +2694,7 @@ pub struct VenvArgs { /// By default, `uv venv` will prompt to clear a non-empty directory (when a TTY is available) /// or exit with an error (when no TTY is available). The `--no-clear` option will force /// an error exit without prompting, regardless of TTY availability. - #[clap(long, conflicts_with = "clear", conflicts_with = "allow_existing")] + #[clap(long, overrides_with = "clear", conflicts_with = "allow_existing")] pub no_clear: bool, /// Preserve any existing files or directories at the target path. diff --git a/crates/uv-virtualenv/src/virtualenv.rs b/crates/uv-virtualenv/src/virtualenv.rs index e7459f0a3803b..d2edf8c5c5bdf 100644 --- a/crates/uv-virtualenv/src/virtualenv.rs +++ b/crates/uv-virtualenv/src/virtualenv.rs @@ -118,61 +118,63 @@ pub(crate) fn create( remove_virtualenv(&location)?; fs::create_dir_all(&location)?; } - OnExisting::Fail => { - match confirm_clear(location, name)? { - Some(true) => { - debug!("Removing existing {name} due to confirmation"); - // Before removing the virtual environment, we need to canonicalize the - // path because `Path::metadata` will follow the symlink but we're still - // operating on the unresolved path and will remove the symlink itself. - let location = location - .canonicalize() - .unwrap_or_else(|_| location.to_path_buf()); - 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}", + OnExisting::Fail(allow_prompt) => { + if allow_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 + // path because `Path::metadata` will follow the symlink but we're still + // operating on the unresolved path and will remove the symlink itself. + let location = location + .canonicalize() + .unwrap_or_else(|_| location.to_path_buf()); + 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(), + ), + ))); + } + // When we don't have a TTY, warn that the behavior will change in the future + None => { + warn_user_once!( + "A {name} already exists at `{}`. In the future, uv will require `{}` to replace it", location.user_display(), - "hint".bold().cyan(), - ":".bold(), - ), - ))); + "--clear".green(), + ); + } } - // When we don't have a TTY, warn that the behavior will change in the future - None => { - warn_user_once!( - "A {name} already exists at `{}`. In the future, uv will require `{}` to replace it", + } else { + // --no-clear was specified, fail without prompting + let hint = format!( + "Use the `{}` flag to clear the {name} or `{}` to allow overwriting", + "--clear".green(), + "--allow-existing".green() + ); + return Err(Error::Io(io::Error::new( + io::ErrorKind::AlreadyExists, + format!( + "A {name} already exists at {}\n\n{}{} {hint}", location.user_display(), - "--clear".green(), - ); - } + "error".bold().red(), + ":".bold(), + ), + ))); } } - OnExisting::FailNoPrompt => { - let hint = format!( - "Use the `{}` flag to clear the {name} or `{}` to allow overwriting", - "--clear".green(), - "--allow-existing".green() - ); - return Err(Error::Io(io::Error::new( - io::ErrorKind::AlreadyExists, - format!( - "A {name} already exists at {}\n\n{}{} {hint}", - location.user_display(), - "error".bold().red(), - ":".bold(), - ), - ))); - } } } Ok(_) => { @@ -639,30 +641,33 @@ pub fn remove_virtualenv(location: &Path) -> Result<(), Error> { Ok(()) } -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum OnExisting { /// Fail if the directory already exists and is non-empty. - #[default] - Fail, + /// The bool parameter controls whether prompting is allowed. + Fail(bool), /// Allow an existing directory, overwriting virtual environment files while retaining other /// files in the directory. Allow, /// Remove an existing directory. Remove, - /// Fail without prompting if the directory already exists and is non-empty. - FailNoPrompt, +} + +impl Default for OnExisting { + fn default() -> Self { + Self::Fail(true) // Allow prompting by default + } } impl OnExisting { pub fn from_args(allow_existing: bool, clear: bool, no_clear: bool) -> Self { - if no_clear { - Self::FailNoPrompt - } else if allow_existing { + if allow_existing { Self::Allow } else if clear { Self::Remove } else { - Self::default() + // If no_clear is true, don't allow prompting + Self::Fail(!no_clear) } } } diff --git a/crates/uv/tests/it/venv.rs b/crates/uv/tests/it/venv.rs index b972996259d95..b5cef6e6b0ed8 100644 --- a/crates/uv/tests/it/venv.rs +++ b/crates/uv/tests/it/venv.rs @@ -1572,3 +1572,123 @@ fn create_venv_nested_symlink_preservation() -> Result<()> { Ok(()) } + +#[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 + + error: Use the `--clear` flag to clear the virtual environment or `--allow-existing` to allow overwriting + " + ); +} + +#[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 + + error: Use the `--clear` flag to clear the directory or `--allow-existing` to allow overwriting + " + ); +} + +#[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] --no-clear --python --exclude-newer + + For more information, try '--help'. + " + ); +} diff --git a/docs/reference/cli.md b/docs/reference/cli.md index a4c3d33bef086..b23948e84cb0f 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -4935,7 +4935,9 @@ uv venv [OPTIONS] [PATH]

By default, uv loads certificates from the bundled webpki-roots crate. The webpki-roots are a reliable set of trust roots from Mozilla, and including them in uv improves portability and performance (especially on macOS).

However, in some cases, you may want to use the platform's native certificate store, especially if you're relying on a corporate trust root (e.g., for a mandatory proxy) that's included in your system's certificate store.

May also be set with the UV_NATIVE_TLS environment variable.

--no-cache, --no-cache-dir, -n

Avoid reading from or writing to the cache, instead using a temporary directory for the duration of the operation

-

May also be set with the UV_NO_CACHE environment variable.

--no-config

Avoid discovering configuration files (pyproject.toml, uv.toml).

+

May also be set with the UV_NO_CACHE environment variable.

--no-clear

Disable clearing and exit with error if target path is non-empty.

+

By default, uv venv will prompt to clear a non-empty directory (when a TTY is available) or exit with an error (when no TTY is available). The --no-clear option will force an error exit without prompting, regardless of TTY availability.

+
--no-config

Avoid discovering configuration files (pyproject.toml, uv.toml).

Normally, configuration files are discovered in the current directory, parent directories, or user configuration directories.

May also be set with the UV_NO_CONFIG environment variable.

--no-index

Ignore the registry index (e.g., PyPI), instead relying on direct URL dependencies and those provided via --find-links

--no-managed-python

Disable use of uv-managed Python versions.

From d787b1b7e7eabfdcb9b7780edf54c96afc0bedcf Mon Sep 17 00:00:00 2001 From: Harshith VH Date: Thu, 11 Sep 2025 22:00:19 +0530 Subject: [PATCH 3/9] code refactor --- crates/uv-virtualenv/src/virtualenv.rs | 84 +++++++++++++------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/crates/uv-virtualenv/src/virtualenv.rs b/crates/uv-virtualenv/src/virtualenv.rs index d2edf8c5c5bdf..4e9221f934875 100644 --- a/crates/uv-virtualenv/src/virtualenv.rs +++ b/crates/uv-virtualenv/src/virtualenv.rs @@ -119,60 +119,60 @@ pub(crate) fn create( fs::create_dir_all(&location)?; } OnExisting::Fail(allow_prompt) => { - if allow_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 - // path because `Path::metadata` will follow the symlink but we're still - // operating on the unresolved path and will remove the symlink itself. - let location = location - .canonicalize() - .unwrap_or_else(|_| location.to_path_buf()); - remove_virtualenv(&location)?; - fs::create_dir_all(&location)?; - } - Some(false) => { + match allow_prompt.then(|| confirm_clear(location, name)).transpose()?.flatten() { + Some(true) => { + debug!("Removing existing {name} due to confirmation"); + // Before removing the virtual environment, we need to canonicalize the + // path because `Path::metadata` will follow the symlink but we're still + // operating on the unresolved path and will remove the symlink itself. + let location = location + .canonicalize() + .unwrap_or_else(|_| location.to_path_buf()); + 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(), + ), + ))); + } + // When we don't have a TTY, warn that the behavior will change in the future + // Or when --no-clear was specified, fail without prompting + None => { + if allow_prompt { + warn_user_once!( + "A {name} already exists at `{}`. In the future, uv will require `{}` to replace it", + location.user_display(), + "--clear".green(), + ); + } else { let hint = format!( - "Use the `{}` flag or set `{}` to replace the existing {name}", + "Use the `{}` flag to clear the {name} or `{}` to allow overwriting", "--clear".green(), - "UV_VENV_CLEAR=1".green() + "--allow-existing".green() ); return Err(Error::Io(io::Error::new( io::ErrorKind::AlreadyExists, format!( - "A {name} already exists at: {}\n\n{}{} {hint}", + "A {name} already exists at {}\n\n{}{} {hint}", location.user_display(), - "hint".bold().cyan(), + "error".bold().red(), ":".bold(), ), ))); } - // When we don't have a TTY, warn that the behavior will change in the future - None => { - warn_user_once!( - "A {name} already exists at `{}`. In the future, uv will require `{}` to replace it", - location.user_display(), - "--clear".green(), - ); - } } - } else { - // --no-clear was specified, fail without prompting - let hint = format!( - "Use the `{}` flag to clear the {name} or `{}` to allow overwriting", - "--clear".green(), - "--allow-existing".green() - ); - return Err(Error::Io(io::Error::new( - io::ErrorKind::AlreadyExists, - format!( - "A {name} already exists at {}\n\n{}{} {hint}", - location.user_display(), - "error".bold().red(), - ":".bold(), - ), - ))); } } } From fa53ccaab0090553f4da404515eef39d26d2c74f Mon Sep 17 00:00:00 2001 From: Harshith VH Date: Fri, 12 Sep 2025 00:13:52 +0530 Subject: [PATCH 4/9] fix formatting and tests --- crates/uv-virtualenv/src/virtualenv.rs | 6 +++++- crates/uv/tests/it/venv.rs | 25 ++++++++++--------------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/crates/uv-virtualenv/src/virtualenv.rs b/crates/uv-virtualenv/src/virtualenv.rs index 0179065b2d889..4d240b27e94b2 100644 --- a/crates/uv-virtualenv/src/virtualenv.rs +++ b/crates/uv-virtualenv/src/virtualenv.rs @@ -132,7 +132,11 @@ pub(crate) fn create( fs::create_dir_all(&location)?; } OnExisting::Fail(allow_prompt) => { - match allow_prompt.then(|| confirm_clear(location, name)).transpose()?.flatten() { + match allow_prompt + .then(|| confirm_clear(location, name)) + .transpose()? + .flatten() + { Some(true) => { debug!("Removing existing {name} due to confirmation"); // Before removing the virtual environment, we need to canonicalize the diff --git a/crates/uv/tests/it/venv.rs b/crates/uv/tests/it/venv.rs index a38feffdc970f..c100369890c23 100644 --- a/crates/uv/tests/it/venv.rs +++ b/crates/uv/tests/it/venv.rs @@ -958,8 +958,7 @@ fn empty_dir_exists() -> Result<()> { fn non_empty_dir_exists() -> Result<()> { let context = TestContext::new_with_versions(&["3.12"]); - // Create a non-empty directory at `.venv`. Creating a virtualenv at the same path should fail, - // unless `--clear` is specified. + // Create a non-empty directory at `.venv`. Creating a virtualenv at the same path should succeed with a warning. context.venv.create_dir_all()?; context.venv.child("file").touch()?; @@ -967,17 +966,15 @@ fn non_empty_dir_exists() -> Result<()> { .arg(context.venv.as_os_str()) .arg("--python") .arg("3.12"), @r" - success: false - exit_code: 2 + success: true + exit_code: 0 ----- 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 + warning: A directory already exists at `.venv`. In the future, uv will require `--clear` to replace it + Activate with: source .venv/[BIN]/activate " ); @@ -1005,7 +1002,7 @@ fn non_empty_dir_exists_allow_existing() -> Result<()> { let context = TestContext::new_with_versions(&["3.12"]); // Create a non-empty directory at `.venv`. Creating a virtualenv at the same path should - // succeed when `--allow-existing` is specified, but fail when it is not. + // succeed with a warning, both with and without `--allow-existing`. context.venv.create_dir_all()?; context.venv.child("file").touch()?; @@ -1013,17 +1010,15 @@ fn non_empty_dir_exists_allow_existing() -> Result<()> { .arg(context.venv.as_os_str()) .arg("--python") .arg("3.12"), @r" - success: false - exit_code: 2 + success: true + exit_code: 0 ----- 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 + warning: A directory already exists at `.venv`. In the future, uv will require `--clear` to replace it + Activate with: source .venv/[BIN]/activate " ); From 4afaf03e0a2405336c296d98e8a1bcd4b9d9d733 Mon Sep 17 00:00:00 2001 From: Harshith VH Date: Fri, 12 Sep 2025 17:45:32 +0530 Subject: [PATCH 5/9] Restore venv.rs to correct version --- crates/uv/tests/it/venv.rs | 91 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 87 insertions(+), 4 deletions(-) diff --git a/crates/uv/tests/it/venv.rs b/crates/uv/tests/it/venv.rs index c100369890c23..25e00d8a6014d 100644 --- a/crates/uv/tests/it/venv.rs +++ b/crates/uv/tests/it/venv.rs @@ -958,7 +958,8 @@ fn empty_dir_exists() -> Result<()> { fn non_empty_dir_exists() -> Result<()> { let context = TestContext::new_with_versions(&["3.12"]); - // Create a non-empty directory at `.venv`. Creating a virtualenv at the same path should succeed with a warning. + // Create a non-empty directory at `.venv`. Creating a virtualenv at the same path should fail, + // unless `--clear` is specified. context.venv.create_dir_all()?; context.venv.child("file").touch()?; @@ -1002,7 +1003,7 @@ fn non_empty_dir_exists_allow_existing() -> Result<()> { let context = TestContext::new_with_versions(&["3.12"]); // Create a non-empty directory at `.venv`. Creating a virtualenv at the same path should - // succeed with a warning, both with and without `--allow-existing`. + // succeed when `--allow-existing` is specified, but fail when it is not. context.venv.create_dir_all()?; context.venv.child("file").touch()?; @@ -1589,6 +1590,88 @@ fn create_venv_nested_symlink_preservation() -> Result<()> { Ok(()) } +/// On Unix, creating a virtual environment in the current working directory should work. +#[test] +#[cfg(unix)] +fn create_venv_current_working_directory() { + let context = TestContext::new_with_versions(&["3.12"]); + + 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 + " + ); + + uv_snapshot!(context.filters(), context.venv() + .arg(".") + .arg("--clear") + .arg("--python") + .arg("3.12") + .current_dir(&context.venv), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtual environment at: . + Activate with: source bin/activate + " + ); + + context.root.assert(predicates::path::is_dir()); +} + +/// On Windows, creating a virtual environment in the current working directory should fail, +/// as you can't delete the current working directory. +#[test] +#[cfg(windows)] +fn create_venv_current_working_directory() { + let context = TestContext::new_with_versions(&["3.12"]); + + 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 + " + ); + + uv_snapshot!(context.filters(), context.venv() + .arg(".") + .arg("--clear") + .arg("--python") + .arg("3.12") + .current_dir(&context.venv), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtual environment at: . + error: Failed to create virtual environment + Caused by: failed to remove directory `[VENV]/`: The process cannot access the file because it is being used by another process. (os error 32) + " + ); +} + #[test] fn no_clear_with_existing_directory() { let context = TestContext::new_with_versions(&["3.12"]); @@ -1703,8 +1786,8 @@ fn no_clear_conflicts_with_allow_existing() { error: the argument '--no-clear' cannot be used with '--allow-existing' Usage: uv venv --cache-dir [CACHE_DIR] --no-clear --python --exclude-newer - + For more information, try '--help'. " ); -} +} \ No newline at end of file From d5efd8d41851a04e96124b48a3c49500fe2d1168 Mon Sep 17 00:00:00 2001 From: Harshith VH Date: Fri, 12 Sep 2025 17:58:25 +0530 Subject: [PATCH 6/9] fix formatting --- crates/uv/tests/it/venv.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/uv/tests/it/venv.rs b/crates/uv/tests/it/venv.rs index 25e00d8a6014d..4028a346d0aca 100644 --- a/crates/uv/tests/it/venv.rs +++ b/crates/uv/tests/it/venv.rs @@ -1790,4 +1790,4 @@ fn no_clear_conflicts_with_allow_existing() { For more information, try '--help'. " ); -} \ No newline at end of file +} From 0dd52bcaf67d0b7283c004da64663091c5fe9173 Mon Sep 17 00:00:00 2001 From: Harshith VH Date: Sat, 13 Sep 2025 02:06:46 +0530 Subject: [PATCH 7/9] fixed missing is_virtualenv logic --- crates/uv-virtualenv/src/virtualenv.rs | 46 ++++++++++---------------- crates/uv/tests/it/venv.rs | 31 +++++++++-------- 2 files changed, 35 insertions(+), 42 deletions(-) diff --git a/crates/uv-virtualenv/src/virtualenv.rs b/crates/uv-virtualenv/src/virtualenv.rs index 4d240b27e94b2..8475a68ea0658 100644 --- a/crates/uv-virtualenv/src/virtualenv.rs +++ b/crates/uv-virtualenv/src/virtualenv.rs @@ -132,11 +132,19 @@ pub(crate) fn create( fs::create_dir_all(&location)?; } OnExisting::Fail(allow_prompt) => { - match allow_prompt - .then(|| confirm_clear(location, name)) - .transpose()? - .flatten() - { + let confirmation = if is_virtualenv { + if allow_prompt { + confirm_clear(location, name)? + } else { + // When --no-clear is specified, don't prompt, just fail + Some(false) + } + } else { + // Refuse to remove a non-virtual environment; don't even prompt. + Some(false) + }; + + match confirmation { Some(true) => { debug!("Removing existing {name} due to confirmation"); // Before removing the virtual environment, we need to canonicalize the @@ -165,30 +173,12 @@ pub(crate) fn create( ))); } // When we don't have a TTY, warn that the behavior will change in the future - // Or when --no-clear was specified, fail without prompting None => { - if allow_prompt { - warn_user_once!( - "A {name} already exists at `{}`. In the future, uv will require `{}` to replace it", - location.user_display(), - "--clear".green(), - ); - } else { - let hint = format!( - "Use the `{}` flag to clear the {name} or `{}` to allow overwriting", - "--clear".green(), - "--allow-existing".green() - ); - return Err(Error::Io(io::Error::new( - io::ErrorKind::AlreadyExists, - format!( - "A {name} already exists at {}\n\n{}{} {hint}", - location.user_display(), - "error".bold().red(), - ":".bold(), - ), - ))); - } + warn_user_once!( + "A {name} already exists at `{}`. In the future, uv will require `{}` to replace it", + location.user_display(), + "--clear".green(), + ); } } } diff --git a/crates/uv/tests/it/venv.rs b/crates/uv/tests/it/venv.rs index 4028a346d0aca..9e72d3f0e4c70 100644 --- a/crates/uv/tests/it/venv.rs +++ b/crates/uv/tests/it/venv.rs @@ -967,17 +967,18 @@ fn non_empty_dir_exists() -> Result<()> { .arg(context.venv.as_os_str()) .arg("--python") .arg("3.12"), @r" - success: true - exit_code: 0 + success: false + exit_code: 2 ----- stdout ----- ----- stderr ----- Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] Creating virtual environment at: .venv - warning: A directory already exists at `.venv`. In the future, uv will require `--clear` to replace it - Activate with: source .venv/[BIN]/activate - " - ); + 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 + "); uv_snapshot!(context.filters(), context.venv() .arg(context.venv.as_os_str()) @@ -1011,15 +1012,17 @@ fn non_empty_dir_exists_allow_existing() -> Result<()> { .arg(context.venv.as_os_str()) .arg("--python") .arg("3.12"), @r" - success: true - exit_code: 0 + success: false + exit_code: 2 ----- stdout ----- ----- stderr ----- Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] Creating virtual environment at: .venv - warning: A directory already exists at `.venv`. In the future, uv will require `--clear` to replace it - Activate with: source .venv/[BIN]/activate + 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 " ); @@ -1706,9 +1709,9 @@ fn no_clear_with_existing_directory() { 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 + Caused by: A virtual environment already exists at: .venv - error: Use the `--clear` flag to clear the virtual environment or `--allow-existing` to allow overwriting + hint: Use the `--clear` flag or set `UV_VENV_CLEAR=1` to replace the existing virtual environment " ); } @@ -1760,9 +1763,9 @@ fn no_clear_overrides_clear() { 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 + Caused by: A directory already exists at: .venv - error: Use the `--clear` flag to clear the directory or `--allow-existing` to allow overwriting + hint: Use the `--clear` flag or set `UV_VENV_CLEAR=1` to replace the existing directory " ); } From 29ca6808f5452601af553b0af3dec22faaa2d141 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Mon, 15 Sep 2025 16:02:36 -0500 Subject: [PATCH 8/9] Review --- crates/uv-cli/src/lib.rs | 14 +++-- crates/uv-virtualenv/src/virtualenv.rs | 81 +++++++++++++------------- crates/uv/tests/it/venv.rs | 4 +- docs/reference/cli.md | 4 +- 4 files changed, 52 insertions(+), 51 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 50d60f6b9dd80..283b99ac3a137 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -2754,12 +2754,16 @@ pub struct VenvArgs { #[clap(long, short, overrides_with = "allow_existing", value_parser = clap::builder::BoolishValueParser::new(), env = EnvVars::UV_VENV_CLEAR)] pub clear: bool, - /// Disable clearing and exit with error if target path is non-empty. + /// Fail without prompting if any existing files or directories are present at the target path. /// - /// By default, `uv venv` will prompt to clear a non-empty directory (when a TTY is available) - /// or exit with an error (when no TTY is available). The `--no-clear` option will force - /// an error exit without prompting, regardless of TTY availability. - #[clap(long, overrides_with = "clear", conflicts_with = "allow_existing")] + /// 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. diff --git a/crates/uv-virtualenv/src/virtualenv.rs b/crates/uv-virtualenv/src/virtualenv.rs index 8475a68ea0658..0c7e359dae1c9 100644 --- a/crates/uv-virtualenv/src/virtualenv.rs +++ b/crates/uv-virtualenv/src/virtualenv.rs @@ -116,6 +116,20 @@ 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() + ); + 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,20 +145,21 @@ pub(crate) fn create( remove_virtualenv(&location)?; fs::create_dir_all(&location)?; } - OnExisting::Fail(allow_prompt) => { - let confirmation = if is_virtualenv { - if allow_prompt { - confirm_clear(location, name)? - } else { - // When --no-clear is specified, don't prompt, just fail - Some(false) - } - } else { - // Refuse to remove a non-virtual environment; don't even prompt. - Some(false) - }; - - match confirmation { + OnExisting::Fail => { + 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(), + ), + ))); + } + // 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 @@ -156,22 +171,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!( @@ -642,11 +642,15 @@ pub fn remove_virtualenv(location: &Path) -> Result<(), Error> { Ok(()) } -#[derive(Debug, Copy, Clone, Eq, PartialEq)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)] pub enum OnExisting { + /// 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. - /// The bool parameter controls whether prompting is allowed. - Fail(bool), + Fail, /// Allow an existing directory, overwriting virtual environment files while retaining other /// files in the directory. Allow, @@ -654,21 +658,16 @@ pub enum OnExisting { Remove, } -impl Default for OnExisting { - fn default() -> Self { - Self::Fail(true) // Allow prompting by default - } -} - impl OnExisting { 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 { - // If no_clear is true, don't allow prompting - Self::Fail(!no_clear) + Self::Prompt } } } diff --git a/crates/uv/tests/it/venv.rs b/crates/uv/tests/it/venv.rs index 9e72d3f0e4c70..7f2e0f6ebbd57 100644 --- a/crates/uv/tests/it/venv.rs +++ b/crates/uv/tests/it/venv.rs @@ -1788,8 +1788,8 @@ fn no_clear_conflicts_with_allow_existing() { ----- stderr ----- error: the argument '--no-clear' cannot be used with '--allow-existing' - Usage: uv venv --cache-dir [CACHE_DIR] --no-clear --python --exclude-newer - + Usage: uv venv --cache-dir [CACHE_DIR] --python --exclude-newer + For more information, try '--help'. " ); diff --git a/docs/reference/cli.md b/docs/reference/cli.md index a1a96792481eb..a63d2222e062a 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -5554,9 +5554,7 @@ uv venv [OPTIONS] [PATH]

By default, uv loads certificates from the bundled webpki-roots crate. The webpki-roots are a reliable set of trust roots from Mozilla, and including them in uv improves portability and performance (especially on macOS).

However, in some cases, you may want to use the platform's native certificate store, especially if you're relying on a corporate trust root (e.g., for a mandatory proxy) that's included in your system's certificate store.

May also be set with the UV_NATIVE_TLS environment variable.

--no-cache, --no-cache-dir, -n

Avoid reading from or writing to the cache, instead using a temporary directory for the duration of the operation

-

May also be set with the UV_NO_CACHE environment variable.

--no-clear

Disable clearing and exit with error if target path is non-empty.

-

By default, uv venv will prompt to clear a non-empty directory (when a TTY is available) or exit with an error (when no TTY is available). The --no-clear option will force an error exit without prompting, regardless of TTY availability.

-
--no-config

Avoid discovering configuration files (pyproject.toml, uv.toml).

+

May also be set with the UV_NO_CACHE environment variable.

--no-config

Avoid discovering configuration files (pyproject.toml, uv.toml).

Normally, configuration files are discovered in the current directory, parent directories, or user configuration directories.

May also be set with the UV_NO_CONFIG environment variable.

--no-index

Ignore the registry index (e.g., PyPI), instead relying on direct URL dependencies and those provided via --find-links

--no-managed-python

Disable use of uv-managed Python versions.

From af15a55e6670d05f7e5395bf9b46eabcd99e5e4d Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Tue, 16 Sep 2025 08:02:28 -0500 Subject: [PATCH 9/9] Re-use the `err` --- crates/uv-virtualenv/src/virtualenv.rs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/crates/uv-virtualenv/src/virtualenv.rs b/crates/uv-virtualenv/src/virtualenv.rs index 0c7e359dae1c9..63122d1fe77f4 100644 --- a/crates/uv-virtualenv/src/virtualenv.rs +++ b/crates/uv-virtualenv/src/virtualenv.rs @@ -121,6 +121,8 @@ pub(crate) fn create( "--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!( @@ -145,17 +147,7 @@ pub(crate) fn create( remove_virtualenv(&location)?; fs::create_dir_all(&location)?; } - OnExisting::Fail => { - 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(), - ), - ))); - } + OnExisting::Fail => return err, // If not a virtual environment, fail without prompting. OnExisting::Prompt if !is_virtualenv => return err, OnExisting::Prompt => {