From dc2473af283e6d747eb8124955d1d6dc5db2021b Mon Sep 17 00:00:00 2001 From: John Mumm Date: Thu, 26 Jun 2025 11:39:39 +0200 Subject: [PATCH] Require `uv venv --clear` before clearing an existing directory --- .github/workflows/ci.yml | 6 +- Cargo.lock | 3 + crates/uv-build-frontend/src/lib.rs | 1 + crates/uv-cli/src/compat.rs | 9 -- crates/uv-cli/src/lib.rs | 13 ++- crates/uv-static/src/env_vars.rs | 7 ++ crates/uv-tool/src/lib.rs | 1 + crates/uv-virtualenv/Cargo.toml | 3 + crates/uv-virtualenv/src/lib.rs | 2 + crates/uv-virtualenv/src/virtualenv.rs | 37 +++++++- crates/uv/src/commands/project/environment.rs | 1 + crates/uv/src/commands/project/mod.rs | 4 + crates/uv/src/commands/project/run.rs | 3 + crates/uv/src/commands/venv.rs | 4 + crates/uv/src/lib.rs | 1 + crates/uv/src/settings.rs | 3 + crates/uv/tests/it/cache_prune.rs | 2 +- crates/uv/tests/it/common/mod.rs | 1 + crates/uv/tests/it/pip_install.rs | 7 +- crates/uv/tests/it/pip_sync.rs | 2 +- crates/uv/tests/it/sync.rs | 1 + crates/uv/tests/it/venv.rs | 92 ++++++++++--------- docs/reference/cli.md | 6 +- docs/reference/environment.md | 8 ++ 24 files changed, 146 insertions(+), 71 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c6d47ce2..033ae4303 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -981,7 +981,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: | @@ -1697,14 +1697,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 0c5bab5a7..f4dc8edec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6075,13 +6075,16 @@ version = "0.7.17" name = "uv-virtualenv" version = "0.0.4" dependencies = [ + "console", "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", diff --git a/crates/uv-build-frontend/src/lib.rs b/crates/uv-build-frontend/src/lib.rs index df6fc09cf..cea6d633f 100644 --- a/crates/uv-build-frontend/src/lib.rs +++ b/crates/uv-build-frontend/src/lib.rs @@ -334,6 +334,7 @@ impl SourceBuild { uv_virtualenv::Prompt::None, false, false, + true, false, false, false, diff --git a/crates/uv-cli/src/compat.rs b/crates/uv-cli/src/compat.rs index d29afa760..344d1a4e7 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 bf605198f..3477774b0 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -2562,16 +2562,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, conflicts_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, conflicts_with = "clear")] pub allow_existing: bool, /// The path to the virtual environment to create. diff --git a/crates/uv-static/src/env_vars.rs b/crates/uv-static/src/env_vars.rs index 4ac2976d9..f28bf6957 100644 --- a/crates/uv-static/src/env_vars.rs +++ b/crates/uv-static/src/env_vars.rs @@ -292,6 +292,13 @@ impl EnvVars { /// Distributions can be read from a local directory by using the `file://` URL scheme. pub const UV_PYPY_INSTALL_MIRROR: &'static str = "UV_PYPY_INSTALL_MIRROR"; + /// 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. + pub const UV_VENV_CLEAR: &'static str = "UV_VENV_CLEAR"; + /// Install seed packages (one or more of: `pip`, `setuptools`, and `wheel`) into the virtual environment /// created by `uv venv`. /// diff --git a/crates/uv-tool/src/lib.rs b/crates/uv-tool/src/lib.rs index ee80a2854..fcb31c8b5 100644 --- a/crates/uv-tool/src/lib.rs +++ b/crates/uv-tool/src/lib.rs @@ -286,6 +286,7 @@ impl InstalledTools { uv_virtualenv::Prompt::None, false, false, + true, false, false, false, diff --git a/crates/uv-virtualenv/Cargo.toml b/crates/uv-virtualenv/Cargo.toml index cb0ae1b9d..8a1dbd0d5 100644 --- a/crates/uv-virtualenv/Cargo.toml +++ b/crates/uv-virtualenv/Cargo.toml @@ -21,14 +21,17 @@ workspace = true [dependencies] uv-configuration = { workspace = true } +uv-console = { workspace = true } uv-fs = { workspace = true } uv-pypi-types = { workspace = true } uv-python = { workspace = true } uv-shell = { workspace = true } uv-version = { workspace = true } +console = { workspace = true } fs-err = { workspace = true } itertools = { workspace = true } +owo-colors = { workspace = true } pathdiff = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } diff --git a/crates/uv-virtualenv/src/lib.rs b/crates/uv-virtualenv/src/lib.rs index 277ab6a8c..088ada59f 100644 --- a/crates/uv-virtualenv/src/lib.rs +++ b/crates/uv-virtualenv/src/lib.rs @@ -51,6 +51,7 @@ pub fn create_venv( prompt: Prompt, system_site_packages: bool, allow_existing: bool, + clear: bool, relocatable: bool, seed: bool, upgradeable: bool, @@ -63,6 +64,7 @@ pub fn create_venv( prompt, system_site_packages, allow_existing, + clear, relocatable, seed, upgradeable, diff --git a/crates/uv-virtualenv/src/virtualenv.rs b/crates/uv-virtualenv/src/virtualenv.rs index bad380c4c..a32b67d64 100644 --- a/crates/uv-virtualenv/src/virtualenv.rs +++ b/crates/uv-virtualenv/src/virtualenv.rs @@ -5,9 +5,11 @@ use std::io; use std::io::{BufWriter, Write}; use std::path::Path; +use console::Term; use fs_err as fs; use fs_err::File; use itertools::Itertools; +use owo_colors::OwoColorize; use tracing::debug; use uv_configuration::PreviewMode; @@ -53,6 +55,7 @@ pub(crate) fn create( prompt: Prompt, system_site_packages: bool, allow_existing: bool, + clear: bool, relocatable: bool, seed: bool, upgradeable: bool, @@ -83,10 +86,17 @@ pub(crate) fn create( format!("File exists at `{}`", location.user_display()), ))); } else if metadata.is_dir() { + let confirmation_required = !clear && !allow_existing; + let confirmed_clear = confirmation_required && confirm_clear(location)?; + if allow_existing { - debug!("Allowing existing directory"); - } else if uv_fs::is_virtualenv_base(location) { - debug!("Removing existing directory"); + debug!("Allowing existing directory due to `--allow-existing`"); + } else if clear || confirmed_clear { + if clear { + debug!("Removing existing directory due to `--clear`"); + } else { + debug!("Removing existing directory"); + } // On Windows, if the current executable is in the directory, guard against // self-deletion. @@ -110,8 +120,12 @@ pub(crate) fn create( return Err(Error::Io(io::Error::new( io::ErrorKind::AlreadyExists, format!( - "The directory `{}` exists, but it's not a virtual environment", - location.user_display() + "The directory `{}` exists. \n\n{}{} Use `{}` to remove the directory first or `{}` to write to the directory without clearing", + location.user_display(), + "hint".bold().cyan(), + ":".bold(), + "--clear".green(), + "--allow-existing".green(), ), ))); } @@ -464,6 +478,19 @@ pub(crate) fn create( }) } +fn confirm_clear(location: &Path) -> Result { + let term = Term::stderr(); + if term.is_term() { + let prompt = format!( + "The directory `{}` exists. Did you mean to clear its contents (`--clear`)?", + location.user_display(), + ); + uv_console::confirm(&prompt, &term, true) + } else { + Ok(false) + } +} + #[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 f5a9713d2..e821c8693 100644 --- a/crates/uv/src/commands/project/environment.rs +++ b/crates/uv/src/commands/project/environment.rs @@ -98,6 +98,7 @@ impl CachedEnvironment { false, false, true, + true, false, false, preview, diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index c84253eaa..b26dc1e56 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -1309,6 +1309,7 @@ impl ProjectEnvironment { prompt, false, false, + true, false, false, upgradeable, @@ -1348,6 +1349,7 @@ impl ProjectEnvironment { prompt, false, false, + true, false, false, upgradeable, @@ -1486,6 +1488,7 @@ impl ScriptEnvironment { prompt, false, false, + true, false, false, upgradeable, @@ -1522,6 +1525,7 @@ impl ScriptEnvironment { prompt, false, false, + true, false, false, upgradeable, diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 6ece28eaf..080eb2792 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -453,6 +453,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl uv_virtualenv::Prompt::None, false, false, + true, false, false, false, @@ -657,6 +658,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl uv_virtualenv::Prompt::None, false, false, + true, false, false, false, @@ -886,6 +888,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl uv_virtualenv::Prompt::None, false, false, + true, false, false, false, diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index fe20634d0..26e168625 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -61,6 +61,7 @@ pub(crate) async fn venv( system_site_packages: bool, seed: bool, allow_existing: bool, + clear: bool, exclude_newer: Option, concurrency: Concurrency, no_config: bool, @@ -87,6 +88,7 @@ pub(crate) async fn venv( python_preference, python_downloads, allow_existing, + clear, exclude_newer, concurrency, no_config, @@ -144,6 +146,7 @@ async fn venv_impl( python_preference: PythonPreference, python_downloads: PythonDownloads, allow_existing: bool, + clear: bool, exclude_newer: Option, concurrency: Concurrency, no_config: bool, @@ -289,6 +292,7 @@ async fn venv_impl( prompt, system_site_packages, allow_existing, + clear, relocatable, seed, upgradeable, diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index ab4aee9e9..60a103f92 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1043,6 +1043,7 @@ async fn run(mut cli: Cli) -> Result { args.system_site_packages, args.seed, args.allow_existing, + args.clear, 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 58a012d89..67968612a 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -2561,6 +2561,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, @@ -2579,6 +2580,7 @@ impl VenvSettings { no_system, seed, allow_existing, + clear, path, prompt, system_site_packages, @@ -2596,6 +2598,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 a6ec48bd4..99493fe21 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 4c411899c..84dd5a4ba 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -1390,6 +1390,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 91da3ce81..482aff196 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -2776,7 +2776,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!( @@ -2794,7 +2794,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. @@ -3005,7 +3005,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!( @@ -8175,6 +8175,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 43cbc26c7..2af352ef5 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 da59682ab..d58404be2 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -9595,6 +9595,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 52291c05d..e197c0cea 100644 --- a/crates/uv/tests/it/venv.rs +++ b/crates/uv/tests/it/venv.rs @@ -30,10 +30,34 @@ fn create_venv() { context.venv.assert(predicates::path::is_dir()); - // Create a virtual environment at the same location, which should replace it. + // Attempt to create a virtual environment at the same location, + // which should fail. uv_snapshot!(context.filters(), context.venv() .arg(context.venv.as_os_str()) .arg("--python") + .arg("3.12"), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtual environment at: .venv + uv::venv::creation + + × Failed to create virtualenv + ╰─▶ The directory `.venv` exists. + + hint: Use `--clear` to remove the directory first or `--allow-existing` to write to the directory without clearing + " + ); + + // 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 +186,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 +198,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 +276,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 +315,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 +358,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 +381,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 +404,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 +438,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 +461,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 +484,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 +499,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 +551,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 +584,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 +617,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 +645,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 +678,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: 1 ----- stdout ----- @@ -981,7 +1005,9 @@ fn non_empty_dir_exists() -> Result<()> { uv::venv::creation × Failed to create virtualenv - ╰─▶ The directory `.venv` exists, but it's not a virtual environment + ╰─▶ The directory `.venv` exists. + + hint: Use `--clear` to remove the directory first or `--allow-existing` to write to the directory without clearing "### ); @@ -1011,7 +1037,9 @@ fn non_empty_dir_exists_allow_existing() -> Result<()> { uv::venv::creation × Failed to create virtualenv - ╰─▶ The directory `.venv` exists, but it's not a virtual environment + ╰─▶ The directory `.venv` exists. + + hint: Use `--clear` to remove the directory first or `--allow-existing` to write to the directory without clearing "### ); @@ -1132,31 +1160,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"); @@ -1184,6 +1187,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 82fe0fa3d..75b043ff3 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -4554,7 +4554,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.

@@ -4563,7 +4563,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 61889ddb3..598c92279 100644 --- a/docs/reference/environment.md +++ b/docs/reference/environment.md @@ -433,6 +433,14 @@ 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` + +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. + ### `UV_VENV_SEED` Install seed packages (one or more of: `pip`, `setuptools`, and `wheel`) into the virtual environment