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:
@@ -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
@@ -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]]
|
||||
|
||||
@@ -331,7 +331,7 @@ impl SourceBuild {
|
||||
interpreter.clone(),
|
||||
uv_virtualenv::Prompt::None,
|
||||
false,
|
||||
false,
|
||||
uv_virtualenv::OnExisting::Remove,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()?;
|
||||
|
||||
@@ -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`.
|
||||
///
|
||||
|
||||
@@ -285,7 +285,7 @@ impl InstalledTools {
|
||||
interpreter,
|
||||
uv_virtualenv::Prompt::None,
|
||||
false,
|
||||
false,
|
||||
uv_virtualenv::OnExisting::Remove,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user