Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions crates/uv-build-frontend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ use uv_pypi_types::VerbatimParsedUrl;
use uv_python::{Interpreter, PythonEnvironment};
use uv_static::EnvVars;
use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, SourceBuildTrait};
use uv_virtualenv::VenvCreationPolicy;
use uv_warnings::warn_user_once;
use uv_workspace::WorkspaceCache;

Expand Down Expand Up @@ -332,7 +331,7 @@ impl SourceBuild {
interpreter.clone(),
uv_virtualenv::Prompt::None,
false,
VenvCreationPolicy::RemoveDirectory,
uv_virtualenv::OnExisting::Remove,
false,
false,
false,
Expand Down
32 changes: 31 additions & 1 deletion crates/uv-console/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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()?;

Expand Down Expand Up @@ -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()?;
Expand Down
3 changes: 1 addition & 2 deletions crates/uv-tool/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use uv_configuration::PreviewMode;
use uv_dirs::user_executable_directory;
use uv_pep440::Version;
use uv_pep508::{InvalidNameError, PackageName};
use uv_virtualenv::VenvCreationPolicy;

use std::io::{self, Write};
use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -286,7 +285,7 @@ impl InstalledTools {
interpreter,
uv_virtualenv::Prompt::None,
false,
VenvCreationPolicy::RemoveDirectory,
uv_virtualenv::OnExisting::Remove,
false,
false,
false,
Expand Down
6 changes: 3 additions & 3 deletions crates/uv-virtualenv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use thiserror::Error;
use uv_configuration::PreviewMode;
use uv_python::{Interpreter, PythonEnvironment};

pub use virtualenv::VenvCreationPolicy;
pub use virtualenv::OnExisting;

mod virtualenv;

Expand Down Expand Up @@ -52,7 +52,7 @@ pub fn create_venv(
interpreter: Interpreter,
prompt: Prompt,
system_site_packages: bool,
venv_creation_policy: VenvCreationPolicy,
on_existing: OnExisting,
relocatable: bool,
seed: bool,
upgradeable: bool,
Expand All @@ -64,7 +64,7 @@ pub fn create_venv(
&interpreter,
prompt,
system_site_packages,
venv_creation_policy,
on_existing,
relocatable,
seed,
upgradeable,
Expand Down
157 changes: 89 additions & 68 deletions crates/uv-virtualenv/src/virtualenv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ pub(crate) fn create(
interpreter: &Interpreter,
prompt: Prompt,
system_site_packages: bool,
venv_creation_policy: VenvCreationPolicy,
on_existing: OnExisting,
relocatable: bool,
seed: bool,
upgradeable: bool,
Expand All @@ -79,64 +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() {
match venv_creation_policy {
VenvCreationPolicy::OverwriteFiles => {
debug!("Allowing existing directory due to `--allow-existing`");
}
VenvCreationPolicy::RemoveDirectory => {
debug!("Removing existing directory due to `--clear`");
remove_venv_directory(location)?;
}
VenvCreationPolicy::FailIfNotEmpty => {
if location
.read_dir()
.is_ok_and(|mut dir| dir.next().is_none())
{
debug!("Ignoring empty directory");
} else {
let directory_message = if uv_fs::is_virtualenv_base(location) {
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");
}
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 virtual environment exists at `{}`.",
location.user_display()
)
} else {
format!("The directory `{}` exists.", location.user_display())
};

if Term::stderr().is_term() {
if confirm_clear(location)? {
debug!("Removing existing directory");
remove_venv_directory(location)?;
} else {
return Err(Error::Io(io::Error::new(
io::ErrorKind::AlreadyExists,
format!(
"{directory_message} \n\n{}{} Use `{}` to remove the directory first",
"hint".bold().cyan(),
":".bold(),
"--clear".green(),
),
)));
}
} else {
// If this is not a tty, warn for now.
warn_user_once!(
"{directory_message} In the future, uv will require `{}` to remove the directory first",
"--clear".green(),
);
}
"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)?;
}
Expand Down Expand Up @@ -484,16 +494,26 @@ pub(crate) fn create(
})
}

fn confirm_clear(location: &Path) -> Result<bool, io::Error> {
/// 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!(
"The directory `{}` exists. Did you mean to clear its contents (`--clear`)?",
"A {name} already exists at `{}`. Do you want to replace it?",
location.user_display(),
);
uv_console::confirm(&prompt, &term, true)
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(false)
Ok(None)
}
}

Expand All @@ -516,24 +536,25 @@ fn remove_venv_directory(location: &Path) -> Result<(), Error> {
}

#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
pub enum VenvCreationPolicy {
/// Do not create a virtual environment if a non-empty directory exists.
pub enum OnExisting {
/// Fail if the directory already exists and is non-empty.
#[default]
FailIfNotEmpty,
/// Overwrite existing virtual environment files.
OverwriteFiles,
/// Remove existing directory.
RemoveDirectory,
Fail,
/// Allow an existing directory, overwriting virtual environment files while retaining other
/// files in the directory.
Allow,
/// Remove an existing directory.
Remove,
}

impl VenvCreationPolicy {
impl OnExisting {
pub fn from_args(allow_existing: bool, clear: bool) -> Self {
if allow_existing {
VenvCreationPolicy::OverwriteFiles
OnExisting::Allow
} else if clear {
VenvCreationPolicy::RemoveDirectory
OnExisting::Remove
} else {
VenvCreationPolicy::default()
OnExisting::default()
}
}
}
Expand Down
17 changes: 8 additions & 9 deletions crates/uv/src/commands/project/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +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 uv_virtualenv::VenvCreationPolicy;

use crate::commands::pip::loggers::{InstallLogger, ResolveLogger};
use crate::commands::pip::operations::Modifications;
use crate::commands::project::{
Expand All @@ -18,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};

/// A [`PythonEnvironment`] stored in the cache.
#[derive(Debug)]
pub(crate) struct CachedEnvironment(PythonEnvironment);
Expand Down Expand Up @@ -120,7 +119,7 @@ impl CachedEnvironment {
base_interpreter,
uv_virtualenv::Prompt::None,
false,
VenvCreationPolicy::RemoveDirectory,
uv_virtualenv::OnExisting::Remove,
true,
false,
false,
Expand Down
9 changes: 4 additions & 5 deletions crates/uv/src/commands/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ use uv_scripts::Pep723ItemRef;
use uv_settings::PythonInstallMirrors;
use uv_static::EnvVars;
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy};
use uv_virtualenv::VenvCreationPolicy;
use uv_warnings::{warn_user, warn_user_once};
use uv_workspace::dependency_groups::DependencyGroupError;
use uv_workspace::pyproject::PyProjectToml;
Expand Down Expand Up @@ -1340,7 +1339,7 @@ impl ProjectEnvironment {
interpreter,
prompt,
false,
VenvCreationPolicy::RemoveDirectory,
uv_virtualenv::OnExisting::Remove,
false,
false,
upgradeable,
Expand Down Expand Up @@ -1379,7 +1378,7 @@ impl ProjectEnvironment {
interpreter,
prompt,
false,
VenvCreationPolicy::RemoveDirectory,
uv_virtualenv::OnExisting::Remove,
false,
false,
upgradeable,
Expand Down Expand Up @@ -1531,7 +1530,7 @@ impl ScriptEnvironment {
interpreter,
prompt,
false,
VenvCreationPolicy::RemoveDirectory,
uv_virtualenv::OnExisting::Remove,
false,
false,
upgradeable,
Expand Down Expand Up @@ -1567,7 +1566,7 @@ impl ScriptEnvironment {
interpreter,
prompt,
false,
VenvCreationPolicy::RemoveDirectory,
uv_virtualenv::OnExisting::Remove,
false,
false,
upgradeable,
Expand Down
Loading
Loading