diff --git a/crates/uv-build-frontend/src/lib.rs b/crates/uv-build-frontend/src/lib.rs index c67c3482fb5ef..67bee9619155a 100644 --- a/crates/uv-build-frontend/src/lib.rs +++ b/crates/uv-build-frontend/src/lib.rs @@ -40,7 +40,6 @@ use uv_pypi_types::VerbatimParsedUrl; use uv_python::{Interpreter, PythonEnvironment}; use uv_static::EnvVars; use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, SourceBuildTrait}; -use uv_virtualenv::VenvCreationPolicy; use uv_warnings::warn_user_once; use uv_workspace::WorkspaceCache; @@ -332,7 +331,7 @@ impl SourceBuild { interpreter.clone(), uv_virtualenv::Prompt::None, false, - VenvCreationPolicy::RemoveDirectory, + uv_virtualenv::OnExisting::Remove, false, false, false, diff --git a/crates/uv-console/src/lib.rs b/crates/uv-console/src/lib.rs index 807b77aa43dc2..24c5eea163e40 100644 --- a/crates/uv-console/src/lib.rs +++ b/crates/uv-console/src/lib.rs @@ -6,6 +6,25 @@ use std::{cmp::Ordering, iter}; /// This is a slimmed-down version of `dialoguer::Confirm`, with the post-confirmation report /// enabled. pub fn confirm(message: &str, term: &Term, default: bool) -> std::io::Result { + confirm_inner(message, None, term, default) +} + +/// Prompt the user for confirmation in the given [`Term`], with a hint. +pub fn confirm_with_hint( + message: &str, + hint: &str, + term: &Term, + default: bool, +) -> std::io::Result { + confirm_inner(message, Some(hint), term, default) +} + +fn confirm_inner( + message: &str, + hint: Option<&str>, + term: &Term, + default: bool, +) -> std::io::Result { let prompt = format!( "{} {} {} {} {}", style("?".to_string()).for_stderr().yellow(), @@ -18,6 +37,13 @@ pub fn confirm(message: &str, term: &Term, default: bool) -> std::io::Result std::io::Result { - if metadata.is_file() { - return Err(Error::Io(io::Error::new( - io::ErrorKind::AlreadyExists, - format!("File exists at `{}`", location.user_display()), - ))); - } else if metadata.is_dir() { - match venv_creation_policy { - VenvCreationPolicy::OverwriteFiles => { - debug!("Allowing existing directory due to `--allow-existing`"); - } - VenvCreationPolicy::RemoveDirectory => { - debug!("Removing existing directory due to `--clear`"); - remove_venv_directory(location)?; - } - VenvCreationPolicy::FailIfNotEmpty => { - if location - .read_dir() - .is_ok_and(|mut dir| dir.next().is_none()) - { - debug!("Ignoring empty directory"); - } else { - let directory_message = if uv_fs::is_virtualenv_base(location) { + Ok(metadata) if metadata.is_file() => { + return Err(Error::Io(io::Error::new( + io::ErrorKind::AlreadyExists, + format!("File exists at `{}`", location.user_display()), + ))); + } + Ok(metadata) if metadata.is_dir() => { + let name = if uv_fs::is_virtualenv_base(location) { + "virtual environment" + } else { + "directory" + }; + match on_existing { + OnExisting::Allow => { + debug!("Allowing existing {name} due to `--allow-existing`"); + } + OnExisting::Remove => { + debug!("Removing existing {name} due to `--clear`"); + remove_venv_directory(location)?; + } + OnExisting::Fail + if location + .read_dir() + .is_ok_and(|mut dir| dir.next().is_none()) => + { + debug!("Ignoring empty directory"); + } + OnExisting::Fail => { + match confirm_clear(location, name)? { + Some(true) => { + debug!("Removing existing {name} due to confirmation"); + remove_venv_directory(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 virtual environment exists at `{}`.", - location.user_display() - ) - } else { - format!("The directory `{}` exists.", location.user_display()) - }; - - if Term::stderr().is_term() { - if confirm_clear(location)? { - debug!("Removing existing directory"); - remove_venv_directory(location)?; - } else { - return Err(Error::Io(io::Error::new( - io::ErrorKind::AlreadyExists, - format!( - "{directory_message} \n\n{}{} Use `{}` to remove the directory first", - "hint".bold().cyan(), - ":".bold(), - "--clear".green(), - ), - ))); - } - } else { - // If this is not a tty, warn for now. - warn_user_once!( - "{directory_message} In the future, uv will require `{}` to remove the directory first", - "--clear".green(), - ); - } + "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(), + "--clear".green(), + ); } } } } } + Ok(_) => { + // It's not a file or a directory + return Err(Error::Io(io::Error::new( + io::ErrorKind::AlreadyExists, + format!("Object already exists at `{}`", location.user_display()), + ))); + } Err(err) if err.kind() == io::ErrorKind::NotFound => { fs::create_dir_all(location)?; } @@ -484,16 +494,26 @@ pub(crate) fn create( }) } -fn confirm_clear(location: &Path) -> Result { +/// Prompt a confirmation that the virtual environment should be cleared. +/// +/// If not a TTY, returns `None`. +fn confirm_clear(location: &Path, name: &'static str) -> Result, io::Error> { let term = Term::stderr(); if term.is_term() { let prompt = format!( - "The directory `{}` exists. Did you mean to clear its contents (`--clear`)?", + "A {name} already exists at `{}`. Do you want to replace it?", location.user_display(), ); - uv_console::confirm(&prompt, &term, true) + let hint = format!( + "Use the `{}` flag or set `{}` to skip this prompt", + "--clear".green(), + "UV_VENV_CLEAR=1".green() + ); + Ok(Some(uv_console::confirm_with_hint( + &prompt, &hint, &term, true, + )?)) } else { - Ok(false) + Ok(None) } } @@ -516,24 +536,25 @@ fn remove_venv_directory(location: &Path) -> Result<(), Error> { } #[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] -pub enum VenvCreationPolicy { - /// Do not create a virtual environment if a non-empty directory exists. +pub enum OnExisting { + /// Fail if the directory already exists and is non-empty. #[default] - FailIfNotEmpty, - /// Overwrite existing virtual environment files. - OverwriteFiles, - /// Remove existing directory. - RemoveDirectory, + Fail, + /// Allow an existing directory, overwriting virtual environment files while retaining other + /// files in the directory. + Allow, + /// Remove an existing directory. + Remove, } -impl VenvCreationPolicy { +impl OnExisting { pub fn from_args(allow_existing: bool, clear: bool) -> Self { if allow_existing { - VenvCreationPolicy::OverwriteFiles + OnExisting::Allow } else if clear { - VenvCreationPolicy::RemoveDirectory + OnExisting::Remove } else { - VenvCreationPolicy::default() + OnExisting::default() } } } diff --git a/crates/uv/src/commands/project/environment.rs b/crates/uv/src/commands/project/environment.rs index 7a3129fafa2f7..86ae18d565748 100644 --- a/crates/uv/src/commands/project/environment.rs +++ b/crates/uv/src/commands/project/environment.rs @@ -2,14 +2,6 @@ use std::path::Path; use tracing::debug; -use uv_cache::{Cache, CacheBucket}; -use uv_cache_key::{cache_digest, hash_digest}; -use uv_configuration::{Concurrency, Constraints, PreviewMode}; -use uv_distribution_types::{Name, Resolution}; -use uv_fs::PythonExt; -use uv_python::{Interpreter, PythonEnvironment, canonicalize_executable}; -use uv_virtualenv::VenvCreationPolicy; - use crate::commands::pip::loggers::{InstallLogger, ResolveLogger}; use crate::commands::pip::operations::Modifications; use crate::commands::project::{ @@ -18,6 +10,13 @@ use crate::commands::project::{ use crate::printer::Printer; use crate::settings::{NetworkSettings, ResolverInstallerSettings}; +use uv_cache::{Cache, CacheBucket}; +use uv_cache_key::{cache_digest, hash_digest}; +use uv_configuration::{Concurrency, Constraints, PreviewMode}; +use uv_distribution_types::{Name, Resolution}; +use uv_fs::PythonExt; +use uv_python::{Interpreter, PythonEnvironment, canonicalize_executable}; + /// A [`PythonEnvironment`] stored in the cache. #[derive(Debug)] pub(crate) struct CachedEnvironment(PythonEnvironment); @@ -120,7 +119,7 @@ impl CachedEnvironment { base_interpreter, uv_virtualenv::Prompt::None, false, - VenvCreationPolicy::RemoveDirectory, + uv_virtualenv::OnExisting::Remove, true, false, false, diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 98c46ff385ef3..ba6454cf21a6e 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -43,7 +43,6 @@ use uv_scripts::Pep723ItemRef; use uv_settings::PythonInstallMirrors; use uv_static::EnvVars; use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy}; -use uv_virtualenv::VenvCreationPolicy; use uv_warnings::{warn_user, warn_user_once}; use uv_workspace::dependency_groups::DependencyGroupError; use uv_workspace::pyproject::PyProjectToml; @@ -1340,7 +1339,7 @@ impl ProjectEnvironment { interpreter, prompt, false, - VenvCreationPolicy::RemoveDirectory, + uv_virtualenv::OnExisting::Remove, false, false, upgradeable, @@ -1379,7 +1378,7 @@ impl ProjectEnvironment { interpreter, prompt, false, - VenvCreationPolicy::RemoveDirectory, + uv_virtualenv::OnExisting::Remove, false, false, upgradeable, @@ -1531,7 +1530,7 @@ impl ScriptEnvironment { interpreter, prompt, false, - VenvCreationPolicy::RemoveDirectory, + uv_virtualenv::OnExisting::Remove, false, false, upgradeable, @@ -1567,7 +1566,7 @@ impl ScriptEnvironment { interpreter, prompt, false, - VenvCreationPolicy::RemoveDirectory, + uv_virtualenv::OnExisting::Remove, false, false, upgradeable, diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 1c0579e7ec7f7..bd2745d2ce131 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -37,7 +37,6 @@ use uv_scripts::Pep723Item; use uv_settings::PythonInstallMirrors; use uv_shell::runnable::WindowsRunnable; use uv_static::EnvVars; -use uv_virtualenv::VenvCreationPolicy; use uv_warnings::warn_user; use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace, WorkspaceCache, WorkspaceError}; @@ -466,7 +465,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl interpreter, uv_virtualenv::Prompt::None, false, - VenvCreationPolicy::RemoveDirectory, + uv_virtualenv::OnExisting::Remove, false, false, false, @@ -671,7 +670,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl interpreter, uv_virtualenv::Prompt::None, false, - VenvCreationPolicy::RemoveDirectory, + uv_virtualenv::OnExisting::Remove, false, false, false, @@ -908,7 +907,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl interpreter, uv_virtualenv::Prompt::None, false, - VenvCreationPolicy::RemoveDirectory, + uv_virtualenv::OnExisting::Remove, false, false, false, diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index 35c24d4c104b6..92eb1ead74f3c 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -27,7 +27,7 @@ use uv_resolver::{ExcludeNewer, FlatIndex}; use uv_settings::PythonInstallMirrors; use uv_shell::{Shell, shlex_posix, shlex_windows}; use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, HashStrategy}; -use uv_virtualenv::VenvCreationPolicy; +use uv_virtualenv::OnExisting; use uv_warnings::warn_user; use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceCache, WorkspaceError}; @@ -74,7 +74,7 @@ pub(crate) async fn venv( prompt: uv_virtualenv::Prompt, system_site_packages: bool, seed: bool, - venv_creation_policy: VenvCreationPolicy, + on_existing: OnExisting, exclude_newer: Option, concurrency: Concurrency, no_config: bool, @@ -210,7 +210,7 @@ pub(crate) async fn venv( interpreter, prompt, system_site_packages, - venv_creation_policy, + on_existing, relocatable, seed, upgradeable, diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 87b5098c00b34..fc7e69eb01b47 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -41,7 +41,6 @@ use uv_requirements_txt::RequirementsTxtRequirement; use uv_scripts::{Pep723Error, Pep723Item, Pep723ItemRef, Pep723Metadata, Pep723Script}; use uv_settings::{Combine, FilesystemOptions, Options}; use uv_static::EnvVars; -use uv_virtualenv::VenvCreationPolicy; use uv_warnings::{warn_user, warn_user_once}; use uv_workspace::{DiscoveryOptions, Workspace, WorkspaceCache}; @@ -1030,8 +1029,7 @@ async fn run(mut cli: Cli) -> Result { let python_request: Option = args.settings.python.as_deref().map(PythonRequest::parse); - let venv_creation_policy = - VenvCreationPolicy::from_args(args.allow_existing, args.clear); + let on_existing = uv_virtualenv::OnExisting::from_args(args.allow_existing, args.clear); commands::venv( &project_dir, @@ -1049,7 +1047,7 @@ async fn run(mut cli: Cli) -> Result { uv_virtualenv::Prompt::from_args(prompt), args.system_site_packages, args.seed, - venv_creation_policy, + on_existing, args.settings.exclude_newer, globals.concurrency, cli.top_level.no_config, diff --git a/crates/uv/tests/it/venv.rs b/crates/uv/tests/it/venv.rs index 867a52aa0f1ff..2430e607d7abf 100644 --- a/crates/uv/tests/it/venv.rs +++ b/crates/uv/tests/it/venv.rs @@ -41,7 +41,7 @@ fn create_venv() { ----- stderr ----- Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] Creating virtual environment at: .venv - warning: A virtual environment exists at `.venv`. In the future, uv will require `--clear` to remove the directory first + warning: A virtual environment already exists at `.venv`. In the future, uv will require `--clear` to replace it Activate with: source .venv/[BIN]/activate " ); @@ -970,7 +970,7 @@ fn non_empty_dir_exists() -> Result<()> { ----- stderr ----- Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] Creating virtual environment at: .venv - warning: The directory `.venv` exists. In the future, uv will require `--clear` to remove the directory first + warning: A directory already exists at `.venv`. In the future, uv will require `--clear` to replace it Activate with: source .venv/[BIN]/activate " ); @@ -998,7 +998,7 @@ fn non_empty_dir_exists_allow_existing() -> Result<()> { ----- stderr ----- Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] Creating virtual environment at: .venv - warning: The directory `.venv` exists. In the future, uv will require `--clear` to remove the directory first + warning: A directory already exists at `.venv`. In the future, uv will require `--clear` to replace it Activate with: source .venv/[BIN]/activate " );