diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0ccc9ea4ec6d1..c01b5df88301a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1042,7 +1042,7 @@ jobs: - name: "Create a virtual environment (uv)" run: | - ./uv venv -p 3.13t --managed-python + ./uv venv -c -p 3.13t --managed-python - name: "Check version (uv)" run: | @@ -1087,7 +1087,7 @@ jobs: - name: "Create a virtual environment (uv)" run: | - ./uv venv -p 3.13 --managed-python + ./uv venv -c -p 3.13 --managed-python - name: "Check version (uv)" run: | @@ -1132,7 +1132,7 @@ jobs: - name: "Create a virtual environment (uv)" run: | - ./uv venv -p 3.13 --managed-python + ./uv venv -c -p 3.13 --managed-python - name: "Check version (uv)" run: | @@ -1758,14 +1758,14 @@ jobs: ./uv run --no-project python -c "from built_by_uv import greet; print(greet())" # Test both `build_wheel` and `build_sdist` through uv - ./uv venv -v + ./uv venv -c -v ./uv build -v --force-pep517 scripts/packages/built-by-uv --find-links crates/uv-build/dist --offline ./uv pip install -v scripts/packages/built-by-uv/dist/*.tar.gz --find-links crates/uv-build/dist --offline --no-deps ./uv run --no-project python -c "from built_by_uv import greet; print(greet())" # Test both `build_wheel` and `build_sdist` through the official `build` rm -rf scripts/packages/built-by-uv/dist/ - ./uv venv -v + ./uv venv -c -v ./uv pip install build # Add the uv binary to PATH for `build` to find PATH="$(pwd):$PATH" UV_OFFLINE=1 UV_FIND_LINKS=crates/uv-build/dist ./uv run --no-project python -m build -v --installer uv scripts/packages/built-by-uv diff --git a/Cargo.lock b/Cargo.lock index e0979cb0a3966..9d1b12591d957 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5996,18 +5996,22 @@ version = "0.7.20" name = "uv-virtualenv" version = "0.0.4" dependencies = [ + "console 0.15.11", "fs-err 3.1.1", "itertools 0.14.0", + "owo-colors", "pathdiff", "self-replace", "thiserror 2.0.12", "tracing", "uv-configuration", + "uv-console", "uv-fs", "uv-pypi-types", "uv-python", "uv-shell", "uv-version", + "uv-warnings", ] [[package]] diff --git a/crates/uv-build-frontend/src/lib.rs b/crates/uv-build-frontend/src/lib.rs index 5cbaece2ecc2d..67bee9619155a 100644 --- a/crates/uv-build-frontend/src/lib.rs +++ b/crates/uv-build-frontend/src/lib.rs @@ -331,7 +331,7 @@ impl SourceBuild { interpreter.clone(), uv_virtualenv::Prompt::None, false, - false, + uv_virtualenv::OnExisting::Remove, false, false, false, diff --git a/crates/uv-cli/src/compat.rs b/crates/uv-cli/src/compat.rs index d29afa760a283..344d1a4e7ee54 100644 --- a/crates/uv-cli/src/compat.rs +++ b/crates/uv-cli/src/compat.rs @@ -266,9 +266,6 @@ enum Resolver { /// These represent a subset of the `virtualenv` interface that uv supports by default. #[derive(Args)] pub struct VenvCompatArgs { - #[clap(long, hide = true)] - clear: bool, - #[clap(long, hide = true)] no_seed: bool, @@ -289,12 +286,6 @@ impl CompatArgs for VenvCompatArgs { /// behavior. If an argument is passed that does _not_ match uv's behavior, this method will /// return an error. fn validate(&self) -> Result<()> { - if self.clear { - warn_user!( - "virtualenv's `--clear` has no effect (uv always clears the virtual environment)" - ); - } - if self.no_seed { warn_user!( "virtualenv's `--no-seed` has no effect (uv omits seed packages by default)" diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index d209bde7a90fa..a8774c15cd14d 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -2615,16 +2615,23 @@ pub struct VenvArgs { #[arg(long, value_parser = clap::builder::BoolishValueParser::new(), env = EnvVars::UV_VENV_SEED)] pub seed: bool, + /// Remove 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 + /// `--clear` option will instead clear a non-empty path before creating a new virtual + /// environment. + #[clap(long, short, overrides_with = "allow_existing", value_parser = clap::builder::BoolishValueParser::new(), env = EnvVars::UV_VENV_CLEAR)] + pub clear: bool, + /// Preserve any existing files or directories at the target path. /// - /// By default, `uv venv` will remove an existing virtual environment at the given path, and - /// exit with an error if the path is non-empty but _not_ a virtual environment. The + /// By default, `uv venv` will exit with an error if the given path is non-empty. The /// `--allow-existing` option will instead write to the given path, regardless of its contents, /// and without clearing it beforehand. /// /// WARNING: This option can lead to unexpected behavior if the existing virtual environment and /// the newly-created virtual environment are linked to different Python interpreters. - #[clap(long)] + #[clap(long, overrides_with = "clear")] pub allow_existing: bool, /// The path to the virtual environment to create. 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() { - if allow_existing { - debug!("Allowing existing directory"); - } else if uv_fs::is_virtualenv_base(location) { - debug!("Removing existing directory"); - - // On Windows, if the current executable is in the directory, guard against - // self-deletion. - #[cfg(windows)] - if let Ok(itself) = std::env::current_exe() { - let target = std::path::absolute(location)?; - if itself.starts_with(&target) { - debug!("Detected self-delete of executable: {}", itself.display()); - self_replace::self_delete_outside_path(location)?; - } - } - - fs::remove_dir_all(location)?; - fs::create_dir_all(location)?; - } else if location - .read_dir() - .is_ok_and(|mut dir| dir.next().is_none()) + 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"); - } else { - return Err(Error::Io(io::Error::new( - io::ErrorKind::AlreadyExists, - format!( - "The directory `{}` exists, but it's not a virtual environment", - location.user_display() - ), - ))); + } + 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 {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)?; } @@ -464,6 +494,71 @@ pub(crate) fn create( }) } +/// 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!( + "A {name} already exists at `{}`. Do you want to replace it?", + location.user_display(), + ); + 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(None) + } +} + +fn remove_venv_directory(location: &Path) -> Result<(), Error> { + // On Windows, if the current executable is in the directory, guard against + // self-deletion. + #[cfg(windows)] + if let Ok(itself) = std::env::current_exe() { + let target = std::path::absolute(location)?; + if itself.starts_with(&target) { + debug!("Detected self-delete of executable: {}", itself.display()); + self_replace::self_delete_outside_path(location)?; + } + } + + fs::remove_dir_all(location)?; + fs::create_dir_all(location)?; + + Ok(()) +} + +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] +pub enum OnExisting { + /// Fail if the directory already exists and is non-empty. + #[default] + Fail, + /// Allow an existing directory, overwriting virtual environment files while retaining other + /// files in the directory. + Allow, + /// Remove an existing directory. + Remove, +} + +impl OnExisting { + pub fn from_args(allow_existing: bool, clear: bool) -> Self { + if allow_existing { + OnExisting::Allow + } else if clear { + OnExisting::Remove + } else { + OnExisting::default() + } + } +} + #[derive(Debug, Copy, Clone)] enum WindowsExecutable { /// The `python.exe` executable (or `venvlauncher.exe` launcher shim). diff --git a/crates/uv/src/commands/project/environment.rs b/crates/uv/src/commands/project/environment.rs index cf1add99a97ca..4f9d936c52f79 100644 --- a/crates/uv/src/commands/project/environment.rs +++ b/crates/uv/src/commands/project/environment.rs @@ -2,13 +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 crate::commands::pip::loggers::{InstallLogger, ResolveLogger}; use crate::commands::pip::operations::Modifications; use crate::commands::project::{ @@ -17,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}; + /// An ephemeral [`PythonEnvironment`] for running an individual command. #[derive(Debug)] pub(crate) struct EphemeralEnvironment(PythonEnvironment); @@ -171,7 +171,7 @@ impl CachedEnvironment { interpreter, uv_virtualenv::Prompt::None, false, - false, + 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 fde2b638cbb47..23655c1ca212d 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -1336,7 +1336,7 @@ impl ProjectEnvironment { interpreter, prompt, false, - false, + uv_virtualenv::OnExisting::Remove, false, false, upgradeable, @@ -1375,7 +1375,7 @@ impl ProjectEnvironment { interpreter, prompt, false, - false, + uv_virtualenv::OnExisting::Remove, false, false, upgradeable, @@ -1527,7 +1527,7 @@ impl ScriptEnvironment { interpreter, prompt, false, - false, + uv_virtualenv::OnExisting::Remove, false, false, upgradeable, @@ -1563,7 +1563,7 @@ impl ScriptEnvironment { interpreter, prompt, false, - false, + 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 16ebf88fb3b6d..ba8935013fac7 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -465,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, - false, + uv_virtualenv::OnExisting::Remove, false, false, false, @@ -670,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, - false, + uv_virtualenv::OnExisting::Remove, false, false, false, @@ -907,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, - false, + uv_virtualenv::OnExisting::Remove, false, false, false, @@ -1038,7 +1038,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl base_interpreter.clone(), uv_virtualenv::Prompt::None, false, - false, + uv_virtualenv::OnExisting::Remove, false, false, false, diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index 02bc818f8e06e..92eb1ead74f3c 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -27,6 +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::OnExisting; use uv_warnings::warn_user; use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceCache, WorkspaceError}; @@ -73,7 +74,7 @@ pub(crate) async fn venv( prompt: uv_virtualenv::Prompt, system_site_packages: bool, seed: bool, - allow_existing: bool, + on_existing: OnExisting, exclude_newer: Option, concurrency: Concurrency, no_config: bool, @@ -209,7 +210,7 @@ pub(crate) async fn venv( interpreter, prompt, system_site_packages, - allow_existing, + on_existing, relocatable, seed, upgradeable, diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 63f4f3203ac2e..d9d8129ee1415 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1029,6 +1029,8 @@ 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); + commands::venv( &project_dir, args.path, @@ -1045,7 +1047,7 @@ async fn run(mut cli: Cli) -> Result { uv_virtualenv::Prompt::from_args(prompt), args.system_site_packages, args.seed, - args.allow_existing, + on_existing, args.settings.exclude_newer, globals.concurrency, cli.top_level.no_config, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 40d2307328566..ff3faa460bfc6 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -2604,6 +2604,7 @@ impl BuildSettings { pub(crate) struct VenvSettings { pub(crate) seed: bool, pub(crate) allow_existing: bool, + pub(crate) clear: bool, pub(crate) path: Option, pub(crate) prompt: Option, pub(crate) system_site_packages: bool, @@ -2622,6 +2623,7 @@ impl VenvSettings { no_system, seed, allow_existing, + clear, path, prompt, system_site_packages, @@ -2639,6 +2641,7 @@ impl VenvSettings { Self { seed, allow_existing, + clear, path, prompt, system_site_packages, diff --git a/crates/uv/tests/it/cache_prune.rs b/crates/uv/tests/it/cache_prune.rs index a6ec48bd411da..99493fe210200 100644 --- a/crates/uv/tests/it/cache_prune.rs +++ b/crates/uv/tests/it/cache_prune.rs @@ -227,7 +227,7 @@ fn prune_unzipped() -> Result<()> { Removed [N] files ([SIZE]) "###); - context.venv().assert().success(); + context.venv().arg("--clear").assert().success(); // Reinstalling the source distribution should not require re-downloading the source // distribution. diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index c0a6e915e5def..588e232f791dd 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -1406,6 +1406,7 @@ pub fn create_venv_from_executable>(path: P, cache_dir: &ChildPat assert_cmd::Command::new(get_bin()) .arg("venv") .arg(path.as_ref().as_os_str()) + .arg("--clear") .arg("--cache-dir") .arg(cache_dir.path()) .arg("--python") diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index bc27228c75668..d0bab599e623a 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -2835,7 +2835,7 @@ fn install_no_binary_cache() { ); // Re-create the virtual environment. - context.venv().assert().success(); + context.venv().arg("--clear").assert().success(); // Re-install. The distribution should be installed from the cache. uv_snapshot!( @@ -2853,7 +2853,7 @@ fn install_no_binary_cache() { ); // Re-create the virtual environment. - context.venv().assert().success(); + context.venv().arg("--clear").assert().success(); // Install with `--no-binary`. The distribution should be built from source, despite a binary // distribution being available in the cache. @@ -3064,7 +3064,7 @@ fn cache_priority() { ); // Re-create the virtual environment. - context.venv().assert().success(); + context.venv().arg("--clear").assert().success(); // Install `idna` without a version specifier. uv_snapshot!( @@ -8228,6 +8228,7 @@ fn install_relocatable() -> Result<()> { context .venv() .arg(context.venv.as_os_str()) + .arg("--clear") .arg("--python") .arg("3.12") .arg("--relocatable") diff --git a/crates/uv/tests/it/pip_sync.rs b/crates/uv/tests/it/pip_sync.rs index 43cbc26c74c04..2af352ef5a54e 100644 --- a/crates/uv/tests/it/pip_sync.rs +++ b/crates/uv/tests/it/pip_sync.rs @@ -5625,7 +5625,7 @@ fn sync_seed() -> Result<()> { ); // Re-create the environment with seed packages. - uv_snapshot!(context.filters(), context.venv() + uv_snapshot!(context.filters(), context.venv().arg("--clear") .arg("--seed"), @r" success: true exit_code: 0 diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index b374fec0796fd..10cc72b21864c 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -9991,6 +9991,7 @@ fn sync_when_virtual_environment_incompatible_with_interpreter() -> Result<()> { context .venv() .arg(context.venv.as_os_str()) + .arg("--clear") .arg("--python") .arg("3.12") .assert() diff --git a/crates/uv/tests/it/venv.rs b/crates/uv/tests/it/venv.rs index 43cacb6402953..2430e607d7abf 100644 --- a/crates/uv/tests/it/venv.rs +++ b/crates/uv/tests/it/venv.rs @@ -30,10 +30,28 @@ fn create_venv() { context.venv.assert(predicates::path::is_dir()); - // Create a virtual environment at the same location, which should replace it. 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 + warning: A virtual environment already exists at `.venv`. In the future, uv will require `--clear` to replace it + Activate with: source .venv/[BIN]/activate + " + ); + + // Create a virtual environment at the same location using `--clear`, + // which should replace it. + uv_snapshot!(context.filters(), context.venv() + .arg(context.venv.as_os_str()) + .arg("--clear") + .arg("--python") .arg("3.12"), @r###" success: true exit_code: 0 @@ -162,7 +180,7 @@ fn create_venv_project_environment() -> Result<()> { .assert(predicates::path::is_dir()); // Or, of they opt-out with `--no-workspace` or `--no-project` - uv_snapshot!(context.filters(), context.venv().arg("--no-workspace"), @r###" + uv_snapshot!(context.filters(), context.venv().arg("--clear").arg("--no-workspace"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -174,7 +192,7 @@ fn create_venv_project_environment() -> Result<()> { "### ); - uv_snapshot!(context.filters(), context.venv().arg("--no-project"), @r###" + uv_snapshot!(context.filters(), context.venv().arg("--clear").arg("--no-project"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -252,7 +270,7 @@ fn create_venv_reads_request_from_python_version_file() { .write_str("3.12") .unwrap(); - uv_snapshot!(context.filters(), context.venv(), @r###" + uv_snapshot!(context.filters(), context.venv().arg("--clear"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -291,7 +309,7 @@ fn create_venv_reads_request_from_python_versions_file() { .write_str("3.12\n3.11") .unwrap(); - uv_snapshot!(context.filters(), context.venv(), @r###" + uv_snapshot!(context.filters(), context.venv().arg("--clear"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -334,7 +352,7 @@ fn create_venv_respects_pyproject_requires_python() -> Result<()> { "# })?; - uv_snapshot!(context.filters(), context.venv(), @r###" + uv_snapshot!(context.filters(), context.venv().arg("--clear"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -357,7 +375,7 @@ fn create_venv_respects_pyproject_requires_python() -> Result<()> { "# })?; - uv_snapshot!(context.filters(), context.venv(), @r###" + uv_snapshot!(context.filters(), context.venv().arg("--clear"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -380,7 +398,7 @@ fn create_venv_respects_pyproject_requires_python() -> Result<()> { "# })?; - uv_snapshot!(context.filters(), context.venv(), @r###" + uv_snapshot!(context.filters(), context.venv().arg("--clear"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -414,7 +432,7 @@ fn create_venv_respects_pyproject_requires_python() -> Result<()> { "# })?; - uv_snapshot!(context.filters(), context.venv(), @r###" + uv_snapshot!(context.filters(), context.venv().arg("--clear"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -437,7 +455,7 @@ fn create_venv_respects_pyproject_requires_python() -> Result<()> { "# })?; - uv_snapshot!(context.filters(), context.venv(), @r###" + uv_snapshot!(context.filters(), context.venv().arg("--clear"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -460,7 +478,7 @@ fn create_venv_respects_pyproject_requires_python() -> Result<()> { "# })?; - uv_snapshot!(context.filters(), context.venv(), @r###" + uv_snapshot!(context.filters(), context.venv().arg("--clear"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -475,7 +493,7 @@ fn create_venv_respects_pyproject_requires_python() -> Result<()> { context.venv.assert(predicates::path::is_dir()); // We warn if we receive an incompatible version - uv_snapshot!(context.filters(), context.venv().arg("--python").arg("3.11"), @r" + uv_snapshot!(context.filters(), context.venv().arg("--clear").arg("--python").arg("3.11"), @r" success: true exit_code: 0 ----- stdout ----- @@ -527,7 +545,7 @@ fn create_venv_respects_group_requires_python() -> Result<()> { "# })?; - uv_snapshot!(context.filters(), context.venv(), @r" + uv_snapshot!(context.filters(), context.venv().arg("--clear"), @r" success: true exit_code: 0 ----- stdout ----- @@ -560,7 +578,7 @@ fn create_venv_respects_group_requires_python() -> Result<()> { "# })?; - uv_snapshot!(context.filters(), context.venv(), @r" + uv_snapshot!(context.filters(), context.venv().arg("--clear"), @r" success: true exit_code: 0 ----- stdout ----- @@ -593,7 +611,7 @@ fn create_venv_respects_group_requires_python() -> Result<()> { "# })?; - uv_snapshot!(context.filters(), context.venv(), @r" + uv_snapshot!(context.filters(), context.venv().arg("--clear"), @r" success: true exit_code: 0 ----- stdout ----- @@ -621,7 +639,7 @@ fn create_venv_respects_group_requires_python() -> Result<()> { "# })?; - uv_snapshot!(context.filters(), context.venv().arg("--python").arg("3.11"), @r" + uv_snapshot!(context.filters(), context.venv().arg("--clear").arg("--python").arg("3.11"), @r" success: true exit_code: 0 ----- stdout ----- @@ -654,7 +672,7 @@ fn create_venv_respects_group_requires_python() -> Result<()> { "# })?; - uv_snapshot!(context.filters(), context.venv().arg("--python").arg("3.11"), @r" + uv_snapshot!(context.filters(), context.venv().arg("--clear").arg("--python").arg("3.11"), @r" success: false exit_code: 2 ----- stdout ----- @@ -945,15 +963,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: The directory `.venv` exists, but it's not a virtual environment + warning: A directory already exists at `.venv`. In the future, uv will require `--clear` to replace it + Activate with: source .venv/[BIN]/activate " ); @@ -973,15 +991,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: The directory `.venv` exists, but it's not a virtual environment + warning: A directory already exists at `.venv`. In the future, uv will require `--clear` to replace it + Activate with: source .venv/[BIN]/activate " ); @@ -1102,31 +1120,6 @@ fn windows_shims() -> Result<()> { Ok(()) } -#[test] -fn virtualenv_compatibility() { - let context = TestContext::new_with_versions(&["3.12"]); - - // Create a virtual environment at `.venv`, passing the redundant `--clear` flag. - uv_snapshot!(context.filters(), context.venv() - .arg(context.venv.as_os_str()) - .arg("--clear") - .arg("--python") - .arg("3.12"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - warning: virtualenv's `--clear` has no effect (uv always clears the virtual environment) - 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 verify_pyvenv_cfg() { let context = TestContext::new("3.12"); @@ -1154,6 +1147,7 @@ fn verify_pyvenv_cfg_relocatable() { context .venv() .arg(context.venv.as_os_str()) + .arg("--clear") .arg("--python") .arg("3.12") .arg("--relocatable") diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 3b57800333089..5a58ac6ebd995 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -4614,7 +4614,7 @@ uv venv [OPTIONS] [PATH]

