Require --force to replace existing virtual environments in uv venv

This commit is contained in:
Zanie Blue 2025-04-15 16:52:15 -05:00
parent df35919d5a
commit 9e69276a6f
11 changed files with 101 additions and 19 deletions

View file

@ -323,6 +323,7 @@ impl SourceBuild {
uv_virtualenv::Prompt::None,
false,
false,
uv_virtualenv::VenvForceMode::ReplaceAny,
false,
false,
)?

View file

@ -2470,16 +2470,26 @@ pub struct VenvArgs {
/// 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 directory 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.
///
/// 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)]
pub allow_existing: bool,
/// Replace an existing directory at the target path.
///
/// By default, `uv venv` will not replace an existing directory. Use `-f` to replace an
/// existing virtual environment, or `-ff` to replace a directory that is not a virtual
/// environment.
///
/// See `--allow-existing` to create a virtual environment in a directory, regardless of its
/// contents, and without clearing it beforehand.
#[clap(long, action = clap::ArgAction::Count)]
pub force: u8,
/// The path to the virtual environment to create.
///
/// Default to `.venv` in the working directory.

View file

@ -281,6 +281,7 @@ impl InstalledTools {
uv_virtualenv::Prompt::None,
false,
false,
uv_virtualenv::VenvForceMode::ReplaceAny,
false,
false,
)?;

View file

@ -38,6 +38,29 @@ impl Prompt {
}
}
// TODO(zanieb): Consider folding `allow_existing` into this?
#[derive(Debug, Clone, Copy)]
pub enum VenvForceMode {
/// Do not replace an existing directory.
Disabled,
/// Replace an existing directory, if it is a virtual environment.
ReplaceEnvironment,
/// Replace an existing directory, regardless of contents.
ReplaceAny,
}
impl VenvForceMode {
pub fn from_args(force: u8) -> Self {
if force == 0 {
VenvForceMode::Disabled
} else if force == 1 {
VenvForceMode::ReplaceEnvironment
} else {
VenvForceMode::ReplaceAny
}
}
}
/// Create a virtualenv.
#[allow(clippy::fn_params_excessive_bools)]
pub fn create_venv(
@ -46,6 +69,7 @@ pub fn create_venv(
prompt: Prompt,
system_site_packages: bool,
allow_existing: bool,
force: VenvForceMode,
relocatable: bool,
seed: bool,
) -> Result<PythonEnvironment, Error> {
@ -56,6 +80,7 @@ pub fn create_venv(
prompt,
system_site_packages,
allow_existing,
force,
relocatable,
seed,
)?;

View file

@ -16,7 +16,7 @@ use uv_python::{Interpreter, VirtualEnvironment};
use uv_shell::escape_posix_for_single_quotes;
use uv_version::version;
use crate::{Error, Prompt};
use crate::{Error, Prompt, VenvForceMode};
/// Activation scripts for the environment, with dependent paths templated out.
const ACTIVATE_TEMPLATES: &[(&str, &str)] = &[
@ -51,6 +51,7 @@ pub(crate) fn create(
prompt: Prompt,
system_site_packages: bool,
allow_existing: bool,
force: VenvForceMode,
relocatable: bool,
seed: bool,
) -> Result<VirtualEnvironment, Error> {
@ -79,11 +80,35 @@ pub(crate) fn create(
format!("File exists at `{}`", location.user_display()),
)));
} else if metadata.is_dir() {
if allow_existing {
debug!("Allowing existing directory");
} else if location.join("pyvenv.cfg").is_file() {
debug!("Removing existing directory");
let is_virtualenv = location.join("pyvenv.cfg").is_file();
let is_empty = !is_virtualenv
&& location
.read_dir()
.is_ok_and(|mut dir| dir.next().is_none());
let should_remove = if !is_empty {
if allow_existing {
debug!("Allowing existing directory due to `--allow-existing`");
false
} else {
match force {
VenvForceMode::Disabled => false,
VenvForceMode::ReplaceEnvironment => {
if is_virtualenv {
debug!("Replacing existing virtual environment due to `-f`");
}
is_virtualenv
}
VenvForceMode::ReplaceAny => {
debug!("Replacing existing directory due to `-ff`");
true
}
}
}
} else {
false
};
if should_remove {
// On Windows, if the current executable is in the directory, guard against
// self-deletion.
#[cfg(windows)]
@ -97,18 +122,20 @@ pub(crate) fn create(
fs::remove_dir_all(location)?;
fs::create_dir_all(location)?;
} else if location
.read_dir()
.is_ok_and(|mut dir| dir.next().is_none())
{
debug!("Ignoring empty directory");
} else {
} else if !is_empty && !allow_existing {
return Err(Error::Io(io::Error::new(
io::ErrorKind::AlreadyExists,
format!(
"The directory `{}` exists, but it's not a virtual environment",
location.user_display()
),
if is_virtualenv {
format!(
"The virtual environment `{}` already exists, use `-f` to replace it",
location.user_display()
)
} else {
format!(
"The directory `{}` already exists, use `-ff` to replace it",
location.user_display()
)
},
)));
}
}

View file

@ -92,6 +92,7 @@ impl CachedEnvironment {
uv_virtualenv::Prompt::None,
false,
false,
uv_virtualenv::VenvForceMode::ReplaceAny,
true,
false,
)?;

View file

@ -1239,6 +1239,7 @@ impl ProjectEnvironment {
prompt,
false,
false,
uv_virtualenv::VenvForceMode::ReplaceAny,
false,
false,
)?;
@ -1276,6 +1277,7 @@ impl ProjectEnvironment {
prompt,
false,
false,
uv_virtualenv::VenvForceMode::ReplaceAny,
false,
false,
)?;
@ -1405,6 +1407,7 @@ impl ScriptEnvironment {
prompt,
false,
false,
uv_virtualenv::VenvForceMode::ReplaceAny,
false,
false,
)?;
@ -1439,6 +1442,7 @@ impl ScriptEnvironment {
prompt,
false,
false,
uv_virtualenv::VenvForceMode::ReplaceAny,
false,
false,
)?;

View file

@ -432,6 +432,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
uv_virtualenv::Prompt::None,
false,
false,
uv_virtualenv::VenvForceMode::ReplaceAny,
false,
false,
)?;
@ -629,6 +630,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
uv_virtualenv::Prompt::None,
false,
false,
uv_virtualenv::VenvForceMode::ReplaceAny,
false,
false,
)?
@ -854,6 +856,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
uv_virtualenv::Prompt::None,
false,
false,
uv_virtualenv::VenvForceMode::ReplaceAny,
false,
false,
)?;

