diff --git a/Cargo.lock b/Cargo.lock index 44f94fcc6..3390012a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7194,7 +7194,6 @@ dependencies = [ "uv-python", "uv-shell", "uv-version", - "uv-warnings", ] [[package]] diff --git a/crates/uv-virtualenv/Cargo.toml b/crates/uv-virtualenv/Cargo.toml index 18b5c9e3e..1f9d17c1d 100644 --- a/crates/uv-virtualenv/Cargo.toml +++ b/crates/uv-virtualenv/Cargo.toml @@ -26,7 +26,6 @@ uv-pypi-types = { workspace = true } uv-python = { workspace = true } uv-shell = { workspace = true } uv-version = { workspace = true } -uv-warnings = { workspace = true } console = { workspace = true } fs-err = { workspace = true } diff --git a/crates/uv-virtualenv/src/lib.rs b/crates/uv-virtualenv/src/lib.rs index a8fdba792..44a39ead9 100644 --- a/crates/uv-virtualenv/src/lib.rs +++ b/crates/uv-virtualenv/src/lib.rs @@ -1,5 +1,5 @@ use std::io; -use std::path::Path; +use std::path::{Path, PathBuf}; use thiserror::Error; @@ -20,6 +20,13 @@ pub enum Error { NotFound(String), #[error(transparent)] Python(#[from] uv_python::managed::Error), + #[error("A {name} already exists at `{}`. Use `--clear` to replace it", path.display())] + Exists { + /// The type of environment (e.g., "virtual environment"). + name: &'static str, + /// The path to the existing environment. + path: PathBuf, + }, } /// The value to use for the shell prompt when inside a virtual environment. diff --git a/crates/uv-virtualenv/src/virtualenv.rs b/crates/uv-virtualenv/src/virtualenv.rs index a4a021491..75282f41e 100644 --- a/crates/uv-virtualenv/src/virtualenv.rs +++ b/crates/uv-virtualenv/src/virtualenv.rs @@ -11,6 +11,7 @@ use itertools::Itertools; use owo_colors::OwoColorize; use tracing::{debug, trace}; +use crate::{Error, Prompt}; use uv_fs::{CWD, Simplified, cachedir}; use uv_platform_tags::Os; use uv_preview::Preview; @@ -19,9 +20,6 @@ use uv_python::managed::{PythonMinorVersionLink, create_link_to_executable}; use uv_python::{Interpreter, VirtualEnvironment}; use uv_shell::escape_posix_for_single_quotes; use uv_version::version; -use uv_warnings::warn_user_once; - -use crate::{Error, Prompt}; /// Activation scripts for the environment, with dependent paths templated out. const ACTIVATE_TEMPLATES: &[(&str, &str)] = &[ @@ -164,13 +162,12 @@ pub(crate) fn create( fs_err::create_dir_all(&location)?; } Some(false) => return err, - // When we don't have a TTY, warn that the behavior will change in the future + // When we don't have a TTY, require `--clear` explicitly. None => { - warn_user_once!( - "A {name} already exists at `{}`. In the future, uv will require `{}` to replace it", - location.user_display(), - "--clear".green(), - ); + return Err(Error::Exists { + name, + path: location.to_path_buf(), + }); } } } diff --git a/crates/uv/tests/it/build.rs b/crates/uv/tests/it/build.rs index 3dc457516..2a92375e1 100644 --- a/crates/uv/tests/it/build.rs +++ b/crates/uv/tests/it/build.rs @@ -2161,7 +2161,7 @@ fn venv_included_in_sdist() -> Result<()> { .child("pyproject.toml") .write_str(pyproject_toml)?; - context.venv().assert().success(); + context.venv().arg("--clear").assert().success(); // context.filters() uv_snapshot!(context.filters(), context.build(), @" diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index d6bc86e23..c5f89efe8 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -13839,7 +13839,13 @@ fn build_backend_wrong_wheel_platform() -> Result<()> { // (compatible host, incompatible host) x (compatible target, incompatible target) // A Python 3.13 host with a 3.13 implicit target works. - context.venv().arg("-p").arg("3.13").assert().success(); + context + .venv() + .arg("--clear") + .arg("-p") + .arg("3.13") + .assert() + .success(); uv_snapshot!(context.filters(), context.pip_install().arg("./child"), @" success: true exit_code: 0 @@ -13853,7 +13859,13 @@ fn build_backend_wrong_wheel_platform() -> Result<()> { "); // A Python 3.13 host with a 3.12 explicit target fails. - context.venv().arg("-p").arg("3.13").assert().success(); + context + .venv() + .arg("--clear") + .arg("-p") + .arg("3.13") + .assert() + .success(); uv_snapshot!(context.filters(), context.pip_install().arg("--python-version").arg("3.12").arg("./child"), @" success: false exit_code: 1 @@ -13866,8 +13878,14 @@ fn build_backend_wrong_wheel_platform() -> Result<()> { "); // A python 3.12 host with a 3.13 explicit target works. - context.venv().arg("-p").arg("3.13").assert().success(); - uv_snapshot!(context.filters(), context.pip_install().arg("--python-version").arg("3.13").arg("./child"), @" + context + .venv() + .arg("--clear") + .arg("-p") + .arg("3.13") + .assert() + .success(); + uv_snapshot!(context.filters(), context.pip_install().arg("--python-version").arg("3.13").arg("./child"), @r" success: true exit_code: 0 ----- stdout ----- @@ -13875,13 +13893,18 @@ fn build_backend_wrong_wheel_platform() -> Result<()> { ----- stderr ----- Resolved 1 package in [TIME] Prepared 1 package in [TIME] - Uninstalled 1 package in [TIME] Installed 1 package in [TIME] - ~ py313==0.1.0 (from file://[TEMP_DIR]/child) + + py313==0.1.0 (from file://[TEMP_DIR]/child) "); // A Python 3.13 host with a 3.12 explicit target fails. - context.venv().arg("-p").arg("3.13").assert().success(); + context + .venv() + .arg("--clear") + .arg("-p") + .arg("3.13") + .assert() + .success(); uv_snapshot!(context.filters(), context.pip_install().arg("--python-version").arg("3.12").arg("./child"), @" success: false exit_code: 1 @@ -13916,7 +13939,13 @@ fn build_backend_wrong_wheel_platform() -> Result<()> { .touch()?; // A build host of 3.13 works. - context.venv().arg("-p").arg("3.13").assert().success(); + context + .venv() + .arg("--clear") + .arg("-p") + .arg("3.13") + .assert() + .success(); uv_snapshot!(context.filters(), context.pip_install().arg("--python-version").arg("3.12").arg("."), @" success: true exit_code: 0 @@ -13930,7 +13959,13 @@ fn build_backend_wrong_wheel_platform() -> Result<()> { "); // A build host of 3.12 fails. - context.venv().arg("-p").arg("3.12").assert().success(); + context + .venv() + .arg("--clear") + .arg("-p") + .arg("3.12") + .assert() + .success(); uv_snapshot!(context.filters(), context.pip_install().arg("--python-version").arg("3.12").arg("."), @" success: false exit_code: 1 diff --git a/crates/uv/tests/it/python_module.rs b/crates/uv/tests/it/python_module.rs index 0b1489c09..3fc0eec32 100644 --- a/crates/uv/tests/it/python_module.rs +++ b/crates/uv/tests/it/python_module.rs @@ -197,7 +197,7 @@ fn find_uv_bin_base_prefix() { " ); - context.venv().assert().success(); + context.venv().arg("--clear").assert().success(); // Mutate `base_prefix` to simulate lookup in a system Python installation uv_snapshot!(context.filters(), context.python_command() diff --git a/crates/uv/tests/it/run.rs b/crates/uv/tests/it/run.rs index 9019ab025..bdbeecbd9 100644 --- a/crates/uv/tests/it/run.rs +++ b/crates/uv/tests/it/run.rs @@ -1478,7 +1478,12 @@ fn run_with_overlay_interpreter() -> Result<()> { "); // Switch to a relocatable virtual environment. - context.venv().arg("--relocatable").assert().success(); + context + .venv() + .arg("--allow-existing") + .arg("--relocatable") + .assert() + .success(); // Cleanup previous shutil fs_err::remove_file(context.temp_dir.child("main"))?; diff --git a/crates/uv/tests/it/venv.rs b/crates/uv/tests/it/venv.rs index 5d09ed391..09e4aaa32 100644 --- a/crates/uv/tests/it/venv.rs +++ b/crates/uv/tests/it/venv.rs @@ -36,16 +36,16 @@ fn create_venv() { uv_snapshot!(context.filters(), context.venv() .arg(context.venv.as_os_str()) .arg("--python") - .arg("3.12"), @" - success: true - exit_code: 0 + .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 - warning: A virtual environment 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 virtual environment already exists at `[VENV]/`. Use `--clear` to replace it " ); @@ -221,7 +221,7 @@ fn virtual_empty() -> Result<()> { wow = "someconfig" "#})?; - uv_snapshot!(context.filters(), context.venv(), @" + uv_snapshot!(context.filters(), context.venv().arg("--clear"), @r" success: true exit_code: 0 ----- stdout ----- @@ -229,7 +229,6 @@ fn virtual_empty() -> Result<()> { ----- 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 "); @@ -250,7 +249,7 @@ fn virtual_dependency_group() -> Result<()> { dev = ["sniffio"] "#})?; - uv_snapshot!(context.filters(), context.venv(), @" + uv_snapshot!(context.filters(), context.venv().arg("--clear"), @r" success: true exit_code: 0 ----- stdout ----- @@ -258,7 +257,6 @@ fn virtual_dependency_group() -> Result<()> { ----- 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 "); @@ -1511,7 +1509,19 @@ fn venv_python_preference() { Activate with: source .venv/[BIN]/activate "); - uv_snapshot!(context.filters(), context.venv().arg("--no-managed-python"), @" + uv_snapshot!(context.filters(), context.venv().arg("--no-managed-python"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.11.[X] interpreter at: [PYTHON-3.11] + Creating virtual environment at: .venv + error: Failed to create virtual environment + Caused by: A virtual environment already exists at `.venv`. Use `--clear` to replace it + "); + + uv_snapshot!(context.filters(), context.venv().arg("--clear").arg("--no-managed-python"), @" success: true exit_code: 0 ----- stdout ----- @@ -1519,23 +1529,22 @@ fn venv_python_preference() { ----- stderr ----- Using CPython 3.11.[X] interpreter at: [PYTHON-3.11] 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 "); - uv_snapshot!(context.filters(), context.venv().arg("--no-managed-python"), @" - success: true - exit_code: 0 + uv_snapshot!(context.filters(), context.venv(), @r" + success: false + exit_code: 2 ----- stdout ----- ----- stderr ----- - Using CPython 3.11.[X] interpreter at: [PYTHON-3.11] + Using CPython 3.12.[X] 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 + error: Failed to create virtual environment + Caused by: A virtual environment already exists at `.venv`. Use `--clear` to replace it "); - uv_snapshot!(context.filters(), context.venv(), @" + uv_snapshot!(context.filters(), context.venv().arg("--clear").arg("--managed-python"), @" success: true exit_code: 0 ----- stdout ----- @@ -1543,19 +1552,6 @@ fn venv_python_preference() { ----- stderr ----- Using CPython 3.12.[X] 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 - "); - - uv_snapshot!(context.filters(), context.venv().arg("--managed-python"), @" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Using CPython 3.12.[X] - 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 "); } @@ -1653,11 +1649,12 @@ fn create_venv_symlink_recreate_preservation() -> Result<()> { // Verify symlink is preserved after first creation assert!(symlink_path.path().is_symlink()); - // Run uv venv again WITHOUT --clear to test recreation behavior + // Run uv venv again with --clear to test symlink preservation during recreation uv_snapshot!(context.filters(), context.venv() .arg(symlink_path.as_os_str()) + .arg("--clear") .arg("--python") - .arg("3.12"), @" + .arg("3.12"), @r" success: true exit_code: 0 ----- stdout ----- @@ -1665,7 +1662,6 @@ fn create_venv_symlink_recreate_preservation() -> Result<()> { ----- 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 " ); @@ -1717,11 +1713,12 @@ fn create_venv_nested_symlink_preservation() -> Result<()> { assert!(symlink_path.path().is_symlink()); assert!(intermediate_link.path().is_symlink()); - // Run uv venv again to test nested symlink preservation during recreation + // Run uv venv again with --clear to test nested symlink preservation during recreation uv_snapshot!(context.filters(), context.venv() .arg(symlink_path.as_os_str()) + .arg("--clear") .arg("--python") - .arg("3.12"), @" + .arg("3.12"), @r" success: true exit_code: 0 ----- stdout ----- @@ -1729,7 +1726,6 @@ fn create_venv_nested_symlink_preservation() -> Result<()> { ----- 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 " );