Options

--allow-existing

Preserve any existing files or directories at the target path.

-

By default, uv venv will remove an existing virtual environment at the given path, and exit with an error if the path is non-empty but not a virtual environment. The --allow-existing option will instead write to the given path, regardless of its contents, and without clearing it beforehand.

+

By default, uv venv will exit with an error if the given path is non-empty. The --allow-existing option will instead write to the given path, regardless of its contents, and without clearing it beforehand.

WARNING: This option can lead to unexpected behavior if the existing virtual environment and the newly-created virtual environment are linked to different Python interpreters.

--allow-insecure-host, --trusted-host allow-insecure-host

Allow insecure connections to a host.

Can be provided multiple times.

@@ -4623,7 +4623,9 @@ uv venv [OPTIONS] [PATH]

May also be set with the UV_INSECURE_HOST environment variable.

--cache-dir cache-dir

Path to the cache directory.

Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

To view the location of the cache directory, run uv cache dir.

-

May also be set with the UV_CACHE_DIR environment variable.

--color color-choice

Control the use of color in output.

+

May also be set with the UV_CACHE_DIR environment variable.

--clear, -c

Remove 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 --clear option will instead clear a non-empty path before creating a new virtual environment.

+

May also be set with the UV_VENV_CLEAR environment variable.

--color color-choice

Control the use of color in output.

By default, uv will automatically detect support for colors when writing to a terminal.

Possible values:

    diff --git a/docs/reference/environment.md b/docs/reference/environment.md index bf8bf29ec3322..57217ba27be89 100644 --- a/docs/reference/environment.md +++ b/docs/reference/environment.md @@ -441,6 +441,11 @@ Equivalent to the `--torch-backend` command-line argument (e.g., `cpu`, `cu126`, Used ephemeral environments like CI to install uv to a specific path while preventing the installer from modifying shell profiles or environment variables. +### `UV_VENV_CLEAR` + +Equivalent to the `--clear` command-line argument. If set, uv will remove any +existing files or directories at the target path. + ### `UV_VENV_SEED` Install seed packages (one or more of: `pip`, `setuptools`, and `wheel`) into the virtual environment