Require uv venv --clear before removing an existing directory (#14309)

By default, `uv venv <venv-name>` currently removes the `<venv-name`>
directory if it exists. This can be surprising behavior: not everyone
expects an existing environment to be overwritten. This PR updates the
default to fail if a non-empty `<venv-name>` directory already exists
and neither `--allow-existing` nor the new `-c/--clear` option is
provided (if a TTY is detected, it prompts first). If it's not a TTY,
then uv will only warn and not fail for now — we'll make this an error
in the future. I've also added a corresponding `UV_VENV_CLEAR` env var.

I've chosen to use `--clear` instead of `--force` for this option
because it is used by the `venv` module and `virtualenv` and will be
familiar to users. I also think its meaning is clearer in this context
than `--force` (which could plausibly mean force overwrite just the
virtual environment files, which is what our current `--allow-existing`
option does).

Closes #1472.

---------

Co-authored-by: Zanie Blue <contact@zanie.dev>
This commit is contained in:
John Mumm
2025-07-16 21:25:48 +02:00
committed by Zanie Blue
parent 25e69458b1
commit 2df06ebfbc
25 changed files with 282 additions and 135 deletions
+5 -5
View File
@@ -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
Generated
+4
View File
@@ -5998,18 +5998,22 @@ version = "0.7.22"
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]]
+1 -1
View File
@@ -331,7 +331,7 @@ impl SourceBuild {
interpreter.clone(),
uv_virtualenv::Prompt::None,
false,
false,
uv_virtualenv::OnExisting::Remove,
false,
false,
false,
-9
View File
@@ -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)"
+10 -3
View File
@@ -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.
+31 -1
View File
@@ -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<bool> {
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<bool> {
confirm_inner(message, Some(hint), term, default)
}
fn confirm_inner(
message: &str,
hint: Option<&str>,
term: &Term,
default: bool,
) -> std::io::Result<bool> {
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<boo
);
term.write_str(&prompt)?;
if let Some(hint) = hint {
term.write_str(&format!(
"\n\n{}{} {hint}",
style("hint").for_stderr().bold().cyan(),
style(":").for_stderr().bold()
))?;
}
term.hide_cursor()?;
term.flush()?;
@@ -56,7 +82,11 @@ pub fn confirm(message: &str, term: &Term, default: bool) -> std::io::Result<boo
.cyan(),
);
term.clear_line()?;
if hint.is_some() {
term.clear_last_lines(2)?;
} else {
term.clear_line()?;
}
term.write_line(&report)?;
term.show_cursor()?;
term.flush()?;
+4
View File
@@ -305,6 +305,10 @@ 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";
/// Equivalent to the `--clear` command-line argument. If set, uv will remove any
/// existing files or directories at the target path.
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`.
///
+1 -1
View File
@@ -285,7 +285,7 @@ impl InstalledTools {
interpreter,
uv_virtualenv::Prompt::None,
false,
false,
uv_virtualenv::OnExisting::Remove,
false,
false,
false,
+4
View File
@@ -21,14 +21,18 @@ 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 }
uv-warnings = { 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 }
+4 -2
View File
@@ -6,6 +6,8 @@ use thiserror::Error;
use uv_configuration::PreviewMode;
use uv_python::{Interpreter, PythonEnvironment};
pub use virtualenv::OnExisting;
mod virtualenv;
#[derive(Debug, Error)]
@@ -50,7 +52,7 @@ pub fn create_venv(
interpreter: Interpreter,
prompt: Prompt,
system_site_packages: bool,
allow_existing: bool,
on_existing: OnExisting,
relocatable: bool,
seed: bool,
upgradeable: bool,
@@ -62,7 +64,7 @@ pub fn create_venv(
&interpreter,
prompt,
system_site_packages,
allow_existing,
on_existing,
relocatable,
seed,
upgradeable,
+132 -37
View File
@@ -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;
@@ -17,6 +19,7 @@ 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};
@@ -52,7 +55,7 @@ pub(crate) fn create(
interpreter: &Interpreter,
prompt: Prompt,
system_site_packages: bool,
allow_existing: bool,
on_existing: OnExisting,
relocatable: bool,
seed: bool,
upgradeable: bool,
@@ -76,47 +79,74 @@ pub(crate) fn create(
// Validate the existing location.
match location.metadata() {
Ok(metadata) => {
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<Option<bool>, 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).
@@ -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,
+4 -4
View File
@@ -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,
+4 -4
View File
@@ -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,
+3 -2
View File
@@ -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<ExcludeNewer>,
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,
+3 -1
View File
@@ -1032,6 +1032,8 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
let python_request: Option<PythonRequest> =
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,
@@ -1048,7 +1050,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
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,
+3
View File
@@ -2623,6 +2623,7 @@ impl BuildSettings {
pub(crate) struct VenvSettings {
pub(crate) seed: bool,
pub(crate) allow_existing: bool,
pub(crate) clear: bool,
pub(crate) path: Option<PathBuf>,
pub(crate) prompt: Option<String>,
pub(crate) system_site_packages: bool,
@@ -2641,6 +2642,7 @@ impl VenvSettings {
no_system,
seed,
allow_existing,
clear,
path,
prompt,
system_site_packages,
@@ -2658,6 +2660,7 @@ impl VenvSettings {
Self {
seed,
allow_existing,
clear,
path,
prompt,
system_site_packages,
+1 -1
View File
@@ -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.
+1
View File
@@ -1415,6 +1415,7 @@ pub fn create_venv_from_executable<P: AsRef<Path>>(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")
+4 -3
View File
@@ -2859,7 +2859,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!(
@@ -2877,7 +2877,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.
@@ -3088,7 +3088,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!(
@@ -8252,6 +8252,7 @@ fn install_relocatable() -> Result<()> {
context
.venv()
.arg(context.venv.as_os_str())
.arg("--clear")
.arg("--python")
.arg("3.12")
.arg("--relocatable")
+1 -1
View File
@@ -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
+1
View File
@@ -9987,6 +9987,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()
+44 -50
View File
@@ -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")
+4 -2
View File
@@ -4683,7 +4683,7 @@ uv venv [OPTIONS] [PATH]
<h3 class="cli-reference">Options</h3>
<dl class="cli-reference"><dt id="uv-venv--allow-existing"><a href="#uv-venv--allow-existing"><code>--allow-existing</code></a></dt><dd><p>Preserve any existing files or directories at the target path.</p>
<p>By default, <code>uv venv</code> will remove an existing virtual environment at the given path, and exit with an error if the path is non-empty but <em>not</em> a virtual environment. The <code>--allow-existing</code> option will instead write to the given path, regardless of its contents, and without clearing it beforehand.</p>
<p>By default, <code>uv venv</code> will exit with an error if the given path is non-empty. The <code>--allow-existing</code> option will instead write to the given path, regardless of its contents, and without clearing it beforehand.</p>
<p>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.</p>
</dd><dt id="uv-venv--allow-insecure-host"><a href="#uv-venv--allow-insecure-host"><code>--allow-insecure-host</code></a>, <code>--trusted-host</code> <i>allow-insecure-host</i></dt><dd><p>Allow insecure connections to a host.</p>
<p>Can be provided multiple times.</p>
@@ -4692,7 +4692,9 @@ uv venv [OPTIONS] [PATH]
<p>May also be set with the <code>UV_INSECURE_HOST</code> environment variable.</p></dd><dt id="uv-venv--cache-dir"><a href="#uv-venv--cache-dir"><code>--cache-dir</code></a> <i>cache-dir</i></dt><dd><p>Path to the cache directory.</p>
<p>Defaults to <code>$XDG_CACHE_HOME/uv</code> or <code>$HOME/.cache/uv</code> on macOS and Linux, and <code>%LOCALAPPDATA%\uv\cache</code> on Windows.</p>
<p>To view the location of the cache directory, run <code>uv cache dir</code>.</p>
<p>May also be set with the <code>UV_CACHE_DIR</code> environment variable.</p></dd><dt id="uv-venv--color"><a href="#uv-venv--color"><code>--color</code></a> <i>color-choice</i></dt><dd><p>Control the use of color in output.</p>
<p>May also be set with the <code>UV_CACHE_DIR</code> environment variable.</p></dd><dt id="uv-venv--clear"><a href="#uv-venv--clear"><code>--clear</code></a>, <code>-c</code></dt><dd><p>Remove any existing files or directories at the target path.</p>
<p>By default, <code>uv venv</code> will exit with an error if the given path is non-empty. The <code>--clear</code> option will instead clear a non-empty path before creating a new virtual environment.</p>
<p>May also be set with the <code>UV_VENV_CLEAR</code> environment variable.</p></dd><dt id="uv-venv--color"><a href="#uv-venv--color"><code>--color</code></a> <i>color-choice</i></dt><dd><p>Control the use of color in output.</p>
<p>By default, uv will automatically detect support for colors when writing to a terminal.</p>
<p>Possible values:</p>
<ul>
+5
View File
@@ -458,6 +458,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