View file

@ -29,6 +29,7 @@ use uv_resolver::{ExcludeNewer, FlatIndex};
use uv_settings::PythonInstallMirrors;
use uv_shell::{shlex_posix, shlex_windows, Shell};
use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, HashStrategy};
use uv_virtualenv::VenvForceMode;
use uv_warnings::warn_user;
use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceCache, WorkspaceError};
@ -58,6 +59,7 @@ pub(crate) async fn venv(
prompt: uv_virtualenv::Prompt,
system_site_packages: bool,
seed: bool,
force: VenvForceMode,
allow_existing: bool,
exclude_newer: Option<ExcludeNewer>,
concurrency: Concurrency,
@ -84,6 +86,7 @@ pub(crate) async fn venv(
seed,
python_preference,
python_downloads,
force,
allow_existing,
exclude_newer,
concurrency,
@ -141,6 +144,7 @@ async fn venv_impl(
seed: bool,
python_preference: PythonPreference,
python_downloads: PythonDownloads,
force: VenvForceMode,
allow_existing: bool,
exclude_newer: Option<ExcludeNewer>,
concurrency: Concurrency,
@ -271,6 +275,7 @@ async fn venv_impl(
prompt,
system_site_packages,
allow_existing,
force,
relocatable,
seed,
)

View file

@ -982,6 +982,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
uv_virtualenv::Prompt::from_args(prompt),
args.system_site_packages,
args.seed,
args.force,
args.allow_existing,
args.settings.exclude_newer,
globals.concurrency,

View file

@ -43,6 +43,7 @@ use uv_settings::{
};
use uv_static::EnvVars;
use uv_torch::TorchMode;
use uv_virtualenv::VenvForceMode;
use uv_warnings::warn_user_once;
use uv_workspace::pyproject::DependencyType;
@ -2390,6 +2391,7 @@ impl BuildSettings {
#[derive(Debug, Clone)]
pub(crate) struct VenvSettings {
pub(crate) seed: bool,
pub(crate) force: VenvForceMode,
pub(crate) allow_existing: bool,
pub(crate) path: Option<PathBuf>,
pub(crate) prompt: Option<String>,
@ -2408,6 +2410,7 @@ impl VenvSettings {
system,
no_system,
seed,
force,
allow_existing,
path,
prompt,
@ -2425,6 +2428,7 @@ impl VenvSettings {
Self {
seed,
force: VenvForceMode::from_args(force),
allow_existing,
path,
prompt,